Skip to content

zodan failover-slot support on PG17+ and Phase 9 sub_create stabilization#461

Merged
mason-sharp merged 4 commits into
mainfrom
SPOC-540
May 8, 2026
Merged

zodan failover-slot support on PG17+ and Phase 9 sub_create stabilization#461
mason-sharp merged 4 commits into
mainfrom
SPOC-540

Conversation

@ibrarahmad
Copy link
Copy Markdown
Contributor

Phase 9 created enabled new→existing subscriptions and returned without confirming any peer had begun applying. Mirror Phases 6/8.

Phase 9 created enabled new→existing subscriptions and returned
without confirming any peer had begun applying. Mirror Phases 6/8.
@ibrarahmad ibrarahmad requested a review from mason-sharp May 7, 2026 14:17
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Probes remote server_version_num to choose the correct pg_create_logical_replication_slot signature, adds a clarifying comment in the ZODAN subscription loop, preserves/restores the caller memory context when copying ErrorData in subscriber init error handling, and adds a regression check that a standby in recovery remains queryable with Spock loaded.

Changes

ZODAN & subscriber init error handling

Layer / File(s) Summary
Replication slot creation (server version probe)
samples/Z0DAN/zodan.sql
spock.create_replication_slot probes server_version_num and conditionally calls pg_create_logical_replication_slot with either the legacy 2-arg form or the PG17+ boolean-augmented signature.
Subscriber init error-context preservation
src/spock_sync.c
In the PG_CATCH handler during subscriber initialization, saves the current memory context, switches to myctx to call CopyErrorData(), captures ErrorData *, then restores the original memory context before rethrowing.
ZODAN per-subscription comment
samples/Z0DAN/zodan.sql
Adds a comment in spock.create_sub_on_new_node_to_src_node clarifying that an existing sleep allows the subscriber apply worker to create its slot on the provider; no behavioral changes.
Failover slots test: standby regression
tests/tap/t/018_failover_slots.pl
Inserts regression block 14b to verify a standby in recovery remains queryable with Spock loaded: waits for WAL replay, checks pg_is_in_recovery(), validates user and catalog SELECTs, verifies slot visibility, and confirms writes are rejected.

Poem

🐰 I sniffed the server's version line,
Chose the slot-call that would fit just fine,
I left a note where sleeps reside,
And caught errors safe, context switched aside,
A rabbit's hop—small, tidy, and divine.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly references adding a sync_event handshake to Phase 9 sub_create loop, which aligns with the core objective described in the PR description about adding synchronization confirmation.
Description check ✅ Passed The PR description clearly explains the change: adding sync_event handshake to Phase 9 to mirror Phases 6/8 behavior and confirm peer began applying, which relates to the changeset modifications.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SPOC-540

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented May 7, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 duplication

Metric Results
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
samples/Z0DAN/zodan.sql (1)

1780-1811: 💤 Low value

Sync handshake correctly mirrors Phase 6/8 pattern.

The added sync_event emit on the new node followed by wait_for_sync_event on each subscriber rec.dsn is the right shape: provider-side emit, subscriber-side wait. Together with enabled := true on create_sub above, this guarantees Phase 9 cannot return until each existing node's apply worker has actually progressed past a known LSN from the new node. The defensive sync_ok IS NULL OR sync_ok::boolean IS NOT TRUE check and the EXCEPTION WHEN OTHERS wrappers are consistent with the rest of the file.

One small consideration (non-blocking): since every iteration uses the same provider (new node) for its respective subscription, you could emit spock.sync_event() on the new node a single time after the create_sub loop and then run only the per-subscriber wait_for_sync_event calls in the loop. That trims O(N) WAL sync events on the new node down to O(1) and still gives each subscriber a reachable target. The current per-iteration form has the upside of failing faster on a specific subscriber, so this is a tradeoff rather than a defect.

Note on timeout naming: The variable timeout_ms is misleading—wait_for_sync_event() compares the timeout parameter directly to EXTRACT(EPOCH FROM ...), which returns elapsed time in seconds. The value 180 paired with -- 3 minutes in comments across the file (lines 1749, 1591, 1677, 1903, 2227) is correctly interpreted as 180 seconds, not 180 milliseconds. The handshake will not timeout prematurely. However, the naming convention should be clarified: consider renaming timeout_ms to timeout_s or timeout to avoid future confusion, as this pre-existing pattern is used elsewhere in the file.

