ergon_graph (ergon v0.5.0)

View Source

Workflow dependency resolution over PostgreSQL 19's SQL/PGQ property graph.

The ergon.workflow graph maps jobs and edges into job vertices connected by triggers edges, so "which jobs are ready to run?" is a single GRAPH_TABLE match rather than a hand-rolled join. Its vertex table is ergon.jobs_current, not ergon.jobs: under the temporal primary key a job owns one row per valid-time version, and using the base table gave a completed parent one vertex per version, which made ready_children/0 return nothing for any parent that had ever run. See the property graph comment in priv/migrations/schema.

These are observability queries, not the scheduler

Dependencies are enforced in checkout.sql through ergon.jobs.pending_parents, so a blocked job is simply not checked out and nothing here needs to be consulted for correctness. ready_children/0 and direct_children/1 answer "what is the workflow doing?", which is what a dashboard or an operator wants.

Multi-hop reachability uses a recursive CTE rather than a graph walk. PG19's SQL/PGQ has no path quantifiers, so variable-length reachability cannot be expressed in MATCH at all.

Summary

Functions

Every job id reachable from AncestorId through triggers edges: its transitive closure, and the set a cascade would touch.

The ids of the available jobs a completed job directly unblocks: everything one triggers hop away.

The ids of every available job whose workflow parents have all completed.

Whether adding ParentId -> ChildId would introduce a cycle, including a self-loop.

Types

db_error()

-type db_error() ::
          empty_result | would_create_cycle |
          {job_not_found, ergon_job:job_id()} |
          {pgo_error, map()} |
          term().

Functions

descendants(AncestorId)

-spec descendants(ergon_job:job_id()) -> {ok, [ergon_job:job_id()]} | {error, db_error()}.

Every job id reachable from AncestorId through triggers edges: its transitive closure, and the set a cascade would touch.

direct_children(ParentId)

-spec direct_children(ergon_job:job_id()) -> {ok, [ergon_job:job_id()]} | {error, db_error()}.

The ids of the available jobs a completed job directly unblocks: everything one triggers hop away.

ready_children()

-spec ready_children() -> {ok, [ergon_job:job_id()]} | {error, db_error()}.

The ids of every available job whose workflow parents have all completed.

Equivalently: the jobs whose pending_parents has just reached zero. The two are maintained independently (one by triggers on the row, one by a graph match), so a disagreement between them means the counter has drifted, which makes this a useful cross-check for the reconciler as well as for a dashboard.

would_create_cycle(ParentId, ChildId)

-spec would_create_cycle(ergon_job:job_id(), ergon_job:job_id()) ->
                            {ok, boolean()} | {error, db_error()}.

Whether adding ParentId -> ChildId would introduce a cycle, including a self-loop.

ergon_db:link/3 runs this itself, inside a transaction and behind an advisory lock, so calling it beforehand is advisory only: between this answer and a later link/3 another process may have added the edge that closes the loop.