Every config :reactive_dag, … key the library reads, what it does, and when
you would change it.
Only :repo is required. Everything else has a working default, and most
hosts never touch more than two or three.
# config/config.exs — a typical host
config :reactive_dag, repo: MyApp.RepoThe keys
| key | default | required? | read by |
|---|---|---|---|
:repo | — | yes | Frontier |
:dirty_table | "reactive_dag_dirty" | no | Frontier, Migration |
:plan_mfa | — | only with ScanWorker | ScanWorker |
:insights_keep | 20 | no | Insights |
:drain_enqueuer | DrainWorker.enqueue/0 | no | dirties_on schedule_drain: |
:around_poll | — | no | ScanWorker |
:repo
Your AshPostgres repo. The library goes through it with raw SQL for the one
table it owns — the dirty frontier — because claim-as-delete
(DELETE … RETURNING) and the coalescing upserts don't express cleanly as Ash
actions.
config :reactive_dag, repo: MyApp.RepoThe only required key. Omitting it raises on the first query — which may be a long way into a deploy — so validate at boot instead (below).
:dirty_table
The physical table name for the frontier.
config :reactive_dag, dirty_table: "my_existing_dirty"This exists so a host adopting the library keeps its table without a rename — both current hosts grew their own frontier table before the library existed. On a green-field app, leave it alone.
The name is the one identifier SQL cannot parameterise, so it is validated against an identifier grammar at read time: a typo fails loudly rather than as a syntax error deep inside a query.
ReactiveDag.Migration resolves :dirty_table exactly as Frontier does, so
a host that sets the config gets a migration matching the table the runtime
queries — there is no second place to keep in sync.
:plan_mfa
How ReactiveDag.ScanWorker builds the plan it scans.
config :reactive_dag, plan_mfa: {MyApp.Dag, :plan, []}A plan is built from resource modules at runtime, so it cannot ride in an Oban
job argument. The worker needs one name for it; this is that name. A job may
override it (%{"plan_mfa" => ["MyApp.Dag", "plan", []]}), which is what lets
one app schedule scans over more than one graph.
Only read by ScanWorker. A host scheduling its own polls never needs it.
:drain_enqueuer
How schedule_drain: true queues the drain that consumes a dirties_on mark.
config :reactive_dag, drain_enqueuer: fn -> MyApp.DrainJob.enqueue() endThe default enqueues ReactiveDag.DrainWorker on its :drain queue, which is
almost always what you want. Override it when the drain needs to be YOUR job —
a different queue, a longer debounce, or a wrapper that records the run id your
activity page groups by.
Called from inside the write's transaction, so it must be cheap: an INSERT, not
the drain itself. Must return {:ok, term} or {:error, term}; an error is
logged and swallowed, because the mark is already durable and a queue being down
should not fail a user's write.
Only read when a node declares dirties_on … schedule_drain: true.
:insights_keep
How many runs ReactiveDag.Insights.record/1 retains in its rolling in-memory
window.
config :reactive_dag, insights_keep: 50Only relevant if you call Insights.record/1 (neither the scan nor the drain
persists anything on its own — the library reports, the host records). It takes
a %ReactiveDag.ScanRun{} (a scan: the poll AND the drain it triggered) or a
bare %ReactiveDag.Drain.Report{} (a drain triggered directly), and retains a
run either way — so a log line can show the poll's duration, changed keys, cost
and unreachable list rather than only the drain's much smaller share.
The buffer is per-BEAM-node, in memory, and lost on restart; it exists so a dashboard has something to show without the host building storage. A host wanting history stores the run where its runs already live.
Validating at boot
Misconfiguration otherwise surfaces at the first query, in whatever process
happened to trigger it. ReactiveDag.Config.validate!/0 moves that to boot:
def start(_type, _args) do
ReactiveDag.Config.validate!()
Supervisor.start_link(children, opts)
end** (ReactiveDag.Config.Error) reactive_dag is misconfigured:
* `:repo` is not set (required) — add `config :reactive_dag, repo: MyApp.Repo`
* `:dirty_table` "my dirty" is not a valid SQL identifierIt reports every problem, not the first — a config with two mistakes
should take one deploy to fix. ReactiveDag.Config.problems/0 returns the same list without
raising, for a host that would rather log them.
The host calls it; the library does not start its own application to do it
automatically. That would give reactive_dag a supervision tree and an opinion
about when it starts, and would make the check unskippable by tests that
deliberately run unconfigured.
It checks only what is definitely wrong, and touches no database — whether
the tables exist is ReactiveDag.Migration's business, and a boot check that
queried would make booting depend on the database being reachable.
What is not configured here
- Scheduling — when to call
Drain.run/2is the host's. See Getting started. - How a node recomputes, and how its changes propagate — declared in the
node's
reactiveblock and read off the plan, not configured and not passed per call.run/2's only option is:max_passes. - Per-node behaviour —
payload_key,payload_action,key_rule,recompute_byand the rest are declared in a resource'sreactiveblock, not in application config. See Authoring nodes.
:around_poll
A wrapper ScanWorker runs the POLL inside, for the one thing a telemetry
handler cannot do: be present while the fetch happens.
config :reactive_dag, around_poll: {MyApp.Audit, :around, []}The function takes the job's args and a one-arity function, and returns
whatever that function returns. Its argument is a keyword list merged into the
poll's options — which is the point: a wrapper that starts a collector hands
the scanner a way to reach it.
def around(_args, run) do
{:ok, pid} = Agent.start_link(fn -> [] end)
try do
run.(collector: pid)
after
persist(Agent.get(pid, & &1))
Agent.stop(pid)
end
endReach for a telemetry handler first. Broadcasts, durable scan rows,
follow-up enqueues — everything that only needs the RESULT — belongs on
[:reactive_dag, :scan, :stop], which carries a ReactiveDag.ScanRun with
both phases. A wrapper puts work inside the job's failure boundary, so use it
only when the work must happen DURING the poll.
The case it exists for: a host that records every HTTP request its crawler makes needs something live for the duration, and a process cannot ride in an Oban argument. Without this, that host forks the worker — and then owns the poll/mark/drain loop forever to keep one wrapper.