Skip to content

Add LISTEN/NOTIFY: schema-change notifications and pub/sub#111

Merged
roed314 merged 3 commits into
roed314:masterfrom
roed-math:listen-notify
Jul 22, 2026
Merged

Add LISTEN/NOTIFY: schema-change notifications and pub/sub#111
roed314 merged 3 commits into
roed314:masterfrom
roed-math:listen-notify

Conversation

@roed-math

@roed-math roed-math commented Jul 21, 2026

Copy link
Copy Markdown

Adds LISTEN/NOTIFY support: schema-change notifications plus a small pub/sub primitive, built on psycopg3.

  • Schema operations (create_table, drop_table, rename_table, add_column, drop_column, the reload swap) emit pg_notify on channel psycodict_schema with the table name as payload — transactionally on the main connection, so notifications are delivered on commit and dropped on rollback (tested both ways). rename_table announces both the old and new names.
  • New psycodict/notifications.py: a pull-based NotificationListener on a dedicated autocommit connection (poll(timeout)/listen() iterator/context manager, using psycopg 3.2's notifies(timeout, stop_after) so polls return the instant a notification arrives), plus db.notify(channel, payload) and db.listener(). No threads, no callbacks, no silent auto-reconnect — deliberately minimal for v1, each documented.
  • The intended follow-up — a long-running website process refreshing its table metadata off psycodict_schema — is sketched in the docstrings without depending on the separate refresh-tables PR (Add db.refresh_tables() so schema changes don't require restarts #99).
  • Docs (added while wiring up the LMFDB consumer, Refresh table metadata when psycodict announces schema changes roed-math/lmfdb#45): notifications.py gained Forking (create listeners per-worker after fork; abandon an inherited one without close() — psycopg's pid-guarded GC makes dropping the reference safe, while an explicit close would kill the parent's copy) and Hot standbys (a server in recovery refuses LISTEN outright, SQLSTATE 25006, and NOTIFY is not WAL-logged — treat as permanent and fall back to scheduled/on-error refresh; verified live against devmirror, which is a physical replica).
  • 17 tests: cross-connection delivery, every schema hook, rollback vs commit semantics, channel isolation, listener lifecycle.

🤖 Generated with Claude Code

roed314 and others added 3 commits July 22, 2026 00:56
Introduce psycodict/notifications.py with a pull-based NotificationListener
that opens its own autocommit connection and collects notifications via
poll()/listen(), plus PostgresDatabase.notify()/listener() and an internal
_notify_schema_change() hook.

Schema-changing operations (create_table, drop_table, rename_table,
add_column, drop_column, and the reload_final_swap) now emit pg_notify on the
"psycodict_schema" channel with the affected table name as payload.  Emission
goes through _execute on the main connection so it is transactional:
delivered on commit, dropped on rollback.  rename_table announces both the old
and new names.

Reconnection, JSON payloads, background threads/callbacks, and auto-refresh of
table metadata are intentionally out of scope for v1 and documented as such.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PostgresDatabase(config=..., **kwargs) lets a caller override host, dbname,
user, etc. for every connection it opens, but listener() built the
NotificationListener from self.config alone, dropping those overrides -- so a
listener could subscribe on a different server or database than the sender
writes to.  Remember the kwargs and forward them (NotificationListener already
accepts **connect_kwargs and applies them over the config, exactly like
_new_connection).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two operational notes for NotificationListener, prompted by wiring the
LMFDB website up to the schema channel:

- Forking: a listener must be created in the process that polls it; an
  inherited one must be abandoned without close() (psycopg skips the
  protocol shutdown when collecting a connection in a different process,
  so dropping the reference is safe, while an explicit close would kill
  the parent's copy through the shared socket).
- Hot standbys: NOTIFY is not WAL-logged and a server in recovery
  refuses LISTEN outright (SQLSTATE 25006, verified against devmirror,
  a physical replica) -- consumers should treat that as permanent for
  the server and fall back to scheduled or on-error refreshes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@roed314
roed314 merged commit 554818e into roed314:master Jul 22, 2026
16 checks passed
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