Skip to content

[FLINK-40560][postgres] Drop the replication slot when a snapshot-only source finishes - #4523

Open
tchivs wants to merge 1 commit into
apache:masterfrom
tchivs:FLINK-40560
Open

[FLINK-40560][postgres] Drop the replication slot when a snapshot-only source finishes#4523
tchivs wants to merge 1 commit into
apache:masterfrom
tchivs:FLINK-40560

Conversation

@tchivs

@tchivs tchivs commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What is the purpose of this pull request?

Fix FLINK-40560: a PostgreSQL snapshot-only source can finish successfully while leaving its replication slot behind, retaining WAL after the job is done.

The stream split starts at the minimum snapshot high watermark and stops at the maximum. Its starting offset is subsequently advanced by emitted data-change records and heartbeats, not by every record consumed from WAL. Therefore, comparing the starting and ending offsets is not a reliable test of whether a bounded split has finished.

Brief change log

  • Release the slot when a snapshot-only stream split finishes normally, using the reader context to distinguish completion from suspension for newly added tables.
  • Retain the slot when the stream split is suspended; do not move cleanup into close(), where it could destroy state needed for recovery.
  • Log a warning when PostgresDialect.removeSlot returns false.
  • Add reader-level tests for lagging starting offsets and suspended splits, plus a MiniCluster/PostgreSQL integration regression with multiple snapshot splits, no captured DML, and heartbeats disabled.

Verifying this change

This change added tests and can be verified as follows:

  • PostgresSourceReaderTest: all 8 tests pass, including both new lifecycle cases.
  • PostgresSourceITCase#testSnapshotOnly*: all 3 tests pass, including the new bounded-job regression. The regression reads all 21 fixture rows through end-of-input and then asserts that the replication slot is absent.
  • Mutation check: changing only the cleanup guard back to the original offset comparison makes the new integration test fail with expected: 0L, but was: 1L for the remaining slot count. Restoring the fix makes it pass.
  • Local verification used JDK 17 and the repository's default Flink 1.20.3 profile. The focused command below completed successfully; the configured unit and integration-test executions each ran the same 11 selected tests with no failures or skips.
mvn -pl flink-cdc-connect/flink-cdc-source-connectors/flink-connector-postgres-cdc \
  com.diffplug.spotless:spotless-maven-plugin:apply
mvn -pl flink-cdc-connect/flink-cdc-source-connectors/flink-connector-postgres-cdc \
  '-Dtest=PostgresSourceReaderTest,PostgresSourceITCase#testSnapshotOnly*' clean verify

Issue ownership: I have posted a claim comment on FLINK-40560. Self-assignment returned HTTP 403, so assignment to tchivs still requires a committer.

Documentation

  • Does this pull request introduce a new feature? no
  • If yes, how is the feature documented? not applicable

Was generative AI tooling used to co-author this PR?
  • Yes

Generated-by: OpenAI ChatGPT via Oh My Pi

…y source finishes

FLINK-38277 made a snapshot-only Postgres source drop its replication slot once the
stream split finishes, but gated it on

    streamSplit.getStartingOffset().isAtOrAfter(streamSplit.getEndingOffset())

which does not answer whether the split finished. HybridSplitAssigner#createStreamSplit
seeds those two offsets with the lowest and highest high watermark of the finished
snapshot splits, so they already differ whenever there is more than one split or any
concurrent write. Afterwards only IncrementalSourceRecordEmitter#updateStreamSplitState
advances the starting offset, and only for data-change records and heartbeats. A split
that reaches its ending offset without emitting such a record — a captured publication
with no traffic emits none — therefore left its slot behind on every run, pinning WAL
until an operator dropped it by hand.

IncrementalSourceReader#onSplitFinished already documents that a stream split finishes
for exactly two reasons: the enumerator suspended it so newly added tables can be
snapshotted, or it reached its ending offset. Only the second means the bounded read is
over, and the reader context already distinguishes them, so gate on
!isStreamSplitReaderSuspended() instead. That also removes the offset dereference and
with it a latent NPE when the stopping offset is null.

PostgresDialect#removeSlot swallows failures and returns false, so a failed cleanup was
only visible as an INFO line reading "false". Log it at WARN with the manual
pg_drop_replication_slot hint instead.

The reader keeps its own reference to IncrementalSourceReaderContext because the base
class holds its copy privately; that avoids widening the shared base class API.

Two tests in PostgresSourceReaderTest cover both finish reasons: a split whose starting
offset is behind its ending offset must release the slot, and a split suspended via
StreamSplitUpdateRequestEvent must keep it, since resuming needs the slot's position.
Reverting the guard turns the first one red.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant