Skip to content

fix(connectors): defer postgres source progress until ack - #3957

Open
rohankumardubey wants to merge 3 commits into
apache:masterfrom
rohankumardubey:fix/postgres-source-ack
Open

fix(connectors): defer postgres source progress until ack#3957
rohankumardubey wants to merge 3 commits into
apache:masterfrom
rohankumardubey:fix/postgres-source-ack

Conversation

@rohankumardubey

Copy link
Copy Markdown
Contributor

Which issue does this PR address?

Closes #3635

Rationale

The PostgreSQL source advanced tracking offsets, deleted or marked rows, and consumed CDC changes before Iggy confirmed delivery. A failed send could therefore permanently skip source records.

What changed?

PostgreSQL polling now stages cursor updates and row operations until the runtime reports a successful batch acknowledgment. NACK discards the staged work, while ACK commits the state and performs the pending delete or mark operations.

CDC now peeks logical-slot changes and advances the slot only after acknowledgment. A deterministic regression test stops Iggy during delivery and verifies that the PostgreSQL rows are redelivered after restart.

Local Execution

  • Passed cargo fmt --all -- --check
  • Passed cargo clippy -p iggy_connector_postgres_source -p integration --all-features --all-targets -- -D warnings
  • Passed all 70 PostgreSQL source unit tests
  • Passed all 7 PostgreSQL polling integration tests
  • Passed both PostgreSQL CDC integration tests
  • Passed the deterministic kill-server regression test
  • Passed git diff --check

@github-actions

Copy link
Copy Markdown

Thanks for the PR. It is labeled S-waiting-on-review and queued for review.

Slash commands (own line, regular comment) move it around the queue:

  • /ready - back to S-waiting-on-review after addressing feedback
  • /author - flip to S-waiting-on-author while you finish changes
  • /request-review @user-or-team - request a reviewer

See CONTRIBUTING.md for details.

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Aug 23, 2026
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.88235% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.69%. Comparing base (a65f177) to head (9ed427d).

Files with missing lines Patch % Lines
core/connectors/sources/postgres_source/src/lib.rs 80.88% 19 Missing and 7 partials ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             master    #3957       +/-   ##
=============================================
- Coverage     83.93%   57.69%   -26.24%     
  Complexity     1358     1358               
=============================================
  Files          1213     1212        -1     
  Lines        168441   139746    -28695     
  Branches     135999   107313    -28686     
=============================================
- Hits         141373    80622    -60751     
- Misses        23389    55625    +32236     
+ Partials       3679     3499      -180     
Components Coverage Δ
Rust Core 50.16% <80.88%> (-34.56%) ⬇️
Java SDK 66.67% <ø> (ø)
C# SDK 76.52% <ø> (ø)
Python SDK 90.13% <ø> (ø)
PHP SDK 84.48% <ø> (ø)
Node SDK 95.81% <ø> (-0.10%) ⬇️
Go SDK 68.37% <ø> (+0.07%) ⬆️
Files with missing lines Coverage Δ
core/connectors/sources/postgres_source/src/lib.rs 73.79% <80.88%> (+0.47%) ⬆️

... and 405 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rohankumardubey

Copy link
Copy Markdown
Contributor Author

/ready

@rohankumardubey

Copy link
Copy Markdown
Contributor Author

/request-review @hubcio

@github-actions
github-actions Bot requested a review from hubcio August 24, 2026 08:40

@hubcio hubcio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

a few things without diff lines to hang them on:

  • .claude/skills/connector-source/SKILL.md still teaches the pre-ack pattern this pr removes (the "matches poll_tables" snippet writes cursors during poll, "always return state in every ProducedMessages" no longer holds for empty polls, and state-serialization failure is now a hard poll error, lib.rs:288-296) and doesn't mention on_batch_result at all - needs an update to the new contract.
  • mark_or_delete_processed_rows maps db errors to Error::InvalidRecord (lib.rs:732, :751) - misleading now that these errors surface through the ack path; Error::Connection like advance_replication_slot uses would fit better.
  • README.md:15 still promises offset tracking "avoid duplicates" - at-least-once redelivery means duplicates are possible; and README.md:57 documents the poll_interval default as 1s while the code fallback is 10s (lib.rs:180).

return Ok(());
};

for operation in pending.operations {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

any error in this loop permanently stops the connector - the sdk treats Err from on_batch_result as fatal, and the staged delete/mark ops are dropped while the state file already advanced past those rows. transient pg errors (deadlock, failover) should retry here; note with_retry can't wrap these calls directly (wrong error type), it has to go around the inner .execute()s.

})
}

async fn on_batch_result(&self, result: SourceBatchResult) -> Result<(), Error> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

after a nack the next poll re-reads the same rows, so while iggy is down every batch nacks - and the sdk stops the connector for good at 5 consecutive nacks. with the shipped 1s poll interval a short outage is enough. before, the counter got reset by empty-poll acks, but only because progress was committed pre-send (the bug being fixed). needs a decision: exempt send-failure nacks from the stop counter, or document the manual-restart behavior loudly.

continue;
}
};
let lsn: String = row.try_get("lsn").map_err(|e| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

a row failing here fails the whole poll forever - right call for at-least-once (can't skip a row and advance the slot past it), but poll errors are invisible: no stats bump, status stays Running, and the stuck slot pins restart_lsn so pg wal grows without bound. worth a readme note about monitoring slot lag.

if !processed_ids.is_empty() {
self.mark_or_delete_processed_rows(pool, table, pk_column, &processed_ids)
.await?;
operations.push(PendingOperation::ProcessRows {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the delete/mark now runs a full send+ack after the select, so a row updated in between gets deleted (or marked processed) and its new version is never delivered when tracking_column is an updated-at column. AND {tracking} <= '{max_offset}' in the where clause would close it; at minimum a readme caveat.

### Delete After Read

Deletes rows from the source table after successful processing:
Deletes rows from the source table only after Iggy acknowledges the batch:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

worth documenting the window: state is persisted before the delete runs, so a crash/shutdown/timeout in between leaves rows delivered but never deleted, and the advanced offset means they are never picked up again.

.connectors_runtime_mut()
.expect("connectors runtime")
// Keep a failed send bounded instead of waiting indefinitely for Iggy to return.
.set_iggy_connection_options("reconnection_retries=0");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

reconnection_retries=0 stays set for the verification phase too (build_envs reruns on every start), so the post-restart polls get no reconnect slack. fine today, just tightens the timing budget for no reason.

state.processed_rows += total_processed;
let pending = if total_processed > 0 {
candidate_state.processed_rows += total_processed;
for (table, offset) in state_updates {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

state_updates and this merge loop are leftovers from the old locked-state design - candidate_state is a local clone now, offsets can go straight into it in the table loop. drops the stale lock-era comments too.

}

#[derive(Debug)]
enum PendingOperation {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

primary_key_column/slot_name are derivable from config at ack time - small slot_name()/pk_column() helpers (like payload_format()) would dedupe the three existing fallback chains and drop these two fields. optional.

Error::InvalidRecord
})?;
let rows = sqlx::query(
"SELECT lsn::text AS lsn, xid, data FROM pg_logical_slot_peek_changes($1, NULL, $2)",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

xid is selected but never read - drop it.

use std::time::Duration;
use tokio::time::sleep;

const API_KEY: &str = "test-api-key";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

API_KEY/SOURCE_KEY duplicate postgres_source_cdc.rs - SOURCE_KEY fits in postgres/mod.rs next to the other shared consts. source_errors/wait_for_source_errors could also share one fetch helper returning Option.

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

connectors: no send acknowledgment from runtime to source plugins — state advances and rows are deleted before delivery is confirmed

2 participants