GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — [connectors] Replay the transaction in the exactly-once Kafka output by bestouff · Pull Request #7682 · feldera/feldera

Conversation

@bestouff

@bestouff bestouff commented Oct 28, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Linked Issue

Third step for #7578, stacked on #7653.

A failed commit left the exactly-once Kafka producer unusable while the controller went on with later batches. This change removes the five-retry limit and keeps each transaction's records until commit:

Transaction error Action
Retriable, including an API timeout Retry with backoff
Requires abort Abort, begin a new transaction, replay the records, commit
Fatal or unclassified Report a fatal error and hold output until shutdown

A transaction cannot simply be retried: librdkafka fails it at the first failed delivery and purges its queue on abort, and the broker aborts transactions older than transaction.max.timeout.ms. So the sink keeps its own copy. The replay spool holds 64 MiB in memory, then spills to a temporary file in the pipeline's storage directory, or the system temporary directory without one. A record that librdkafka refuses for its own sake, such as an oversized message, is reported once and not kept. The broker refuses whole batches, so a broker refusal, a revoked permission, a missing topic or an expired transaction drops nothing: after three aborts in a row the sink reports a connector error that names the cause and keeps retrying. Each transaction call waits at most 10 seconds and cancellation is checked between calls. A fenced producer is never recreated; a restart skips transactions already committed.

Hot path cost of the spool, release build against the mock broker, 50k records of 1 KiB per transaction, three rounds each:

Variant push_buffer loop Commit done after
Before this change 18 to 33 ms 0.52 to 0.64 s
Spool in memory 14 to 36 ms 0.52 to 0.54 s
Spool spilled to disk (1 MiB limit) 106 to 145 ms 0.51 to 0.55 s

The sink reports unhealthy during recovery and exports the shared queue metrics plus kafka_output_transaction_aborts_total, kafka_output_commit_retries_total and kafka_output_replay_spool_bytes.

PRs #7432 and #7433 touch the same code and need a rebase when this lands.

Describe Manual Test Plan

Twelve mock-cluster tests and five single-broker tests through a test-only TCP proxy: abort and replay from disk with headers, null payloads and several partitions; a commit reply delayed past the API timeout; restart after a lost commit reply; fencing by a replacement producer; an outage longer than the transaction timeout. Each ends with exactly one committed copy.

Mock-cluster cases added for review findings: a record refused on its first send is not reported again by a replay; a record first refused during replay is reported once over two replays; a topic-level produce error is replayed, not dropped; an abortable error at begin leaves a fresh transaction open; a spool write failure holds the sink without a commit; a batch the broker refuses is reported once after repeated aborts and nothing is dropped. Each fails with its fix reverted.

With ghcr.io/feldera/redpanda-ci:v24: all transport::kafka:: tests pass, the fencing test passed 20 parallel runs, clippy -D warnings and cargo fmt --check pass.

Checklist

  • Unit tests added/updated
  • Integration tests added/updated
  • Documentation updated
  • Changelog updated

Breaking Changes?

  • OpenAPI / REST HTTP API / feldera-types / manager
  • Feldera SQL
  • feldera-sqllib
  • Python SDK
  • fda
  • Adapters (including configuration)
  • Storage Format / Checkpoints

Describe Incompatible Changes

The exactly-once sink no longer forces retries=5. Recoverable failures hold output during recovery, fatal ones until restart. The sink keeps up to 64 MiB of replay data in memory and uses temporary disk space above that. Position formats are unchanged.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@bestouff bestouff mentioned this pull request Oct 28, 2026
[docs] Link the Kafka output delivery guarantees from the pages that need them #7683
Merged
1 of 4 tasks
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 28, 2026
View reviewed changes
Comment thread crates/connectors/src/transport/kafka/ft/output/tests.rs Outdated
.unwrap()
.expect_err("a fatal error must end the batch with an error");
assert!(error.to_string().contains("fenced"), "{error}");
let (fatal, tag, text) = &callbacks.errors()[0];

@mythical-fred-oss mythical-fred-oss Bot Oct 28, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

bug (high, flaky test): errors()[0] isn't always the fatal one. The test failed once in about 50 runs under parallel load (assertion failed: *fatal, line 285): a non-fatal error sometimes comes first. Look up the fatal entry instead:

Suggested change
let (fatal, tag, text) = &callbacks.errors()[0];
let errors = callbacks.errors();
let (fatal, tag, text) = errors.iter().find(|(fatal, ..)| *fatal).unwrap();

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 28, 2026
View reviewed changes
Comment thread crates/connectors/src/transport/kafka/ft/output/tests.rs Outdated
message.payload_view::<str>().unwrap().unwrap().to_owned(),
));
}
received.len() >= expected

@mythical-fred-oss mythical-fred-oss Bot Oct 28, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

missing (medium): read_topic stops once it has expected messages, so it never shows that there are no extra records. A duplicate after the first N passes, and read_topic(&cluster, 6).len() == 6 (line 180) can't fail. To back the "exactly once" claim, read up to the high watermark, or poll until a quiet period, and then assert the exact count.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 28, 2026
View reviewed changes
Comment thread crates/connectors/src/transport/kafka/ft/spool.rs Outdated
let size = record.size();
if self.file.is_none() && self.memory_bytes + size > self.memory_limit {
let mut writer = BufWriter::new(
tempfile::tempfile().context("creating the replay spool file for Kafka output")?,

@mythical-fred-oss mythical-fred-oss Bot Oct 28, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

pitfall (medium): any step over 64 MiB now spills to the system temp dir on the normal (no-failure) path. In containers that directory is often a small or read-only overlay, or a tmpfs, and a tmpfs counts against pod memory, which defeats the spill. A failed write there is now fatal and parks the pipeline. Consider spilling under the pipeline's storage directory, when one is configured. Also, there is no cap on spill size, and nothing besides the bytes gauge shows it.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mythical-fred-oss

mythical-fred-oss Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What I ran (PR head be944ac, debug build)

Check Result
cargo test -p dbsp_connectors (4 mock-cluster, 3 spool, 1 metrics) pass
mock-cluster tests ×~55, including 3 parallel copies 1 failure: a_fenced_producer_holds_the_sink_until_shutdown (see inline)
pre-commit run --files <changed> pass
extra QA test below: two outages in consecutive transactions pass
  • Gate failed: flaky tests (high). Zero tolerance, so the fenced-producer assertion needs fixing before merge.
  • Commit quality (medium). 2a4c15e ("Replay the transaction…") doesn't compile alone: kafka::metrics and SendError arrive only in the later commit be944ac. That breaks bisect on a linear history. Reorder or squash.
  • Missing coverage (medium).
    • Nothing exercises the spill-to-file path through the endpoint. The 64 MiB SPOOL_MEMORY_LIMIT isn't injectable, so the spool's unit tests are the only coverage.
    • No test covers a replay that itself hits NeedsAbort (a broker that flaps during the replay), or the Terminal path followed by the next batch_start.
    • No test runs an outage longer than transaction.timeout.ms against a real broker, to check whether a coordinator-side abort comes back as abortable or as fenced/fatal.

QA test I added locally:

#[test]
fn qa_consecutive_outages_recover_independently() {
    let cluster = cluster();
    let (mut endpoint, callbacks, _shutdown) = endpoint(&cluster);
    let mut total = 0;
    for txn in 0..2u64 {
        endpoint.batch_start(txn, OutputBatchType::Delta).unwrap();
        cluster.broker_down(1).unwrap();
        for i in 0..3 {
            endpoint.push_buffer(format!("t{txn} m{i}").as_bytes()).unwrap();
        }
        total += 3;
        let before = callbacks.metric("kafka_output_delivery_errors_total");
        wait(|| callbacks.metric("kafka_output_delivery_errors_total") > before, 30_000).unwrap();
        let worker = thread::spawn(move || (endpoint.batch_end(), endpoint));
        thread::sleep(Duration::from_millis(1500));
        cluster.broker_up(1).unwrap();
        let (result, e) = worker.join().unwrap();
        result.unwrap();
        endpoint = e;
        assert_eq!(callbacks.metric("kafka_output_replay_spool_bytes"), 0.0);
    }
    assert!(callbacks.metric("kafka_output_transaction_aborts_total") >= 2.0);
    let expected: Vec<String> = (0..2).flat_map(|t| (0..3).map(move |i| format!("t{t} m{i}"))).collect();
    assert_eq!(read_topic(&cluster, total).into_iter().map(|(_, p)| p).collect::<Vec<_>>(), expected);
}
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

bestouff added a commit that referenced this pull request Oct 28, 2026
@bestouff
[docs] Link the Kafka output delivery guarantees from the pages that …
6c8fdf6
…need them

The Kafka sink page now documents delivery, retries, backpressure, health,
exactly-once recovery and the connector's metrics, but the pages a reader
starts from did not point there. The fault tolerance page names the Kafka
output connector as the one with exactly-once delivery and links its
recovery section. The metrics reference says that output connectors can
export metrics of their own and links the Kafka ones. The sink page's
introduction links its own delivery sections.

Fourth step for #7578, stacked on #7682.
@bestouff
bestouff force-pushed the issue-7578-step3 branch from be944ac to dd6c1f6 Compare October 28, 2026 18:49
@bestouff bestouff mentioned this pull request Oct 28, 2026
[connectors] Give transport output endpoints a health and metrics channel #7639
Merged
3 of 12 tasks
@bestouff
bestouff added this pull request to stack #7686 October 28, 2026 19:05
@bestouff
bestouff requested a review from blp October 28, 2026 19:07
@bestouff
bestouff marked this pull request as ready for review October 28, 2026 19:17
bestouff added a commit that referenced this pull request Oct 28, 2026
@bestouff
[docs] Link the Kafka output delivery guarantees from the pages that …
1e2176e
…need them

The Kafka sink page now documents delivery, retries, backpressure, health,
exactly-once recovery and the connector's metrics, but the pages a reader
starts from did not point there. The fault tolerance page names the Kafka
output connector as the one with exactly-once delivery and links its
recovery section. The metrics reference says that output connectors can
export metrics of their own and links the Kafka ones. The sink page's
introduction links its own delivery sections.

Fourth step for #7578, stacked on #7682.

Signed-off-by: Xavier Bestel <[email protected]>
@bestouff
bestouff force-pushed the issue-7578-step3 branch from dd6c1f6 to ea7c518 Compare October 28, 2026 19:31
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 28, 2026
View reviewed changes
Comment thread crates/connectors/src/transport/kafka/ft/output.rs Outdated
}

// Keep the record before sending it, so that a replay has it.
if let Err(error) = self.spool.push(record.clone()) {

@mythical-fred-oss mythical-fred-oss Bot Oct 28, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

bug (medium): a record that the first send refuses is already in the spool. push_key returns the error, and then every later replay attempt reports it again with kafka_ft_replay. During a long outage that repeats once per abort cycle. I confirmed it with this test, which fails with 2 replay errors after one refused push and one forced abort:

#[test]
fn a_record_refused_on_first_send_is_not_reported_again_on_replay() {
    let cluster = cluster();
    let (mut endpoint, callbacks, _shutdown) = endpoint(&cluster);
    endpoint.batch_start(0, OutputBatchType::Delta).unwrap();
    assert!(endpoint.push_buffer(&vec![b'x'; DEFAULT_MAX_MESSAGE_SIZE + 1]).is_err());
    cluster.request_errors(RDKafkaApiKey::Produce,
        &[RDKafkaRespErr::RD_KAFKA_RESP_ERR_TOPIC_AUTHORIZATION_FAILED]);
    endpoint.push_buffer(b"forces an abort").unwrap();
    wait(|| callbacks.metric("kafka_output_delivery_errors_total") > 0.0, 30_000).unwrap();
    let (result, _) = BatchWorker::new(endpoint).finish();
    result.unwrap();
    let n = callbacks.errors().iter().filter(|(_, t, _)| *t == Some("kafka_ft_replay")).count();
    assert_eq!(n, 0, "{:?}", callbacks.errors()); // fails: 2
}

Suggested fix: send &record first, then move it into the spool on Sent/NeedsAbort only. That also removes the record.clone(), which copies every payload a second time on the hot path. Records refused for the first time during replay still need a "reported once" marker, for example dropping them from the spool or keeping a set of refused indices.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 28, 2026
View reviewed changes
Comment thread crates/connectors/src/transport/kafka.rs
pub error: KafkaError,
}

impl std::fmt::Display for SendError {

@mythical-fred-oss mythical-fred-oss Bot Oct 28, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

pitfall (low-medium): Display already prints self.error, and source() returns it as well, so {error:#} prints the cause twice. Users see this text in the replay error: ...topic 'ft_test_topic': Message production error: MessageSizeTooLarge (...): Message production error: MessageSizeTooLarge (...): MessageSizeTooLarge (...). Either leave self.error out of Display, or return None from source().

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
mythical-fred-oss[bot]
mythical-fred-oss Bot reviewed Oct 28, 2026
View reviewed changes
Comment thread crates/connectors/src/transport/kafka/ft/spool.rs Outdated
let size = record.size();
if self.file.is_none() && self.memory_bytes + size > self.memory_limit {
let mut writer = BufWriter::new(
tempfile::tempfile().context("creating the replay spool file for Kafka output")?,

@mythical-fred-oss mythical-fred-oss Bot Oct 28, 2026

Copy link
Copy Markdown
Contributor

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

pitfall (medium): tempfile::tempfile() writes to $TMPDIR, which is usually a small ephemeral container filesystem, and it has no size bound. One large transaction, for example the first output after a backfill or a big snapshot, now copies its whole output to /tmp. Before this change it needed no local disk at all. If that disk fills up, push_key calls fail_fatally and the pipeline stalls. Please consider spilling into the pipeline's storage directory when one is configured. Also consider exposing the threshold, or at least documenting the expected disk use per transaction.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@mythical-fred-oss

mythical-fred-oss Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

What I ran

Check Result
cargo test -p dbsp_connectors --lib spool and mock-cluster ft::output::tests (10 tests) pass, 5 repeated runs stable (~13 s each)
pre-commit run --files <changed> pass
real::* (Redpanda via proxy) not run, no broker here
Extra QA test: a record refused on first send, then an abort fails (see inline comment on output.rs)

Gates: tests, docs, changelog and breaking-change notes are present. No unsafe, no new dependencies. Per-type SQL coverage does not apply: the spool replays encoded bytes and does not depend on the column type.

Missing tests (medium)

  • A spool I/O failure (temp dir full or read-only) should reach fail_fatally without committing. Nothing covers this today.
  • A begin_transaction error on the Begin { replay: false } path (an abortable error there leads to an abort with no open transaction).
  • A refused record that is replayed more than once (see the inline comment).

Flakiness (low): the mock tests depend on wall-clock timing: the 10 s API timeout, plus elapsed < 1s and < 15s assertions. They were stable here, but under a loaded CI runner shutdown_ends_a_stalled_commit has only a 5 s margin.

Overall the design looks sound. Please address the double-reporting bug before merge.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

bestouff added a commit that referenced this pull request Oct 28, 2026
@bestouff
[docs] Link the Kafka output delivery guarantees from the pages that …
4f61f74
…need them

The Kafka sink page now documents delivery, retries, backpressure, health,
exactly-once recovery and the connector's metrics, but the pages a reader
starts from did not point there. The fault tolerance page says that output
connectors document their own support, that today only the Kafka output
connector supports exactly-once output, and links its recovery section.
The metrics reference says that output connectors can export metrics of
their own and points at the Kafka and Delta Lake pages. The sink page's
introduction links its own delivery sections.

Fourth step for #7578, stacked on #7682.

Signed-off-by: Xavier Bestel <[email protected]>
@bestouff
bestouff force-pushed the issue-7578-step3 branch from ea7c518 to 71463b1 Compare October 28, 2026 21:20
blp
blp reviewed Oct 28, 2026
View reviewed changes
Comment thread crates/connectors/src/transport/kafka/nonft/output.rs Outdated
// error would incorrectly imply that records were lost.
debug!("Kafka producer reconnecting: {error}; {reason}");
// error would incorrectly imply that records were lost. Log it,
// so the log says why the pipeline parked.

@blp blp Oct 28, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I don't recognize the term "parked" here. Perhaps "hung"?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/connectors/src/transport/kafka/ft/output.rs
const SPOOL_MEMORY_LIMIT: usize = 64 << 20;

/// Longest wait for one transaction API call. A call that times out returns a
/// retriable error and the next call resumes it, so this bounds the shutdown

@blp blp Oct 28, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I would spell this "retryable"

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions

@blp blp Oct 28, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I take it back, apparently librdkafka uses "retriable"

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@blp

blp commented Oct 28, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

My first thought in looking at this was that it's odd to record and replay transactions instead of just retrying them forever. Looking closer, I found out that this doesn't work because librdkafka puts a limit on the length of a transaction, and even if we made that very long, Kafka brokers also cap the length of a transaction (at 15 minutes by deafult). So we have to record and replay.

I feel like this should be mentioned somewhere, probably both in comments and in the commit message, because it's surprising.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@blp

blp commented Oct 28, 2026

Copy link
Copy Markdown
Member

These notes are from Claude (an AI assistant), posted at the request of blp, who is also writing a separate review. Line numbers refer to crates/connectors/src/transport/kafka/ft/output.rs at 71463b1.

The earlier bot findings (double report on replay, spill directory, SendError duplicate cause, flaky errors()[0], commit that did not build alone) look fixed. These are new:

1. A record that the broker refuses at delivery time makes the sink loop forever (high)

send_record (L205-221) only classifies refusals that librdkafka returns from produce(). The broker can also refuse a single record at delivery time, for example:

  • a record larger than the topic's max.message.bytes, which can be smaller than the client's message.max.bytes;
  • INVALID_RECORD (compaction requirements, broker-side schema validation);
  • CORRUPT_MESSAGE.

That failure goes to delivery() (L1018-1029), which only counts it and logs at debug level. The transaction then needs an abort. The sink aborts, replays the same record, the broker refuses it again, and this repeats every 10 s with no end. The only signal is the unhealthy status, with no connector error. Before this change, the sink stopped after 5 retries. The docs say "a permanently refused record ... is dropped and reported", which is not true for this case.

Possible fix: set DeliveryOpaque to the spool index, so that the delivery callback can sort out per-record permanent codes and add the index to refused before the next replay.

2. A transaction that cannot be sent within transaction.timeout.ms never commits (medium)

If sending the spool plus the commit takes longer than transaction.timeout.ms (60 s by default), every replay expires on the broker. The sink aborts and replays without end. The first output after a backfill or a snapshot is the likely case, and the docs use that same case for spool sizing. Nothing detects this. As a minimum, after N consecutive aborts in drive (L476-491), log a warning that names transaction.timeout.ms, and document this limit.

3. Hot-path cost (medium, request for numbers)

push_key (L709-723) now copies every payload and clones the configured headers for each record. Above 64 MiB it also serializes each record with rmp to disk. This happens on the normal path, when there is no failure. A before/after throughput number for a large output would show if this is significant.

4. Smaller items

  • L553-577 is not formatted. The long string on L566 stops rustfmt in the closure. Move the message out and rustfmt can format the match.
  • The error text on L583 says "temporary directory" and L758 says "spool directory". The file can now be in the storage directory, so use one term.
  • TopicAuthorizationFailed and UnknownTopicOrPartition go to NeedsAbort (L217-219). That is correct for a short metadata glitch. But for a deleted topic or a revoked ACL, the sink aborts and replays forever with only an unhealthy status. Consider a connector error after some time.
  • blp noted earlier that record-and-replay is necessary because librdkafka and the broker limit how long a transaction can stay open. The docs now say this, but a code comment near ReplaySpool or drive, and a sentence in the commit message, would help the next reader.
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

blp
blp approved these changes Oct 28, 2026
View reviewed changes
Comment thread crates/connectors/src/transport/kafka/ft/output.rs

/// Hands `record` to `producer`, waiting out a full queue, and sorts the
/// result for the commit loop.
fn send_record(

@blp blp Oct 28, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

This function seems familiar. Maybe there is a very similar function in the non-ft version?

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread crates/connectors/src/transport/kafka/ft/spool.rs

impl SpooledRecord {
/// Bytes this record holds in memory, including the container overhead.
pub fn size(&self) -> usize {

@blp blp Oct 28, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

Might be able to just use the SizeOf trait that we use eleswhere, e.g. #[derive(SizeOf)] then size_of().total_bytes().

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread docs.feldera.com/docs/connectors/sinks/kafka.md Outdated
transaction must be aborted, the connector aborts it, begins a new one, sends
the kept records again, and commits. It repeats this, with a growing backoff
of up to 10 seconds, until the commit succeeds or the pipeline shuts down. A
commit that only times out is repeated as is, without a replay. Aborted

@blp blp Oct 28, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

A commit that only times out is repeated as is, without a replay.

I'm not sure of the difference between a repeat and a replay.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
Comment thread docs.feldera.com/docs/connectors/sinks/kafka.md Outdated
It reports healthy again after the next successful commit.

A fatal producer error, or a transaction error whose recovery action is unknown,
is reported as a fatal connector error. One example is a producer fenced by

@blp blp Oct 28, 2026

Copy link
Copy Markdown
Member

There was a problem hiding this comment.

Choose a reason for hiding this comment

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

I had to look up the term "fenced" in relation to Kafka (maybe I knew it once but I forgot). Perhaps defining it here would be helpful to others. For example:

Kafka fences a transactional producer when another producer starts with the same transactional.id: the broker then refuses all further writes from the older producer, so that only one instance can commit.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
bestouff added a commit that referenced this pull request Oct 29, 2026
@bestouff
[docs] Link the Kafka output delivery guarantees from the pages that …
373bec8
…need them

The Kafka sink page now documents delivery, retries, backpressure, health,
exactly-once recovery and the connector's metrics, but the pages a reader
starts from did not point there. The fault tolerance page says that output
connectors document their own support, that today only the Kafka output
connector supports exactly-once output, and links its recovery section.
The metrics reference says that output connectors can export metrics of
their own and points at the Kafka and Delta Lake pages. The sink page's
introduction links its own delivery sections.

Fourth step for #7578, stacked on #7682.

Signed-off-by: Xavier Bestel <[email protected]>
@bestouff
bestouff force-pushed the issue-7578-step3 branch from 71463b1 to 4a53112 Compare October 29, 2026 13:00
@bestouff
bestouff added this pull request to the merge queue Oct 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Oct 29, 2026
bestouff added a commit that referenced this pull request Oct 29, 2026
@bestouff
[docs] Link the Kafka output delivery guarantees from the pages that …
89c1f1b
…need them

The Kafka sink page now documents delivery, retries, backpressure, health,
exactly-once recovery and the connector's metrics, but the pages a reader
starts from did not point there. The fault tolerance page says that output
connectors document their own support, that today only the Kafka output
connector supports exactly-once output, and links its recovery section.
The metrics reference says that output connectors can export metrics of
their own and points at the Kafka and Delta Lake pages. The sink page's
introduction links its own delivery sections.

Fourth step for #7578, stacked on #7682.

Signed-off-by: Xavier Bestel <[email protected]>
@bestouff
bestouff force-pushed the issue-7578-step3 branch from 4a53112 to 38740c4 Compare October 29, 2026 15:40
@bestouff
bestouff added this pull request to the merge queue Oct 29, 2026
@bestouff
bestouff removed this pull request from the merge queue due to a manual request Oct 29, 2026
@mihaibudiu
mihaibudiu added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Oct 30, 2026
@mihaibudiu
mihaibudiu added this pull request to the merge queue Oct 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Oct 30, 2026
Base automatically changed from issue-7578-step2 to main October 30, 2026 06:20
@bestouff @mihaibudiu
[connectors] Recover Kafka output transactions through broker outages
1ae5241
The exactly-once Kafka sink stopped after five delivery retries. A
failed commit then left its producer in an error state, and later
batches failed while the controller continued processing them.

A transaction cannot simply be retried: librdkafka fails it at its first
failed delivery and purges its queue on abort, and the broker aborts any
transaction older than transaction.max.timeout.ms (15 minutes by
default). So keep each transaction's encoded records in a replay spool,
with a 64 MiB memory threshold and a spill file in the pipeline's
storage directory. Retry retriable commits as-is; abort and replay
abortable transactions. Fatal and unclassified errors report the failure
and hold output until shutdown, retaining the spool. Never recreate a
fenced producer. A broker can fence an expired transaction too; restart
then recovers from the last committed position.

A record that librdkafka refuses for its own sake is reported once and
dropped, also when a replay first discovers it. The broker refuses whole
batches, so a broker refusal, a revoked permission, a missing topic or
an expired transaction drops nothing: after three aborts in a row the
sink reports a connector error that names the cause and keeps retrying.

Check cancellation between transaction calls, during backoff, and before
each replayed send. Expose health and queue, delivery, transaction, and
spool metrics.

Mock tests cover error classification, replay refusals, broker refusals,
cancellation, and fencing. Real-broker tests use a connection proxy to
exercise disk-backed replay, delayed and lost commit replies, restart
deduplication, ownership fencing, and transaction expiry. Consumers read
through the high watermark so assertions catch trailing duplicates as
well as missing records.

Third step for #7578, stacked on #7653.

Signed-off-by: Xavier Bestel <[email protected]>
mihaibudiu pushed a commit that referenced this pull request Oct 30, 2026
@bestouff @mihaibudiu
[docs] Link the Kafka output delivery guarantees from the pages that …
6e244c6
…need them

The Kafka sink page now documents delivery, retries, backpressure, health,
exactly-once recovery and the connector's metrics, but the pages a reader
starts from did not point there. The fault tolerance page says that output
connectors document their own support, that today only the Kafka output
connector supports exactly-once output, and links its recovery section.
The metrics reference says that output connectors can export metrics of
their own and points at the Kafka and Delta Lake pages. The sink page's
introduction links its own delivery sections.

Fourth step for #7578, stacked on #7682.

Signed-off-by: Xavier Bestel <[email protected]>
@mihaibudiu
mihaibudiu force-pushed the issue-7578-step3 branch from 38740c4 to 1ae5241 Compare October 30, 2026 06:20
@mihaibudiu
mihaibudiu added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 6ad915e Oct 30, 2026
1 check passed

Uh oh!

There was an error while loading. Please reload this page.

@mihaibudiu
mihaibudiu deleted the issue-7578-step3 branch October 30, 2026 10:05
mihaibudiu pushed a commit that referenced this pull request Oct 30, 2026
@bestouff @mihaibudiu
[docs] Link the Kafka output delivery guarantees from the pages that …
6dcd479
…need them

The Kafka sink page now documents delivery, retries, backpressure, health,
exactly-once recovery and the connector's metrics, but the pages a reader
starts from did not point there. The fault tolerance page says that output
connectors document their own support, that today only the Kafka output
connector supports exactly-once output, and links its recovery section.
The metrics reference says that output connectors can export metrics of
their own and points at the Kafka and Delta Lake pages. The sink page's
introduction links its own delivery sections.

Fourth step for #7578, stacked on #7682.

Signed-off-by: Xavier Bestel <[email protected]>
yermakoffivan pushed a commit to yermakoffivan/feldera that referenced this pull request Oct 30, 2026
@bestouff
[docs] Link the Kafka output delivery guarantees from the pages that …
014fe10
…need them

The Kafka sink page now documents delivery, retries, backpressure, health,
exactly-once recovery and the connector's metrics, but the pages a reader
starts from did not point there. The fault tolerance page says that output
connectors document their own support, that today only the Kafka output
connector supports exactly-once output, and links its recovery section.
The metrics reference says that output connectors can export metrics of
their own and points at the Kafka and Delta Lake pages. The sink page's
introduction links its own delivery sections.

Fourth step for feldera#7578, stacked on feldera#7682.

Signed-off-by: Xavier Bestel <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@mythical-fred-oss mythical-fred-oss[bot] mythical-fred-oss[bot] left review comments
@blp blp blp approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

3 participants

@bestouff @blp @mihaibudiu
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.