Skip to content

Cherry-pick PG19-adaptation commits and fixes from v5_STABLE - #557

Open
danolivo wants to merge 7 commits into
mainfrom
spoc-643
Open

Cherry-pick PG19-adaptation commits and fixes from v5_STABLE#557
danolivo wants to merge 7 commits into
mainfrom
spoc-643

Conversation

@danolivo

@danolivo danolivo commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes

  • Exception replay mode leaked onto healthy transactions. use_try_block lives in the worker's shmem slot, not per-transaction state. An error raised between remote transactions never reaches the commit that clears it, and apply_work() enters replay mode anyway — so the flag landed on the next transaction from the stream. handle_begin() only reasoned about the flag when commit_lsn matched the recorded failure, so a non-matching transaction inherited it, was replayed read-only, and under TRANSDISCARD/SUB_DISABLE was discarded. A transaction that never failed, lost, and blamed on an unrelated error. Now the flag is derived from the comparison, and the recorded root cause is dropped with it.

  • Off-by-one scan of the exception log array. Both search loops ran to i <= SpockCtx->total_workers, one past the end. [total_workers] is outside the region startup zeroes, and an empty-looking name there would be taken as the free slot, so namestrcpy() wrote past the array. It landed in slack only because worker_shmem_size() over-allocates.

  • Slot name matched by prefix instead of exactly. strncmp() bounded by strlen(MySubscription->name) made test_subscription share a slot with test_subscription_parallel — and sharing the slot means sharing commit_lsn, so one subscription's failure could push the other into exception handling for a transaction that never failed. Now namestrcmp().

  • Failover-slots worker died while the walreceiver reconnected. WalRcv->conninfo is empty for as long as a connection attempt is in flight, which is every walreceiver start and restart, not just standby startup — the worker died in that window for 60s at a time. make_sync_failover_slots_dsn() now reports whether a DSN is available instead of building an unusable one; both callers wait for the next cycle. The field is also copied under WalRcv->mutex, matching core's pg_stat_get_wal_receiver(); it was read unlocked while the walreceiver rewrote it under that spinlock.

  • Log-level re-check was inverted. spock_error_callback() downgrades two serialization-failure retry messages to DEBUG1, then re-tests whether they should still be emitted — backwards in both comparisons. DEBUG1 is 14 and the default log_min_messages is WARNING = 19, so 19 < 14 never fired and the messages went to the server log regardless of the threshold, which is what this code exists to prevent. Now compares in the direction is_log_level_output() uses, as spock_dependency.c already does.

  • Four PG19 compiler warnings. log_min_messages became an array indexed by backend type (pointer-vs-int comparison, the bug above); pg_fallthrough is new in PG19's c.h and clang won't accept a comment as the annotation; hash_get_num_entries() now returns int64, no longer matching %lu; generate_subscription_id() had an empty parameter list. New compat macros for the 15–18 headers.

Refactor

  • clear_exception_log_entry() collects the four assignments that reset a shared exception log entry. Two call sites need the same set, and local_tuple is easy to overlook — it's the only non-scalar, and it dangles by the time either caller runs. No functional change.

@danolivo danolivo self-assigned this Aug 4, 2026
@danolivo danolivo added the bug Something isn't working label Aug 4, 2026
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The changes add PostgreSQL compatibility macros, adjust log filtering and portability details, improve exception replay cleanup, and defer failover-slot operations when no usable connection string exists.

Changes

Compatibility, replay, and failover