♻️ Optional refactor: emit sync_event once outside the loop
+    -- Emit one sync_event on the new node after all subscriptions are created;
+    -- each subscriber will wait for the same target LSN.
+    BEGIN
+        remotesql := 'SELECT spock.sync_event();';
+        SELECT * FROM dblink(new_node_dsn, remotesql) AS t(lsn pg_lsn) INTO sync_lsn;
+        RAISE NOTICE '    OK: %', rpad('Triggered sync_event on new node ' || new_node_name || ' (LSN: ' || sync_lsn || ')...', 120, ' ');
+    EXCEPTION
+        WHEN OTHERS THEN
+            RAISE EXCEPTION '    ✗ %', rpad('Triggering sync_event on new node ' || new_node_name || ' (error: ' || SQLERRM || ')', 120, ' ');
+    END;
+
+    FOR rec IN SELECT * FROM temp_spock_nodes WHERE node_name != new_node_name LOOP
+        BEGIN
+            remotesql := format('CALL spock.wait_for_sync_event(true, %L, %L::pg_lsn, %s, true);',
+                                new_node_name, sync_lsn, timeout_ms);
+            SELECT * INTO sync_ok FROM dblink(rec.dsn, remotesql) AS t(result text);
+            IF sync_ok IS NULL OR sync_ok::boolean IS NOT TRUE THEN
+                RAISE EXCEPTION 'wait_for_sync_event timed out for % on node %', new_node_name, rec.node_name;
+            END IF;
+        EXCEPTION
+            WHEN OTHERS THEN
+                RAISE EXCEPTION '    ✗ %', rpad('Unable to wait for sync event from ' || new_node_name || ' on node ' || rec.node_name || ' (error: ' || SQLERRM || ')', 120, ' ');
+        END;
+    END LOOP;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/Z0DAN/zodan.sql` around lines 1780 - 1811, The timeout variable name
is misleading: wait_for_sync_event() expects seconds but the code uses the
identifier timeout_ms; rename the identifier (e.g. timeout_ms -> timeout_s or
timeout) everywhere it is declared and referenced (including where format()
builds the CALL for spock.wait_for_sync_event and any comments near
create_sub/Phase 9) so the name matches units, update the in-line comments that
currently say "-- 3 minutes" to reference seconds if needed, and run a
project-wide search/replace for timeout_ms to keep all callers/uses (e.g., the
format(...) call that builds the CALL to spock.wait_for_sync_event and any other
functions referencing timeout_ms) consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@samples/Z0DAN/zodan.sql`:
- Around line 1780-1811: The timeout variable name is misleading:
wait_for_sync_event() expects seconds but the code uses the identifier
timeout_ms; rename the identifier (e.g. timeout_ms -> timeout_s or timeout)
everywhere it is declared and referenced (including where format() builds the
CALL for spock.wait_for_sync_event and any comments near create_sub/Phase 9) so
the name matches units, update the in-line comments that currently say "-- 3
minutes" to reference seconds if needed, and run a project-wide search/replace
for timeout_ms to keep all callers/uses (e.g., the format(...) call that builds
the CALL to spock.wait_for_sync_event and any other functions referencing
timeout_ms) consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de5ac347-062a-4d4d-8a52-92b4213ec480

📥 Commits

Reviewing files that changed from the base of the PR and between f294f83 and ab054ed.

📒 Files selected for processing (1)
  • samples/Z0DAN/zodan.sql

Ibrar Ahmed added 2 commits May 7, 2026 20:45
create_replication_slot now detects the remote PG version and
passes failover := true on PG17+, matching spock_sync.c.
Also revert Phase 9 sync_event handshake; deadlocks against cswp.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
samples/Z0DAN/zodan.sql (1)

1416-1431: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Phase 3 slot creation bypasses the version check — PG17+ "other" nodes get slots without failover=true.

create_disable_subscriptions_and_slots (Phase 3) builds the pg_create_logical_replication_slot call directly at Line 1420 using the unconditional 2-argument form, bypassing the version-aware spock.create_replication_slot procedure. On PG17+ "other" nodes this creates slots without failover=true, inconsistent with what create_replication_slot now does in Phase 4. All slots in a ZODAN-built cluster should have the same failover posture.

🐛 Proposed fix — add a version check consistent with `create_replication_slot`
-            remotesql := format('SELECT slot_name, lsn FROM pg_create_logical_replication_slot(%L, ''spock_output'');', slot_name);
+            DECLARE
+                _remote_ver int;
+            BEGIN
+                SELECT v INTO _remote_ver
+                FROM dblink(rec.dsn, 'SHOW server_version_num') AS t(v int);
+            END;
+            IF _remote_ver >= 170000 THEN
+                remotesql := format(
+                    'SELECT slot_name, lsn FROM pg_create_logical_replication_slot(%L, ''spock_output'', false, false, true)',
+                    slot_name);
+            ELSE
+                remotesql := format(
+                    'SELECT slot_name, lsn FROM pg_create_logical_replication_slot(%L, ''spock_output'')',
+                    slot_name);
+            END IF;

