GenServer managing compute locks for cache-aside with stampede protection.
When a cache miss occurs, only one client across the cluster is allowed to compute the value (the "computer"). All other clients requesting the same key block as "waiters" until the computer delivers the result or an error.
Cluster-wide stampede protection
Stampede protection uses a Raft-replicated distributed lock keyed on
"__foc_lock__:" <> key. Because Router.lock/4 is forced through the
quorum path (see Router.always_quorum?/1), exactly one caller across the
whole cluster wins the lock and receives {:compute, hint}. All other
callers — local or remote — fall into a poll-wait loop on Router.get/2
until the computer publishes the value (also Raft-replicated) or the
compute_timeout expires.
Local fast-path waiter coordination
Within a single node we still maintain a local ETS table of waiters. When
a caller on the same node sees an existing local lock, it parks as a
waiter and is woken directly via GenServer.reply/2 when the computer
calls fetch_or_compute_result/3 on this node. This avoids the polling
overhead for same-node waiters; cross-node waiters poll.
Timeout handling
If the computer does not publish a result within compute_timeout_ms
(default 30 seconds), waiters' polling loops give up with
{:error, :timeout}. The Raft lock has its own TTL (also
compute_timeout_ms) so it auto-expires and the next caller can retry.
Summary
Functions
Returns a specification to start this module under a supervisor.
Attempts to fetch a key, or initiates a cluster-wide compute lock.
Reports a compute error. Releases the lock, wakes local waiters with the error.
Delivers the computed result for a key.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec fetch_or_compute(binary(), pos_integer(), binary()) :: {:hit, binary()} | {:compute, binary()} | {:ok, binary()} | {:error, term()}
Attempts to fetch a key, or initiates a cluster-wide compute lock.
Returns:
{:hit, value}— cache hit{:compute, hint}— caller won the lock and must compute the value, then callfetch_or_compute_result/3{:ok, value}— caller waited and received the result from another node{:error, reason}— compute failed or timed out
Reports a compute error. Releases the lock, wakes local waiters with the error.
@spec fetch_or_compute_result(binary(), binary(), pos_integer()) :: :ok
Delivers the computed result for a key.
Stores the value via Router (Raft-replicated to all nodes), releases the distributed lock, and wakes all local waiters.
@spec start_link(keyword()) :: GenServer.on_start()