Ankusa.WAL.Postgres (ankusa_postgres v0.1.0)

Copy Markdown View Source

Shared, multi-node Ankusa.WAL backed by Postgres. This is the adapter that turns "many ingest servers" into a real fleet: every BEAM node runs its own local Postgrex connection pool (registered under the same Ankusa.via(instance, :wal) name DiskLog would use), and coordination between nodes happens entirely through the shared database — no BEAM distribution, no RPC between edge nodes, matching the framework's existing rule that components hand off through durable state, never through each other.

Group commit, translated to SQL

append/2 runs inside one Postgrex.transaction/2 (still exactly one COMMIT, i.e. one fsync, per batch — matching the core invariant):

  1. Claim dedup keys. A single INSERT ... ON CONFLICT DO NOTHING against ankusa_wal_dedup, batched via unnest/1. Postgres takes a row lock on the conflicting index entry and blocks until the other writer's transaction resolves, so two nodes racing the same dedup key never double-claim it — the loser reliably sees the winner's committed row afterward.
  2. Insert winners. Rows that either had no dedup key or won their claim are inserted into ankusa_wal (again batched via unnest/1), RETURNING event_id, seq. A record whose dedup key collided is never written here — the same "duplicate absorbed, nothing extra stored" contract as WAL.DiskLog.
  3. Resolve losers' seq. For rows that lost their dedup claim, one lookup joins ankusa_wal_dedup back to ankusa_wal by event_id to find the seq of the row that already owns that key.

Every row is correlated by the envelope's own id (a UUIDv7, always unique per envelope regardless of dedup key), never by array position — positional matching against RETURNING is not guaranteed to preserve input order.

Config

config :ankusa,
  wal: {Ankusa.WAL.Postgres, hostname: "localhost", port: 5433,
        username: "ankusa", password: "ankusa", database: "ankusa_dev",
        pool_size: 10}

Any Postgrex.start_link/1 option is accepted and passed through verbatim (:migrate false skips the DDL bootstrap, e.g. if you run migrations separately in CI).

Summary

Functions

child_spec(opts)

start_link(opts)