Install mesh_node_notify per TABLE, not once per database (V54) - #1816
Merged
Conversation
MEASURED on a live 33-partition database: `mesh_node_notify` was present on
exactly ONE schema's mesh_nodes and missing from 32. The one schema that had it
is `public` — which is empty by design — so no node write in that database had
ever emitted pg_notify.
Root cause, in three schema scripts:
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'mesh_node_notify')
`pg_trigger` is a database-global catalog; trigger names are unique per TABLE.
So the first schema provisioned satisfied the predicate for the whole database
and every later partition schema silently skipped its own trigger. Nothing
failed — each schema decided the work was already done, because elsewhere it was.
This is the identical defect V44 repaired for the sibling trigger
mesh_node_copy_to_history, in one of the same methods. That repair fixed one
trigger and left its neighbour.
Why it survived: satellite tables were never affected (GetSatelliteTableScript
always used the per-table form — 231 correctly installed in the same database),
so a partition's _Access/_Thread/_Activity writes notified while that same
partition's mesh_nodes writes did not. And the in-process IStorageAdapter.Changes
feed still carries same-process writes, so only a notification that had to cross
a process boundary was lost, silently.
Fix, both halves:
- The three scripts now use the per-table DROP/CREATE form the other ~20 triggers
in the file already use, so new schemas are correct.
- V54 installs the trigger on existing schemas that lack it. It only touches
schemas genuinely missing it — CREATE/DROP TRIGGER takes an ACCESS EXCLUSIVE
lock and this loop runs over every partition on the server — and it reports how
many were missing rather than just reporting success.
mesh_node_copy_to_history needs no data repair: measured 33/33 already present
(V44 covered every existing schema). Its source-side guard in GetMeshSchemaScript
is fixed alongside so a NEW schema cannot regress.
🚨 That third site was found BY THE TEST, not by eye. The test rejects the SHAPE —
any trigger created under a guard that checks only a name, without constraining
the table — which is why it caught an instance a targeted fix would have missed.
It carries a negative control (the real guard must still be flagged) and its
complement (a properly table-scoped probe must still be allowed), so it cannot
quietly become vacuous.
Verified: Release + -warnaserror clean (Hosting.PostgreSql, Database.Migration,
Documentation); 9/9 tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a PostgreSQL schema-provisioning defect where the mesh_node_notify trigger was installed only once per database (due to a globally-scoped pg_trigger name probe), leaving most partition schemas without pg_notify emissions for node writes. It updates the schema DDL to install the trigger per-table and adds a v54 migration + tests to prevent regressions.
Changes:
- Update partition/unversioned schema scripts to
DROP TRIGGER IF EXISTS … ON mesh_nodes; CREATE TRIGGER …(per-table, non-global) formesh_node_notify(and alignmesh_node_copy_to_historysimilarly where applicable). - Add migration V54 to install
mesh_node_notifyon existing schemas that are missing it, and register it asDbVersion.Latest = 54. - Add tests that scan generated schema SQL to forbid globally-scoped
pg_trigger.tgnameguards and assert thatmesh_node_notifyis created via the per-table form.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.Hosting.PostgreSql.Test/MeshNodeNotifyTriggerScriptTests.cs | Adds regression tests to forbid database-global trigger-name guards and assert per-table trigger creation. |
| src/MeshWeaver.Hosting.PostgreSql/PostgreSqlSchemaInitializer.cs | Switches trigger installation to per-table DROP/CREATE form; exposes unversioned script for test coverage. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-18-partition-writes-notify-again.md | Adds a “What’s New” entry documenting the defect and fix. |
| memex/aspire/Memex.Database.Migration/Migrations/V54_FixMeshNodeNotifyTriggerPerSchema.cs | New migration that installs mesh_node_notify for schemas that lack it. |
| memex/aspire/Memex.Database.Migration/MigrationRegistry.cs | Registers the new V54 migration. |
| memex/aspire/Memex.Database.Migration/DbVersion.cs | Bumps latest DB version from 53 to 54. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+112
to
+116
| await using (var cmd = ctx.DataSource.CreateCommand($""" | ||
| CREATE TRIGGER mesh_node_notify | ||
| AFTER INSERT OR UPDATE OR DELETE ON {quotedSchema}.mesh_nodes | ||
| FOR EACH ROW EXECUTE FUNCTION notify_mesh_node_changes(); | ||
| """)) |
Comment on lines
2407
to
2409
| /// <summary> | ||
| /// Returns SQL for unversioned partitions: core tables only, no history table or triggers. | ||
| /// </summary> |
This was referenced Aug 17, 2026
This was referenced Aug 30, 2026
Merged
rbuergi
added a commit
that referenced
this pull request
Aug 30, 2026
…tener is never started" claim, and drop the hard-coded count Copilot review: RegistryUpdateReconciler, Build Coordination and Plugin Update on Green Build still carried the claim. All corrected to the actual loss semantics — the feed is live (#1816) but a NOTIFY reaches only a live session on the SAME database and is never replayed — and the design page now names the sites instead of counting them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPCWramjFYquX5SaMdeJKW
rbuergi
added a commit
that referenced
this pull request
Aug 30, 2026
…leans memory stream (#2724) * docs: Durable Streams Are Mesh Nodes — the design that retires the Orleans memory stream (#1742, #2320, #2322, #2406) The memory stream carries three kinds of traffic, and "use mesh nodes for durable streams" is a different answer for each: data-sync frames already recover from the node's own version chain (nothing to build); a request to a hub that is not live wants a transient NACK in milliseconds, not a queue (the roll plan's N+2, made precise — indefinite pod-hub claim with a DERIVED lifetime, fallback gated on DECLARED client-hosted address types, which the fleet has none of); and the cross-silo change broadcast — the only stream user whose loss is permanent — moves onto the database's own LISTEN/NOTIFY feed, which every pod already runs. At-least-once work keeps the _Inbox node pattern, whose consumer must live on an always-on hub (Plugins#777). Also corrects four code comments claiming PostgreSqlChangeListener "is registered and never started in the partitioned wiring": that was #1440, fixed by #1816 and pinned by ChangeListenerWiringTests. The conclusions those comments draw still hold — a NOTIFY reaches only a live session and is never replayed — and they now say so. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPCWramjFYquX5SaMdeJKW * docs: correct every core occurrence of the stale "PostgreSqlChangeListener is never started" claim, and drop the hard-coded count Copilot review: RegistryUpdateReconciler, Build Coordination and Plugin Update on Green Build still carried the claim. All corrected to the actual loss semantics — the feed is live (#1816) but a NOTIFY reaches only a live session on the SAME database and is never replayed — and the design page now names the sites instead of counting them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPCWramjFYquX5SaMdeJKW --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Measured first
On a live 33-partition database, before any change:
mesh_node_notifyonmesh_nodes*_notifyon satellite tablesmesh_node_copy_to_historyThe one schema carrying
mesh_node_notifyispublic— which AGENTS.md documents as empty by design. So in practice no node write in that database had ever emittedpg_notify.Root cause
Three schema scripts created the trigger like this:
pg_triggeris a database-global catalog; trigger names are unique per table. The first schema provisioned therefore satisfied the predicate for the entire database, and every schema created afterwards silently skipped installing its own. Nothing failed — each schema concluded the work was already done, because somewhere else it was.This is the identical defect V44 repaired for the sibling trigger
mesh_node_copy_to_history— in one of the same methods (GetVersionedPartitionDdl). That repair fixed one trigger and left its neighbour.Why it stayed hidden
Two independent reasons, which is why it lasted:
GetSatelliteTableScriptalways used the per-table form, so a partition's_Access/_Thread/_Activitywrites notified while that same partition'smesh_nodeswrites did not. The asymmetry makes the symptom look selective rather than systemic.IStorageAdapter.Changesfeed still carries same-process writes, so only a notification that had to cross a process boundary was lost — and it was lost silently, with the write reporting success.The fix, both halves
DROP TRIGGER IF EXISTS … ON <table>; CREATE TRIGGER …form that the other ~20 triggers in the file already use. New schemas are correct by construction.CREATE/DROP TRIGGERtakes anACCESS EXCLUSIVElock and this loop runs over every partition on the server, so re-applying an identical definition would buy nothing and cost a lock per partition. It reports how many were missing, not merely that it finished.mesh_node_copy_to_historygets no data repair — measured 33/33 already present, because V44 covered every existing schema. Its source-side guard inGetMeshSchemaScriptis fixed alongside so a new schema cannot regress.🚨 The third site was found by the test, not by eye
The test rejects the shape — any trigger created under a guard that checks only a name without constraining the table — rather than this one trigger. On its first run it immediately flagged a live instance I had not fixed:
mesh_node_copy_to_historyinGetMeshSchemaScript, still on the global guard. A targeted fix would have shipped that regression forward into every newly created schema.It also carries a negative control (the real guard must still be flagged) and its complement (a properly table-scoped probe must still be allowed), so it forbids the defect rather than the catalog and cannot quietly become vacuous.
WithoutCommentsexists because the scripts document this anti-pattern in their own comments — the first run flagged that prose, which is itself evidence the scanner reads what it claims to.Verification
dotnet build -c Release -warnaserrorclean:MeshWeaver.Hosting.PostgreSql,Memex.Database.Migration,MeshWeaver.Documentation.Passed! - Failed: 0, Passed: 9.DbVersion.Latest53 → 54, registered inMigrationRegistry.🤖 Generated with Claude Code