analyze-read-replica-risk
Analyze Read Replica Risk
Surge.ReadRepo is a read replica with ~20ms average lag behind the primary Surge.Repo. Moving a query to it (Repo.all/one/get/preload → ReadRepo.*) is safe ONLY if every caller can tolerate reading data that may be up to ~20ms stale. Your job: given a query function, trace its full usage context and check each call path against the red flags below, then report which paths (if any) trip one.
This is a procedural scan, not a data-imagination exercise. Don't enumerate every way a stale field or a missing row could ripple through the system. Instead, walk each path and ask one question: does it match one of the red-flag patterns below? The red flags are the structural signatures of a lag problem - if a path matches none of them and fits a green-light signal, it's safe.
Workflow
-
Locate the query. Find the function definition the user named. Confirm it actually runs DB reads and isn't already on
ReadRepo. -
Trace the full call graph - not just direct callers. This is the core of the task. Lag risk usually lives several layers up, not at the query itself. Be exhaustive: under-counting paths is the most common way this analysis goes wrong.
- Find direct callers (Grep the function name across
lib/;test/is for context only). - For each caller, find ITS callers. Repeat up the chain - parents, grandparents, and beyond - until you reach a stable entry point (controller action, LiveView event, Oban worker
perform/1, channel handler, webhook handler, GenServer callback, public context function with many external callers). - Don't stop at an Oban worker
perform/1- climb to who enqueues the job. A worker is a fan-in point: Grep for the worker module (.new(,.insert(,Oban.insert) to find every enqueue site. Different entry points (e.g. different provider webhooks) often funnel into one worker, and each is a distinct path. The enqueue-then-job hop is itself a timing gap where the job can start within ms of the enqueuing write. - Expand dispatcher/multiplexer functions. A single function (e.g. a controller
create/2, a catch-all webhook handler, ahandle_event/3) often serves many event types or routes. Treat each meaningfully different trigger as its own path rather than collapsing them. - Follow shared helpers and
defdelegates. If the query is reached through a wrapper, plug, or delegated function, trace callers of the wrapper too. - Stop climbing a branch only when you understand what triggers it. If a branch fans out very widely into clearly equivalent leaves, note the entry-point category once - but only after confirming the leaves really are equivalent.
- Find direct callers (Grep the function name across
-
Check each call path against the red flags below. For each path, scan for the red-flag patterns. Track each distinct path separately - one path may trip a flag while another is clean. A single path that trips a "do NOT move" flag means the query stays on primary (unless you split it).