Repository navigation
Conversation
A replica-role session skipped the ORIGIN fence and its write was lost at the swap. The swap now also refuses when the fence is gone or not ALWAYS.
ac0398a to
8841472
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Review 1/2 of adb5a99: 0 blocking, 2 non-blocking. I read the whole PR at Invariants: extends CO-1 (the quiesced proof's quiescence is now confirmed again at the swap, not only installed before the copy); upholds LK-2 (the read runs inside the swap's bounded transaction and takes no lock of its own) and CO-9 (every catalog name in the new query is BlockingNone. Non-blocking1. The swap's read matches the trigger by name, function, and statement kinds, but not by whether it fires for every statement, so a replaced fence the docs say is refused is not. Test that fails on adb5a99func TestRunnerRefusesTheSwapWhenTheFenceWasReplacedByAConditionalOne(t *testing.T) {
f := newRunnerFixture(t)
f.createOrders(t)
opts := schemachange.RunOptions{AfterCheckpoint: func(_ context.Context, cp checkpoint.Checkpoint) error {
if cp.Phase == checkpoint.PhaseCutover {
f.exec(t, `CREATE OR REPLACE TRIGGER pgsprite_write_fence
BEFORE INSERT OR UPDATE OR DELETE OR TRUNCATE ON %s.orders
FOR EACH STATEMENT WHEN (false) EXECUTE FUNCTION pgsprite.pgsprite_write_fence()`)
f.exec(t, `ALTER TABLE %s.orders ENABLE ALWAYS TRIGGER pgsprite_write_fence`)
f.exec(t, `INSERT INTO %s.orders (id, qty) VALUES (999999, 1)`)
}
return nil
}}
_, err := f.run(t, "orders", `ALTER TABLE %s.orders DROP COLUMN note`, opts)
require.Error(t, err)
assert.Equal(t, schemachange.CauseCutoverUnfenced, schemachange.RefusalCauseOf(err))
assert.True(t, f.hasColumn(t, "orders", "note"), "the source was not swapped out")
}
2. The swap check's branches are pinned only for a disabled or dropped fence; the origin-only branch and each of the query's match conditions are not. The PR's two swap tests disable the trigger ( Test that passes on adb5a99 and fails when the check refuses only a disabled fencefunc TestRunnerRefusesTheSwapWhenTheFenceIsOriginOnly(t *testing.T) {
f := newRunnerFixture(t)
f.createOrders(t)
opts := schemachange.RunOptions{AfterCheckpoint: func(_ context.Context, cp checkpoint.Checkpoint) error {
if cp.Phase == checkpoint.PhaseCutover {
f.exec(t, `ALTER TABLE %s.orders ENABLE TRIGGER pgsprite_write_fence`)
}
return nil
}}
_, err := f.run(t, "orders", `ALTER TABLE %s.orders DROP COLUMN note`, opts)
require.Error(t, err)
assert.Equal(t, schemachange.CauseCutoverUnfenced, schemachange.RefusalCauseOf(err))
assert.Equal(t, "O", f.fenceEnabled(t, "orders"))
}Passes on Verified
This review was generated by Claude Code (claude-sonnet-5-5). |
|
🤖 Review 2/2 of adb5a99: OSS adoption and SchemaBot integration lenses, 0 blocking, 3 non-blocking. This is the second of two comments; the correctness pass is the one above. The PR adds one exported refusal cause, BlockingNone. Non-blocking1. Docs name the replica-role session but not the writers that run in it, which is what an operator will actually meet. engine-role.md#L103 now says the fence fires for a session running under 2. The fence's strength is stated in three places with three wordings. The limit reads "a role that owns the table can disable it, write, and restore it" in the 3. Prose formatting. The edits leave several lines far past the wrap width the surrounding text keeps: engine-role.md#L103 (122 characters) and #L118 (119), a short ragged tail in doc.go#L23 ( Verified
This review was generated by Claude Code (claude-sonnet-5-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Stamping with comments: 0 blocking, 5 non-blocking (see the two review comments above).
This stamp was left by Claude Code (claude-sonnet-5-5).
The quiesced
Runner's write fence now fires for every session, and its swap refuses when the fence is no longer in place.Why
The fence was an ordinary (
ORIGIN) trigger, so a session running withsession_replication_role = replicaskipped it: its write succeeded after the verification pass, the run still reacheddone, and the row was gone from the live table. Nothing between that pass and the rename looked at the fence again, so a fence disabled or dropped mid-run went unnoticed too. TheRunnerdoc promised "a writer that reaches the table gets an error, never a silent loss" (CO-1).What
ALWAYSwhen installed. A resumed run installs over it, so a fence an older engine leftORIGINis upgraded rather than refused.ACCESS EXCLUSIVElock, theRunner's swap re-reads the fence by the source's OID and refuses as the newcutover-unfenced(classenvironmental, CO-1) unless the engine's trigger is there, calling the engine's function, enabledALWAYS. Nothing is swapped; the nextRunreinstalls the fence and verifies again.Runneronly: a library caller ofCutoverthat quiesces the table by its own means has no fence to find.Runnerdoc:ALWAYScloses the replica-role hole by itself; the swap check only catches a fence that is disabled, dropped, or replaced at swap time. A table owner who disables the fence, writes, and restores it between the pass and the swap is still not seen.refusal-classes.md,engine-role.md,schemabot-integration.md, and the copy-and-swap design updated (extends CO-1).Before / after
ordersis being changed by the quiescedRunner. After the clean verification pass, a session runsSET session_replication_role = replica; INSERT INTO orders (id, qty) VALUES (999999, 1). Only what the fence does with that insert differs.With the fence disabled (
DISABLE TRIGGER) or dropped before the swap: before, the swap went ahead; after, it refuses ascutover-unfencedand the source stays in place.