Alternatively, refactor Phase 3 to call spock.create_replication_slot directly (the same helper already used in Phase 4 via create_replication_slots) to avoid this duplication entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@samples/Z0DAN/zodan.sql` around lines 1416 - 1431, Phase 3's
create_disable_subscriptions_and_slots currently builds a raw
pg_create_logical_replication_slot call (using spock.spock_gen_slot_name and
remotesql) which bypasses the PG-version-aware behavior and omits failover=true
on PG17+ nodes; change it to honor the same version check used by
spock.create_replication_slot (or simply call
spock.create_replication_slot/create_replication_slots instead of building the
two-argument pg_create_logical_replication_slot SQL) so slots created in Phase 3
include failover=true when appropriate; update the code in
create_disable_subscriptions_and_slots to either branch on the server version
(and use the correct 3-argument form with failover=true for PG17+) or delegate
to the existing spock.create_replication_slot helper to ensure consistent
failover posture across phases.
🧹 Nitpick comments (1)
src/spock_sync.c (1)

1498-1516: ⚡ Quick win

FlushErrorState() + PG_RE_THROW() should be replaced with ReThrowError(edata).

After FlushErrorState() (Line 1498) the error stack is empty (errordata_stack_depth == -1). The subsequent PG_RE_THROW() (Line 1516) longjmps to the outer handler, which will find no valid error entry and fall back to a generic ERRCODE_INTERNAL_ERROR with an empty message — the original error details are silently lost even though they were captured in edata. The correct PostgreSQL idiom after CopyErrorData() + FlushErrorState() is ReThrowError(edata), which pushes edata back onto the error stack before re-throwing. This is a pre-existing issue, but since this PR now holds edata in scope, it is a low-effort fix.

♻️ Proposed fix
         FreeErrorData(edata);
         MemoryContextSwitchTo(savecxt);
-        PG_RE_THROW();
+        ReThrowError(edata);   /* push edata back onto stack, then longjmp */

Note: ReThrowError calls FlushErrorState internally, so you can also drop the explicit FlushErrorState() call on line 1498 and keep both edata and error state in sync.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/spock_sync.c` around lines 1498 - 1516, The error handler currently calls
FlushErrorState() then PG_RE_THROW(), which clears the error stack and causes
the rethrow to lose the original edata; replace this sequence by calling
ReThrowError(edata) (or remove the explicit FlushErrorState() and call
ReThrowError(edata)) so that the saved edata is pushed back onto the error stack
before re-throwing; update the block around edata handling in spock_sync.c to
call ReThrowError(edata) instead of FlushErrorState() + PG_RE_THROW(), keeping
FreeErrorData(edata) / MemoryContextSwitchTo(savecxt) semantics as appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@samples/Z0DAN/zodan.sql`:
- Around line 1416-1431: Phase 3's create_disable_subscriptions_and_slots
currently builds a raw pg_create_logical_replication_slot call (using
spock.spock_gen_slot_name and remotesql) which bypasses the PG-version-aware
behavior and omits failover=true on PG17+ nodes; change it to honor the same
version check used by spock.create_replication_slot (or simply call
spock.create_replication_slot/create_replication_slots instead of building the
two-argument pg_create_logical_replication_slot SQL) so slots created in Phase 3
include failover=true when appropriate; update the code in
create_disable_subscriptions_and_slots to either branch on the server version
(and use the correct 3-argument form with failover=true for PG17+) or delegate
to the existing spock.create_replication_slot helper to ensure consistent
failover posture across phases.

---

Nitpick comments:
In `@src/spock_sync.c`:
- Around line 1498-1516: The error handler currently calls FlushErrorState()
then PG_RE_THROW(), which clears the error stack and causes the rethrow to lose
the original edata; replace this sequence by calling ReThrowError(edata) (or
remove the explicit FlushErrorState() and call ReThrowError(edata)) so that the
saved edata is pushed back onto the error stack before re-throwing; update the
block around edata handling in spock_sync.c to call ReThrowError(edata) instead
of FlushErrorState() + PG_RE_THROW(), keeping FreeErrorData(edata) /
MemoryContextSwitchTo(savecxt) semantics as appropriate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bbeabbbc-7a02-4ac4-a66c-4c285d7ac940

📥 Commits

Reviewing files that changed from the base of the PR and between f771932 and 5648cad.

📒 Files selected for processing (2)
  • samples/Z0DAN/zodan.sql
  • src/spock_sync.c

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/tap/t/018_failover_slots.pl`:
- Around line 449-455: The test currently only checks $write_rc from system(...)
and discards stderr, so change the invocation to capture command output (both
stdout and stderr) instead of using system; for example run the psql command
with backticks or qx(...) and assign output to a variable like $write_out and
set $write_rc from $?. Then keep the existing non-zero assertion on $write_rc
and add an assertion that $write_out matches a read-only error (e.g. /read[-
]?only|cannot execute INSERT in a read-only transaction/i) so the test verifies
the failure reason for the "Write against read-only standby is rejected
(read-only enforced)" case.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a70451cd-cee1-4bf9-80a6-5e55a4238b16

📥 Commits

Reviewing files that changed from the base of the PR and between 5648cad and ec3b18a.

📒 Files selected for processing (1)
  • tests/tap/t/018_failover_slots.pl

Comment thread tests/tap/t/018_failover_slots.pl
@mason-sharp mason-sharp merged commit 8ba18fe into main May 8, 2026
17 of 18 checks passed
@mason-sharp mason-sharp deleted the SPOC-540 branch May 8, 2026 16:55
@mason-sharp mason-sharp changed the title zodan: add sync_event handshake to Phase 9 sub_create loop. zodan failover-slot support on PG17+ and Phase 9 sub_create stabilization May 10, 2026
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