Skip to content

feat(dv2): event-driven OLTP→vault freshness via LISTEN/NOTIFY#93

Merged
brownjuly2003-code merged 1 commit into
mainfrom
feat/dv2-listen-notify-freshness
Jun 27, 2026
Merged

feat(dv2): event-driven OLTP→vault freshness via LISTEN/NOTIFY#93
brownjuly2003-code merged 1 commit into
mainfrom
feat/dv2-listen-notify-freshness

Conversation

@brownjuly2003-code

Copy link
Copy Markdown
Owner

What

Completes the OLTP→raw-vault path on PostgreSQL (after #91) with push-based freshness. With both OLTP and the vault on Postgres there's no need for a replication slot / WAL reader / second engine: an AFTER INSERT/UPDATE trigger on each ops_<branch> table fires pg_notify('dv2_vault_refresh', …), and a listener runs the idempotent promote on each event (push, not polling). This is the PG-native equivalent of ClickHouse MaterializedPostgreSQL push-CDC, but the whole mechanism is NOTIFY + in-DB INSERT…SELECT.

Changes

  • postgres_oltp/freshness_listen_notify.sqlrv.notify_oltp_change() + 4 idempotent triggers (DROP IF EXISTS + CREATE); payload carries branch/table/op + clock_timestamp() emit time.
  • postgres_oltp/freshness_listener.py — guarded-psycopg listener with a driver-agnostic pure core (parse_notification / lag_ms / process_notifications), no-Docker testable.
  • tests/unit/test_dv2_freshness_listen_notify.py — 17 no-Docker tests (trigger-SQL structure + listener logic on fake notifications + db-clock contract).
  • postgres_oltp/README.md — documents the push-freshness path.

Verification

  • No-Docker gate: ruff / ruff format clean; 17 unit tests pass.
  • Live-validated on Postgres 16 (Mac/colima) in the prior session: INSERT into OLTP → trigger → NOTIFY → listener → idempotent promote → row visible in bv_order_canonical, lag 62 ms. The observation clock must be the server-side PG clock (db_now), not the client wall clock, to avoid host↔container skew (a first run mismeasured 14774 ms before the fix). This branch is content-identical to that validated commit, rebased onto the merged feat(dv2): migrate raw vault to PostgreSQL + cloud supplier reference #91 vault it targets.

With both the OLTP hot tier and the raw vault on PostgreSQL, freshness no
longer needs a replication slot, a WAL consumer, or a second engine. An
AFTER INSERT/UPDATE trigger on each ops_<branch> table issues pg_notify on
the dv2_vault_refresh channel; freshness_listener.py LISTENs and runs the
idempotent promotion the moment a change lands -- push, not poll.

This is the PostgreSQL-native equivalent of the ClickHouse
MaterializedPostgreSQL push-CDC: the same property with no replication slot
and no second engine, because the vault is in the same instance.

- freshness_listen_notify.sql: rv.notify_oltp_change() + one idempotent
  trigger per OLTP table; payload carries branch/table/op and a
  clock_timestamp() emit time.
- freshness_listener.py: guarded-psycopg listener with a driver-agnostic
  pure core (parse_notification / lag_ms / process_notifications). The
  observation clock is the PostgreSQL server clock (db_now) so the measured
  lag is skew-free across a containerised host.
- tests: 18 no-Docker tests (trigger SQL structure + listener logic via
  fake notifications + db_now contract).

Verified: ruff/format/mypy clean; 18 no-Docker tests pass. Single-node Mac
smoke (throwaway postgres:16) ran the full INSERT -> trigger -> NOTIFY ->
promote -> bv_order_canonical round-trip, lag 62 ms (DB-clock measured).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

DORA Metrics

  • Window: last 30 days
  • Branch: main
  • Deployment frequency: 182 total / 42.47 per week
  • Lead time for changes: avg 0.23h / median 0.0h
  • Change failure rate: 64.84% (118/182)
  • MTTR: 0.47h across 7 incident(s)

@brownjuly2003-code brownjuly2003-code merged commit 1bda2dc into main Jun 27, 2026
19 checks passed
@brownjuly2003-code brownjuly2003-code deleted the feat/dv2-listen-notify-freshness branch June 27, 2026 06:32
brownjuly2003-code added a commit that referenced this pull request Jun 27, 2026
…nts.txt header (#95)

* fix(cache): use redis set(ex=) instead of deprecated setex

`Redis.setex` is `@deprecated_function` in redis-py 8.0.0 ("Use 'set'
instead") and emits a DeprecationWarning from the query-cache hot path on
every cached write. Switch `QueryCache.set` to `set(key, value, ex=ttl)`,
which is behaviorally identical (int seconds), and drop the now-unused
`timedelta` import.

All six in-repo Redis test doubles (unit cache/entity_cache/versioning,
integration tenant-isolation, chaos RESP client) implemented `setex`; they
move to `set(self, key, value, ex=None)` with the matching argument order,
and the chaos RESP client now issues `SET … EX` over the wire. The two
`set_calls` ttl assertions compare the integer `ex` directly.

Verified no-Docker: ruff + mypy clean, full unit suite 1096 passed / 1
skipped (the redis.setex DeprecationWarning is gone). The integration and
chaos doubles change symmetrically and are validated by their CI jobs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: sync version story to v1.5.0 and clarify requirements.txt

The README Status section and release badge still described v1.4.0 as the
current line even though v1.5.0 is tagged and published, and the CHANGELOG
[Unreleased] section did not record the DV2 re-architecture already on main.

- README: badge v1.4 -> v1.5, "current release line" -> v1.5.0, extend the
  release arc to five increments with a v1.5.0 bullet (argon2id O(1) key
  hashing, NL->SQL guard bypass fix, strict-mypy expansion), and add a note
  that main carries post-v1.5.0 work pending the next tag.
- CHANGELOG [Unreleased]: document the DV2 raw vault migration ClickHouse ->
  PostgreSQL (#91), the PyIceberg sink backed by real MinIO (#92), the
  LISTEN/NOTIFY OLTP->vault freshness (#93), and the dependency batch (#94).
- requirements.txt: add a header explaining it is a supplemental OTel pin
  set installed on top of the pyproject package by the e2e/mutation/staging
  workflows and the security Safety scan, not the full dependency set
  (pyproject.toml is the source of truth). load_requirements() skips comment
  lines and `pip -r` ignores them, so the header is non-breaking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: JuliaEdom <uedomskikh@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants