Skip to content

feat(connectors): let sinks control when offsets are committed - #3954

Open
kriti-sc wants to merge 2 commits into
apache:masterfrom
kriti-sc:manual-offset-commit
Open

feat(connectors): let sinks control when offsets are committed#3954
kriti-sc wants to merge 2 commits into
apache:masterfrom
kriti-sc:manual-offset-commit

Conversation

@kriti-sc

Copy link
Copy Markdown
Contributor

Closes #2928, #2927
Related: #3203

Rationale

Sinks must have an option to decide exactly when it is safe to advance the offset, as only the sink knows when a write is truly durable from the target's perspective.

What changed?

This PR adds an offset_commit setting to sink configs:

  • after_polling (default) - existing behaviour of offset being committed when messages are polled. Nothing changes for existing configs.
  • after_consuming - the offset is stored only once the sink reports that it wrote the batch.

The offset commit decision hinges upon the i32 returned by the sink.

This also required binding the i32 returned by the sink, which was previous discarded. Now, the sink exits if i32 != 0, regardless of offset commit configuration, which is the correct behaviour as a sink returns error for failures lying outside its purview.

This PR also introduces integration tests that inject failures into a running sink and verify the desired commit behaviour.

@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 22, 2026
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.03%. Comparing base (e362c0f) to head (9766124).
⚠️ Report is 24 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3954      +/-   ##
============================================
+ Coverage     84.98%   85.03%   +0.04%     
  Complexity     1402     1402              
============================================
  Files          1226     1225       -1     
  Lines        180701   179798     -903     
  Branches     147005   146102     -903     
============================================
- Hits         153575   152889     -686     
+ Misses        23070    22897     -173     
+ Partials       4056     4012      -44     
Components Coverage Δ
Rust Core 85.94% <ø> (+0.06%) ⬆️
Java SDK 67.29% <ø> (ø)
C# SDK 75.37% <ø> (ø)
Python SDK 90.06% <ø> (ø)
PHP SDK 85.65% <ø> (ø)
Node SDK 96.22% <ø> (-0.03%) ⬇️
Go SDK 69.35% <ø> (+0.03%) ⬆️
Files with missing lines Coverage Δ
core/connectors/runtime/src/configs/connectors.rs 41.07% <ø> (ø)
core/connectors/runtime/src/error.rs 26.66% <ø> (ø)
core/connectors/runtime/src/main.rs 85.76% <ø> (ø)
core/connectors/runtime/src/manager/sink.rs 94.16% <ø> (ø)
core/connectors/runtime/src/sink.rs 78.81% <ø> (+2.96%) ⬆️

... and 45 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.

@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs.

If you need a review, please ensure CI is green and the PR is rebased on the latest master. Don't hesitate to ping the maintainers - either @core on Discord or by mentioning them directly here on the PR.

Thank you for your contribution!

@github-actions github-actions Bot added the S-stale Inactive issue or pull request label Aug 30, 2026
@github-actions github-actions Bot removed the S-stale Inactive issue or pull request label Sep 1, 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.

plz read this end to end. the core claim holds - i manually checked code, from after_consuming through the server's PollingKind::Next handling and a restarted consumer really does resume at stored_offset + 1. the tests are honest too: the after_polling one asserts the loss instead of hiding it.

three things before this lands.

the failure semantics change on the default path, not just the opt-in one. SinkContainer::consume maps every Err from Sink::consume to 1, so after this each of the 15 shipped sinks permanently stops its connector on a single transient failure - no retry, no backoff, no auto-restart, restart_connector only reachable over HTTP. "nothing changes for existing configs" in the description is not right, and your own given_after_polling_when_sink_rejects_batch_should_still_advance_offset asserts the new stop. either gate the hard stop behind AfterConsuming, or add backoff.

the sibling-task shutdown is a second behaviour change riding along, and it drops partial batches. it deserves at least a line in the description.

a question i could not settle: the batch is not partition-scoped but the commit is. consume_messages pushes into one batch regardless of message.partition_id, overwriting partition_id and message_offset each iteration, then commits only the last message's partition. a group member owning several partitions could flush a batch spanning a switch and leave the other partition's offset stale. i could not build a trigger - the server bounds a read by commit_offset and batch_size equals batch_length - so it may be unreachable by construction. is it?

on "closes #2928": the literal condition in that issue survives by default. its stated impact is resolved opt-in and #2927 is fixed outright, so this may well be fine - just flagging it rather than asserting it.

ci is red on two one-line fixes (module ordering, a stray blank line in one toml). everything else red is matrix fail-fast cancellation rather than real failures, which also means clippy, sort, machete, doctest and the test suite have never reported on this head. the new config unit tests have not run anywhere, since cargo build skips cfg(test). the three --locked cross-builds did pass.

// modes stop: continuing would hand every later batch to the same
// failing target, and under `AfterPolling` each of those is already
// committed at poll time, so the topic would drain into nothing.
return Err(RuntimeError::SinkRejectedBatch(plugin_id, consume_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.

critical: this fires under the default after_polling too, and the SDK maps every Err from a sink to 1, so one transient write failure now permanently stops the connector. nothing auto-restarts it - restart_connector is only reachable over HTTP.

// failure condemns the rest. Stopping them here keeps the
// failure domain the same as the recovery domain: the whole
// connector goes down, and `restart_connector` brings it back.
let _ = shutdown_tx.send(());

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.

critical: a sibling task woken by this breaks out and drops its half-filled batch. under after_polling those offsets are already committed, so one topic failing silently loses in-flight messages on the others.


if offset_commit == OffsetCommitMode::AfterConsuming
&& let Err(error) = consumer
.store_offset(message_offset, Some(partition_id))

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.

critical: any IggyError here, including a transient disconnect, kills the connector with no retry. RuntimeError::IggyError prints just "Iggy error", so last_error tells an operator nothing about this path.

return Err(RuntimeError::SinkRejectedBatch(plugin_id, consume_result));
}

if offset_commit == OffsetCommitMode::AfterConsuming

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.

warning: this commits the batch's last offset, marking everything up to it consumed - including messages already dropped by decode or transform failures. after_consuming is at-least-once per batch, not per message.

Err(_) => 0,
};

let (processed_count, decode_us, prepare_us, ffi_us) = 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.

warning: a rejected batch takes the Ok arm, so the decode/prepare/ffi histograms and the benchmark event record it as processed. contradicts the comment right above about sub-stages only on success.

@@ -27,6 +27,8 @@ pub enum RuntimeError {
FailedToSerializeMessagesMetadata,
#[error("Failed to serialize raw messages")]

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.

nit: positional tuple variant - named fields read better, and this message is the only thing a test or an operator can match on.

let batch_index = state.batches_consumed;
let topic_selected =
self.reject_topics.is_empty() || self.reject_topics.contains(&topic_metadata.topic);
let should_fail = topic_selected

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.

nit: batches_consumed is one counter across all topics but the threshold is checked per topic. fine at 0, meaningless at any other value.


mod benchmark;
mod error_isolation;
mod offset_commit;

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.

nit: rustfmt wants this after mod http_state; - this is the cargo fmt failure.

# specific language governing permissions and limitations
# under the License.


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.

nit: extra blank line, drop one. this alone fails both taplo and the license header check.


let consume_result = match &result {
Ok(timing) => timing.consume_result,
Err(_) => 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.

simplification: the Err arm is unreachable - line 446 returns first. this block also repeats the match &result directly below it.

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.

bug(connectors): PollingMessages auto-commit commits offsets before sink processing

2 participants