ReactiveDag.Insights (reactive_dag v0.17.0-rc)

Copy Markdown View Source

The engine, viewed from outside: what the graph LOOKS like, what state each cell is in, and what the last drains actually did.

Everything here is a READ. Nothing new is computed — the plan already carries the structure and depths, the coordination tuple already carries per-key status and freshness, and ReactiveDag.Drain.Report is already a complete causal trace. This module is those three assembled into the shape a human (or a dashboard, a mix task, an alerting check) actually asks for.

Deliberately UI-free: no Phoenix, no rendering, no assumptions about a web layer. reactive_dag_dashboard renders this; a host with no web layer at all can still call summary/1 from a mix task or a health check.

Retaining reports

Drain.run/2 returns a %Report{} and most callers discard it — the drain deliberately does not persist anything (see ReactiveDag.Drain.Report: the library reports, the host records). For a rolling window without a host writing its own storage, record/1 keeps the last N in memory:

{:ok, report} = ReactiveDag.Drain.run(plan, opts)
ReactiveDag.Insights.record(report)

recent/1 then reads them back, newest first. This is an opt-in observer, not a durable log: the buffer is per-node, in-memory, and lost on restart. A host that needs history stores the report where its runs already live.

Summary

Types

A cell's observable state.

Functions

One cell's observable state: its declaration (depth, inputs, shape) plus its live coordination state (status histogram, key count, freshness, and a small sample of failing keys for the drawer).

The graph's edges as {from, to} pairs — an input edge points from the input cell TO the cell that reads it, i.e. the direction change flows.

Forget every retained report.

The most recent report, or nil if none has been recorded.

The graph's shape: cells grouped by depth, in execution order.

Cell ids with dirty keys waiting — what the NEXT drain would work on.

The most recent reports, newest first (default all retained).

Keep report in the rolling in-memory window (default 20, configurable with config :reactive_dag, insights_keep: n).

Every cell's state, in execution order — the dashboard's landing view.

Types

cell_status()

@type cell_status() :: %{
  id: String.t(),
  depth: non_neg_integer(),
  inputs: [String.t()],
  leaf?: boolean(),
  verdict?: boolean(),
  op: atom() | nil,
  statuses: %{required(String.t()) => non_neg_integer()},
  key_count: non_neg_integer(),
  last_observed_at: DateTime.t() | nil,
  failing_sample: [String.t()]
}

A cell's observable state.

Functions

cell_status(plan, cell_id)

@spec cell_status(ReactiveDag.Plan.t(), String.t()) :: cell_status() | nil

One cell's observable state: its declaration (depth, inputs, shape) plus its live coordination state (status histogram, key count, freshness, and a small sample of failing keys for the drawer).

The status read hits the tuple table, so this is a query per cell — call it for the cells being displayed, not the whole graph, unless the graph is small (summary/1 does exactly that, with the same caveat).

edges(plan)

@spec edges(ReactiveDag.Plan.t()) :: [{String.t(), String.t()}]

The graph's edges as {from, to} pairs — an input edge points from the input cell TO the cell that reads it, i.e. the direction change flows.

forget_reports()

@spec forget_reports() :: :ok

Forget every retained report.

last_report()

@spec last_report() :: %{report: ReactiveDag.Drain.Report.t(), at: DateTime.t()} | nil

The most recent report, or nil if none has been recorded.

levels(plan)

@spec levels(ReactiveDag.Plan.t()) :: [{non_neg_integer(), [ReactiveDag.Cell.t()]}]

The graph's shape: cells grouped by depth, in execution order.

Depth is the longest path from a leaf, so every cell in a level can only depend on shallower ones — which is exactly the order the drain runs them in, and therefore the order worth drawing them in.

pending(plan)

@spec pending(ReactiveDag.Plan.t()) :: [String.t()]

Cell ids with dirty keys waiting — what the NEXT drain would work on.

Reads the frontier rather than the tuple table: a cell is pending because something dirtied it, whether or not its tuples have changed yet.

recent(limit \\ :all)

@spec recent(pos_integer() | :all) :: [
  %{report: ReactiveDag.Drain.Report.t(), at: DateTime.t()}
]

The most recent reports, newest first (default all retained).

record(report)

Keep report in the rolling in-memory window (default 20, configurable with config :reactive_dag, insights_keep: n).

Opt-in: the drain persists nothing on its own. Returns the report, so it drops into a pipeline:

plan |> ReactiveDag.Drain.run(opts) |> then(fn {:ok, r} -> Insights.record(r) end)

summary(plan)

@spec summary(ReactiveDag.Plan.t()) :: [cell_status()]

Every cell's state, in execution order — the dashboard's landing view.

One status query per cell. Fine for the graphs this library is built for (tens of cells); for a very large graph, page it with cell_status/2 instead.