SuperCache.Cluster.Router (SuperCache v1.3.0)

Copy Markdown View Source

Routes SuperCache operations to the correct primary node and applies replication after each write.

Routing contract

  1. Determine the partition order (integer index) for the operation from the data tuple or explicit partition argument via Partition.get_partition_order/1.
  2. Look up {primary, replicas} from Manager.get_replicas/1 (zero-cost :persistent_term read).
  3. If node() == primary → apply locally, then call Replicator.replicate/3.
  4. Otherwise → forward the entire operation to the primary via :erpc, which applies and replicates it. Forwarded calls never forward again (detected via a :forwarded flag in opts) to prevent cycles.

Anti-cycle guard

Every outbound :erpc call appends forwarded: true to its opts list. A function that receives forwarded: true always executes locally and skips the primary check, preventing infinite forwarding chains when the partition map is momentarily inconsistent.

No anonymous functions across node boundaries

All :erpc calls pass only plain, serializable Erlang terms — integers, atoms, and tuples. Anonymous functions (closures) are never passed via :erpc because Erlang fun serialization is fragile: the remote node must have the identical module version, otherwise the call raises badfun. Instead, every remote read goes through the explicit public dispatcher local_read/3, which takes an operation atom (:get | :match | :match_object) and a plain argument.

3PC writes

When Manager.replication_mode/0 returns :strong, writes are handed to ThreePhaseCommit.commit/2 on the primary instead of the normal local-write + async/sync replicate path.

Read-your-writes consistency

When a process writes a key, the Router records the partition order in a per-process ETS table. Subsequent reads of the same partition (within a configurable TTL) are automatically routed to the primary node, ensuring the reader sees its own writes even in :local read mode.

The tracking table is cleaned up lazily — entries older than the TTL are pruned on each write. This adds negligible overhead (~100ns per write) while providing strong read-your-writes guarantees without requiring read_mode: :primary on every call.

Summary

Functions

Route a key-based delete to the correct primary.

Delete all records — one routed call per partition.

Route a delete by explicit key + partition value to the correct primary.

Route a match-based delete, one partition order at a time.

Route a key-based get.

Route a get by explicit key + partition value.

Route a match-pattern scan across one or all partitions.

Route a match-object scan across one or all partitions.

Route a put to the correct primary, then replicate.

Route a batch of puts to the correct primary, then replicate.

Fold over local ETS — always local, never forwarded.

Functions

route_delete!(data, opts \\ [])

@spec route_delete!(
  tuple(),
  keyword()
) :: :ok

Route a key-based delete to the correct primary.

route_delete_all()

@spec route_delete_all() :: :ok

Delete all records — one routed call per partition.

route_delete_by_key_partition!(key, partition_data, opts \\ [])

@spec route_delete_by_key_partition!(any(), any(), keyword()) :: :ok

Route a delete by explicit key + partition value to the correct primary.

route_delete_match!(partition_data, pattern)

@spec route_delete_match!(any(), tuple()) :: :ok

Route a match-based delete, one partition order at a time.

route_get!(data, opts \\ [])

@spec route_get!(
  tuple(),
  keyword()
) :: [tuple()]

Route a key-based get.

route_get_by_key_partition!(key, partition_data, opts \\ [])

@spec route_get_by_key_partition!(any(), any(), keyword()) :: [tuple()]

Route a get by explicit key + partition value.

route_get_by_match!(partition_data, pattern, opts \\ [])

@spec route_get_by_match!(any(), tuple(), keyword()) :: [[any()]]

Route a match-pattern scan across one or all partitions.

route_get_by_match_object!(partition_data, pattern, opts \\ [])

@spec route_get_by_match_object!(any(), tuple(), keyword()) :: [tuple()]

Route a match-object scan across one or all partitions.

route_put!(data, opts \\ [])

@spec route_put!(
  tuple(),
  keyword()
) :: true

Route a put to the correct primary, then replicate.

route_put_batch!(data_list, opts \\ [])

@spec route_put_batch!(
  [tuple()],
  keyword()
) :: :ok

Route a batch of puts to the correct primary, then replicate.

Groups data by partition order and sends each group in a single :erpc call, dramatically reducing network overhead for bulk writes.

Example

Router.route_put_batch!([
  {:user, 1, "Alice"},
  {:user, 2, "Bob"},
  {:session, "tok1", :active}
])

route_scan!(partition_data, fun, acc)

@spec route_scan!(any(), (any(), any() -> any()), any()) :: any()

Fold over local ETS — always local, never forwarded.