Skip to content

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

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

fix(connectors): defer postgres source progress until ack#3957
rohankumardubey wants to merge 11 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 82.27848% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.96%. Comparing base (328b289) to head (0dc0dd0).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
core/connectors/sources/postgres_source/src/lib.rs 80.73% 30 Missing and 12 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3957      +/-   ##
============================================
- Coverage     85.00%   84.96%   -0.04%     
  Complexity     1402     1402              
============================================
  Files          1225     1225              
  Lines        180283   180436     +153     
  Branches     146587   146740     +153     
============================================
+ Hits         153248   153309      +61     
- Misses        22993    23052      +59     
- Partials       4042     4075      +33     
Components Coverage Δ
Rust Core 85.85% <82.27%> (-0.05%) ⬇️
Java SDK 67.29% <ø> (ø)
C# SDK 75.38% <ø> (+0.01%) ⬆️
Python SDK 90.06% <ø> (ø)
PHP SDK 85.65% <ø> (ø)
Node SDK 96.24% <ø> (ø)
Go SDK 69.31% <ø> (ø)
Files with missing lines Coverage Δ
core/connectors/runtime/src/stream.rs 90.86% <100.00%> (+0.76%) ⬆️
core/connectors/sources/postgres_source/src/lib.rs 74.20% <80.73%> (+0.88%) ⬆️

... and 27 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).

Comment thread core/connectors/sources/postgres_source/src/lib.rs
Comment thread core/connectors/sources/postgres_source/src/lib.rs
Comment thread core/connectors/sources/postgres_source/src/lib.rs
Comment thread core/connectors/sources/postgres_source/src/lib.rs
Comment thread core/connectors/sources/postgres_source/README.md
Comment thread core/integration/tests/connectors/postgres/postgres_source.rs
Comment thread core/connectors/sources/postgres_source/src/lib.rs Outdated
Comment thread core/connectors/sources/postgres_source/src/lib.rs
Comment thread core/connectors/sources/postgres_source/src/lib.rs Outdated
Comment thread core/integration/tests/connectors/postgres/postgres_source.rs Outdated
@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
@rohankumardubey
rohankumardubey requested a review from hubcio August 26, 2026 10:54
@hubcio

hubcio commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

@rohankumardubey please write
/ready
when PR is ready for review. at any top level comment, in the beginning of any line. this will let know maintainers that something is ready for review by flipping gh labels.
image

@github-actions github-actions Bot added S-waiting-on-review PR is waiting on a reviewer and removed S-waiting-on-author PR is waiting on author response labels Aug 26, 2026
@rohankumardubey

Copy link
Copy Markdown
Contributor Author

/ready

@hubcio

hubcio commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

looks like CI is failing.

@hubcio hubcio 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 31, 2026
@rohankumardubey

Copy link
Copy Markdown
Contributor Author

The failure came from a merge-resolution type mismatch in the TLS connection-string branch. Thanks @hubcio

@rohankumardubey

Copy link
Copy Markdown
Contributor Author

/ready

@github-actions github-actions Bot removed the S-waiting-on-author PR is waiting on author response label Aug 31, 2026
@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Aug 31, 2026

@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.

the staging model is right and closes the old send-failure hole. two things block: 55006 isn't transient, so any slot collision at ack time stops the source for good, and an empty cdc peek never advances the slot, so an idle database retains wal without bound. the tracking <= boundary guard also regresses custom_query, null and numeric tracking users, and the new cdc test races the connector on the slot.

outside this diff, two pre-existing bugs in build_polling_query worth their own issue: $now is replaced before $now_unix so $now_unix never expands, and $offset is substituted raw into custom_query from row data.

let pool = self.get_pool()?;
with_retry(
|| {
sqlx::query("SELECT pg_replication_slot_advance($1, $2::pg_lsn)")

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.

55006 (slot active for another pid) isn't in is_transient_error, so one collision on this advance stops the source for good with zero retries. add it to the transient list.

debug!("CDC: Fetched {} change records", messages.len());
}
Ok(messages)
let pending = if let Some(lsn) = last_lsn {

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.

an empty peek never advances the slot (get_changes confirmed the reader position on every poll), so an idle database in a busy cluster now pins restart_lsn and retains wal without bound. read pg_current_wal_flush_lsn() before the peek and advance to it when no rows come back.

.bind(DEFAULT_SLOT)
.fetch_all(pool)
.await
.expect("CDC replication slot should be readable");

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.

this peeks the connector's live slot without a 55006 retry while the connector polls it every 50ms, so either side can lose the nowait acquire and the test flakes. retry on 55006 like drop_replication_slot_retrying.

format!("DELETE FROM {quoted_table} WHERE {quoted_pk} IN ({ids_list})");
let delete_query = format!(
"DELETE FROM {quoted_table} WHERE {quoted_pk} IN ({ids_list}) \
AND {quoted_tracking} <= {tracking_boundary}"

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.

max_offset is the last row's value, not the max, so with a custom_query that isn't ordered by tracking this guard skips rows and they get redelivered every poll. apply the boundary only on the built query path, or match exact (pk, tracking) pairs.

self.mark_or_delete_processed_rows(pool, table, pk_column, &processed_ids)
.await?;
if self.should_process_rows() && !processed_ids.is_empty() {
let max_offset = max_offset.clone().ok_or_else(|| {

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 custom query that selects the pk but not the tracking column now fails every poll here (when primary_key_column differs from tracking_column), and the source keeps reporting running. make max_offset optional and skip the boundary when it's None.

)
.await
.map_err(|e| {
error!("Failed to delete processed rows: {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.

also at lines 674, 752.

with_retry already logs the failure and the sdk logs the returned error again, so this is the third line for the same event. drop it, the error payload carries the context.

### Mark as Processed

Updates a boolean column instead of deleting:
Updates a boolean column after Iggy acknowledges the batch instead of deleting:

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 one sentence here: a row whose tracking value moves past the batch boundary between poll and ack is left unmarked and comes back next poll.

- Keep `State` small - rewritten every batch. No unbounded vecs.

The SDK allows one in-flight batch. Five consecutive NACKs stop the source and
require a manual restart. Returning `Err` from `on_batch_result` is fatal, so

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 callback runs inside the sdk's 30s batch-result window and this tells authors to retry inside it without saying so. name the budget.


async fn on_batch_result(&self, result: SourceBatchResult) -> Result<(), Error> {
let pending = self.pending_batch.lock().await.take();
match result {

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.

match plus let Some ... else collapses to one line: let (SourceBatchResult::Ack, Some(pending)) = (result, self.pending_batch.lock().await.take()) else { return Ok(()) };. same shape fits TEMPLATE.md.

#[test]
fn given_nack_when_batch_is_staged_should_keep_committed_state() {
let src = PostgresSource::new(1, test_config(), None);
let runtime = tokio::runtime::Runtime::new().expect("failed to create test runtime");

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 module now mixes #[tokio::test] (line 2662) with hand-built runtimes. pick one, #[tokio::test] is shorter.

@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 Sep 1, 2026
@rohankumardubey

Copy link
Copy Markdown
Contributor Author

@hubcio Agreed, I’ll track staged-operation persistence and replay, along with the two pre-existing custom_query bugs, in separate follow-up issues.

@hubcio

hubcio commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

@rohankumardubey sure thing. if you plan to do that, please fill-in the TODOs in code so that it won't be forgotten (in relevant places).

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