Layer / File(s) Summary
Compatibility and portability
src/compat/*/spock_compat.h, src/spock.c, src/spock_group.c, src/spock_node.c
Compatibility headers define portable fallthrough and log-level access. Logging, integer formatting, and the static function declaration use compatible forms.
Exception replay state
src/spock_apply.c
Exception-log scans use valid bounds and exact subscription-name matching. Stale entries are cleared on LSN mismatch, and replay state is reset for the next transaction.
Failover connection handling
src/spock_failover_slots.c
The DSN builder reports connection availability and copies WAL receiver data under its mutex. Catch-up and synchronization defer work when the DSN is empty.

Poem

A rabbit hops through logs so bright,
Clears stale tracks from yesterday’s night.
It checks each name, each worker’s trail,
Then waits when failover strings fail.
“Retry next round,” the rabbit sings,
While safe macros guard the springs.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.
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.
Title check ✅ Passed The title accurately identifies the PG19 adaptations and fixes included in the changeset.
Description check ✅ Passed The description clearly explains the replay, exception-log, failover-slot, logging, compatibility, and warning fixes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch spoc-643

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.

@codacy-production

codacy-production Bot commented Aug 4, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 5 complexity · 0 duplication

Metric Results
Complexity 5
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.

The two loops searching for this subscription's exception-log slot ran to
i <= SpockCtx->total_workers, while the array holds exactly total_workers
entries (see worker_shmem_size() and the memset in spock_worker_shmem_init).
Every other loop over the array in the tree stops at < total_workers.

Reading entry [total_workers] is outside the region that startup zeroes, so
the slot_name compared there is whatever the adjacent shared memory happens to
hold.  Worse, an empty-looking name would be picked up as free_slot_index and
namestrcpy() would then write past the end of the array.  Nothing has been
observed to break, because the region is allocated with worker_shmem_size()
which over-allocates enough that the write lands in slack, but that is luck
rather than design.

(cherry picked from commit ad49bf9)
handle_begin() looked up this subscription's slot in the shared exception log
with strncmp() bounded by strlen(MySubscription->name), which matches on any
slot whose name merely starts with ours.  Two subscriptions in a
prefix relationship therefore share one slot: "test_subscription" matches the
slot belonging to "test_subscription_parallel", and the shorter name wins
because the comparison stops before the longer name's extra characters.

Sharing the slot means sharing commit_lsn, and that is the marker handle_begin()
uses to decide whether a transaction has already failed once.  Subscriptions
from the same provider are delivered the same remote transactions with the same
commit LSNs, so a failure recorded by one subscription can make the other enter
exception handling for a transaction that never failed -- which under
transdiscard means the transaction is discarded.  The initial_error_message and
failed_action of the two subscriptions are mixed up as well.

Compare with namestrcmp(), which is what the rest of the file uses for a
NameData against a C string, and which is bounded by NAMEDATALEN.  The
comparison is now exact.

In passing, drop the slot_name variable.  Its only remaining use was the
strlen() emptiness test for the free-slot search, which reads more directly as
NameStr(...)[0] == '\0' and no longer needs a separate declaration.

(cherry picked from commit 59ab49e)
…ninfo

Without spock.failover_slots_dsn the primary's connection string is taken from
WalRcv->conninfo, and that field is empty for as long as a connection attempt is
in flight: WalReceiverMain() clears it under the walreceiver mutex before calling
walrcv_connect() and fills it in only once the connection is fully established.
The window therefore opens on every walreceiver start and restart, not merely
while the standby comes up.

Have make_sync_failover_slots_dsn() report whether a connection string is
available rather than build an unusable one, and let both callers wait for the
next cycle instead.  Since the walreceiver rewrites the field with its spinlock
held, copy it out under WalRcv->mutex too: an unlocked reader could otherwise
catch the memset() or strlcpy() in progress and build a DSN from a truncated
string.

(cherry picked from commit d9e911c)
clear_transient_exception_state_on_connection_loss() open-codes the four
assignments that put a shared exception log entry back into the "no failure
recorded" state.  A second caller is about to need exactly the same set, and a
set of fields spelled out in two places is a set of fields that will drift --
in particular local_tuple, which is easy to overlook because it is the only one
that is not a scalar marker.

No functional change.

A little refactoring in preparation for the next commit, to make the material
change there more clear.
use_try_block lives in the apply worker's shared memory slot and is not
per-transaction state, so once set it stays set until something clears it.  It
is cleared when a transaction commits, but an error raised between remote
transactions never reaches that point: apply_work() switches to replay mode
regardless, and the flag is left over for whichever transaction the provider
sends next.

handle_begin() only reasoned about the flag when the incoming commit_lsn
matched the recorded failure.  A non-matching transaction is a first attempt,
but it inherited the stale flag and was applied as a read-only replay -- and
under TRANSDISCARD or SUB_DISABLE it was then discarded, reported through the
exception log as if the policy had fired on a genuine conflict.  The
transaction was never attempted and never conflicted, so this loses data and
attributes the loss to an unrelated earlier error.

Derive the flag from the comparison instead: a transaction that is not the
recorded failure clears it, and drops the recorded root cause with it.  Leaving
initial_error_message behind would make the next genuine exception surface a
stale "Initial error" belonging to a different transaction.

Note the fix depends on apply_work() setting first_begin_at_startup back to
true when it catches an exception, which is what brings the next BEGIN through
this comparison at all; comment that coupling at both ends.

In passing, fix two comments that no longer describe the code: case 3 of
handle_begin()'s case analysis, and the claim in handle_commit() that it is the
only place use_try_block may be cleared -- untrue even before this commit, as
the path which allocates a fresh exception log slot clears it too.
Annotate the deliberate fall-through in handle_queued_message() with
pg_fallthrough.  clang does not accept a comment as the annotation the way gcc
does, so it warned here.  pg_fallthrough is new in PG19; add it to the older
compat headers, picking the spelling from the compiler rather than the server
version since it is a compiler feature.

hash_get_num_entries() returns int64 as of PG19, which no longer matches the
%lu in spock_group_shmem_startup()'s debug message.  Cast and use INT64_FORMAT,
which is right on every supported major and on platforms where long is 32 bits.

Give generate_subscription_id() a (void) parameter list.  An empty list is a
declaration without a prototype, deprecated in all C versions and rejected
outright in C23.
spock_error_callback() lowers two serialization-failure retry messages from LOG
to DEBUG1 and then reconsiders whether they should still be emitted, because
errstart() decided that for the original level.  Both comparisons were the wrong
way round: they suppressed the message exactly when it should have been shown
and showed it when it should have been suppressed.  With the default settings
the branch was simply never taken -- log_min_messages of WARNING is not less
than DEBUG1 -- so the messages went to the server log at DEBUG1 regardless of
the threshold, which is the opposite of what this code exists to do.

PG19 made the mistake visible: log_min_messages is now an array indexed by
backend type, so comparing it against an elevel compares a pointer with an
integer and clang warns.  Add SPOCK_LOG_MIN_MESSAGES to the compat headers to
hide that difference, and compare in the direction is_log_level_output() uses --
emit when elevel is at least the configured minimum.

spock_dependency.c already tests its thresholds this way, which is what the
comparison here should have looked like all along.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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)
src/spock_apply.c (1)

3934-3937: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not enter replay mode when the replay queue is empty.

After apply_replay_queue_start_replay() returns from a non-transaction queue, need_replay still triggers MyApplyWorker->use_try_block = true. Replay loop then exits immediately and the next incoming transaction applies with replay mode on until handle_commit(), causing read-only/discard behavior, incorrect feedback handling, and SUB_DISABLE disablement on unrelated errors. Enable replay state only when apply_replay_head or spilled records exist.

🤖 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_apply.c` around lines 3934 - 3937, Update the need_replay branch in
the stream replay flow so it enables MyApplyWorker->use_try_block and enters
replay mode only when apply_replay_head or spilled replay records exist. When
the replay queue is empty after apply_replay_queue_start_replay(), skip the
replay transition and allow the next transaction to apply normally.
🤖 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 `@src/spock_apply.c`:
- Around line 3934-3937: Update the need_replay branch in the stream replay flow
so it enables MyApplyWorker->use_try_block and enters replay mode only when
apply_replay_head or spilled replay records exist. When the replay queue is
empty after apply_replay_queue_start_replay(), skip the replay transition and
allow the next transaction to apply normally.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8994201e-a7ad-44ff-9576-def7524a4801

📥 Commits

Reviewing files that changed from the base of the PR and between b95c7c1 and 7c11f40.

📒 Files selected for processing (10)
  • src/compat/15/spock_compat.h
  • src/compat/16/spock_compat.h
  • src/compat/17/spock_compat.h
  • src/compat/18/spock_compat.h
  • src/compat/19/spock_compat.h
  • src/spock.c
  • src/spock_apply.c
  • src/spock_failover_slots.c
  • src/spock_group.c
  • src/spock_node.c
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/spock_failover_slots.c

@danolivo danolivo changed the title Don't leak exception replay mode onto a healthy transaction Cherry-pick PG19-adaptation commits and fixes from v5_STABLE Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant