Copilot commented on code in PR #4523:
URL: https://github.com/apache/flink-cdc/pull/4523#discussion_r4267525254


##########
flink-cdc-connect/flink-cdc-source-connectors/flink-connector-postgres-cdc/src/main/java/org/apache/flink/cdc/connectors/postgres/source/reader/PostgresSourceReader.java:
##########
@@ -157,14 +165,35 @@ protected void onSplitFinished(Map finishedSplitIds) {
         for (Object splitState : finishedSplitIds.values()) {
             SourceSplitBase sourceSplit = ((SourceSplitState) 
splitState).toSourceSplit();
             if (sourceSplit.isStreamSplit()) {
-                StreamSplit streamSplit = sourceSplit.asStreamSplit();
+                // A stream split finishes for one of two reasons, see
+                // IncrementalSourceReader#onSplitFinished: it was suspended 
by the enumerator so
+                // that newly added tables can be snapshotted, or it reached 
its ending offset.
+                // Only the latter means the bounded snapshot-only read is 
over and the slot is no
+                // longer needed.
+                //
+                // Comparing the split's starting and ending offsets cannot 
answer that question.
+                // For snapshot-only, HybridSplitAssigner#createStreamSplit 
seeds them with the
+                // lowest and highest high watermark of the finished snapshot 
splits, and afterwards
+                // only IncrementalSourceRecordEmitter#updateStreamSplitState 
advances the starting
+                // offset, 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 the slot behind on every run.
                 if (this.sourceConfig.getStartupOptions().isSnapshotOnly()
-                        && streamSplit
-                                .getStartingOffset()
-                                .isAtOrAfter(streamSplit.getEndingOffset())) {
+                        && 
!incrementalSourceReaderContext.isStreamSplitReaderSuspended()) {
                     PostgresDialect dialect = (PostgresDialect) this.dialect;
-                    boolean removed = 
dialect.removeSlot(dialect.getSlotName());
-                    LOG.info("Remove slot '{}' result is {}.", 
dialect.getSlotName(), removed);
+                    String slotName = dialect.getSlotName();
+                    boolean removed = dialect.removeSlot(slotName);
+                    if (removed) {
+                        LOG.info("Removed replication slot '{}'.", slotName);
+                    } else {
+                        // removeSlot swallows the failure, so without this 
the only trace of a
+                        // slot that keeps pinning WAL would be an INFO line 
reading "false".
+                        LOG.warn(
+                                "Failed to remove replication slot '{}'. It 
will keep retaining WAL "
+                                        + "until it is dropped, e.g. with 
pg_drop_replication_slot('{}').",
+                                slotName,
+                                slotName);
+                    }

Review Comment:
   Dropping the replication slot from every reader that observes the stream 
split finishing can produce noisy WARN logs or misleading ‘failure’ messages in 
parallel execution (e.g., only one subtask can drop the shared slot; others 
will see `removed=false`). Consider scoping slot removal to a single 
deterministic place (e.g., only subtask 0 via reader context index, or 
coordinator/enumerator side), or at least treating ‘slot already absent’ as a 
benign outcome to avoid false alarms.



##########
flink-cdc-connect/flink-cdc-source-connectors/flink-connector-postgres-cdc/src/test/java/org/apache/flink/cdc/connectors/postgres/source/reader/PostgresSourceReaderTest.java:
##########
@@ -189,6 +195,95 @@ void testNotifyCheckpointWindowSizeDefault() throws 
Exception {
         assertThat(completedCheckpointIds).containsExactly(101L);
     }
 
+    /**
+     * A snapshot-only stream split that reached its ending offset must 
release the replication slot
+     * even though its starting offset is behind that ending offset.
+     *
+     * <p>That combination is the normal case rather than an edge case: {@code
+     * HybridSplitAssigner#createStreamSplit} seeds the two offsets with the 
lowest and highest high
+     * watermark of the finished snapshot splits, so they differ as soon as 
there is more than one
+     * split or any concurrent write, and only emitted data-change or 
heartbeat records move the
+     * starting offset afterwards.
+     */
+    @Test
+    void testSnapshotOnlyRemovesSlotWhenStartingOffsetIsBehindEndingOffset() 
throws Exception {
+        final PostgresSourceReader reader = createSnapshotOnlyReader();
+        createSlot();
+        assertThat(customDatabase.checkSlot(slotName)).isEqualTo(slotName);
+
+        reader.onSplitFinished(finishedStreamSplit());
+
+        // checkSlot returns a human-readable "does not exist" sentence that 
itself embeds the slot
+        // name, so compare for equality rather than containment.
+        assertThat(customDatabase.checkSlot(slotName)).isNotEqualTo(slotName);
+    }
+
+    /**
+     * A suspended stream split is the other reason {@code onSplitFinished} 
fires — the enumerator
+     * paused streaming so newly added tables can be snapshotted, and the 
split will resume. The
+     * slot has to survive that, otherwise resuming loses its position.
+     */
+    @Test
+    void testSnapshotOnlyKeepsSlotWhenStreamSplitIsMerelySuspended() throws 
Exception {
+        final PostgresSourceReader reader = createSnapshotOnlyReader();
+        createSlot();
+        // StreamSplitUpdateRequestEvent is what the enumerator sends to pause 
streaming; note the
+        // reader context flag is the authority here, not 
StreamSplit#isSuspended.
+        reader.handleSourceEvents(new StreamSplitUpdateRequestEvent());
+
+        reader.onSplitFinished(finishedStreamSplit());
+
+        assertThat(customDatabase.checkSlot(slotName)).isEqualTo(slotName);
+    }
+
+    private Map<String, SourceSplitState> finishedStreamSplit() {
+        // Deliberately startingOffset < endingOffset: that is what the 
assigner produces for a
+        // multi-split snapshot, and what the previous guard treated as "not 
finished".
+        final Offset startingOffset = lsnOffset(1000L);
+        final Offset endingOffset = lsnOffset(2000L);
+        final StreamSplit split =
+                new StreamSplit(
+                        StreamSplit.STREAM_SPLIT_ID,
+                        startingOffset,
+                        endingOffset,
+                        new ArrayList<>(),
+                        new HashMap<>(),
+                        0,
+                        false,
+                        true);
+        final Map<String, SourceSplitState> finished = new HashMap<>();
+        finished.put(split.splitId(), new StreamSplitState(split));
+        return finished;
+    }
+
+    private static Offset lsnOffset(long lsn) {
+        final Map<String, String> offset = new HashMap<>();
+        offset.put("lsn", Long.toString(lsn));
+        return PostgresOffset.of(offset);
+    }
+
+    private void createSlot() throws Exception {
+        try (Connection connection =
+                        getJdbcConnection(POSTGRES_CONTAINER, 
customDatabase.getDatabaseName());
+                Statement statement = connection.createStatement()) {
+            statement.execute(
+                    String.format(
+                            "SELECT pg_create_logical_replication_slot('%s', 
'pgoutput')",
+                            slotName));
+        }

Review Comment:
   This test constructs SQL by string-formatting `slotName` directly into the 
query. Even in tests, this pattern is brittle (breaks on quotes/special chars) 
and can normalize unsafe practices. Prefer using a PreparedStatement (bind 
`slotName`) or at least escaping single quotes in `slotName` before 
interpolation.



##########
flink-cdc-connect/flink-cdc-source-connectors/flink-connector-postgres-cdc/src/test/java/org/apache/flink/cdc/connectors/postgres/source/PostgresSourceITCase.java:
##########
@@ -1363,6 +1410,23 @@ private static long getCountOfTable(JdbcConnection jdbc, 
TableId tableId) throws
                 });
     }
 
+    private long getSlotCount(JdbcConnection jdbc) throws SQLException {
+        final String query =
+                String.format(
+                        "SELECT count(*) FROM pg_replication_slots WHERE 
slot_name = '%s'",
+                        slotName);
+        return jdbc.queryAndMap(
+                query,
+                rs -> {
+                    if (!rs.next()) {
+                        throw new SQLException(
+                                String.format(
+                                        "No result returned after running 
query [%s]", query));
+                    }
+                    return rs.getLong(1);
+                });
+    }

Review Comment:
   Like `createSlot()`, this query embeds `slotName` directly into SQL. Using 
parameter binding (if supported by `JdbcConnection` helpers) or escaping would 
make the test more robust (e.g., if `slotName` contains quotes) and avoids 
propagating unsafe SQL construction patterns.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to