Client guarantees
pyrula-kafka has no dependency on the workflow or agent packages. Every cell below
maps to a test in pyrula-kafka/tests/conformance
against the broker named under Tested versions. yes means a conformance contract
asserts the guarantee holds; partial means it holds within the stated boundary; n/a
means the capability has no meaning for that surface.
The compat.confluent row is the migration shim: the native
rows’ guarantees, observed through confluent-kafka’s API.
| Surface | delivery | ordering | exit | offset commit | transactions | backpressure | rebalance |
|---|---|---|---|---|---|---|---|
producer | yes | yes | yes | n/a | yes | yes | n/a |
consumer | yes | n/a | yes | yes | yes | yes | yes |
compat.confluent | partial | yes | yes | yes | yes | yes | yes |
Column definitions
Section titled “Column definitions”- delivery: A receipt resolves only after the broker acks the record per
acks(defaultall).- A record the broker refuses fails its receipt with a typed error, never silently.
- Receipts resolve without the caller polling.
- ordering: Records produced to one partition by one producer land in produce order, with
enable_idempotence=True. - exit: Closing settles what is in flight.
- Producer: leaving the context manager (or
close()) drains every queued record and waits for the actor to stop.purge()before close is the explicit drop. - Consumer: wherever a partition leaves (
close(),unsubscribe(),unassign(), a revoke), an offset staged withstore_offsets()is committed regardless ofenable_auto_commit. The fetch position is committed too only when it isTrue.
- Producer: leaving the context manager (or
- offset commit: Nothing is committed that was not polled.
- Auto-commit is off by default (the
compat.confluentshim defaults it on, as confluent-kafka does). commit(batch)commits exactly that batch’s offsets, synchronously, and reports failure.store_offsetsstages offsets for the next commit.
- Auto-commit is off by default (the
- transactions:
begin_transaction/produce_*_txn/send_offsets_to_transaction/commit_transactionland as one unit.abort_transactiondiscards both output and offsets.- A failure from a fenced producer is classified fatal and says so.
- A
read_committedconsumer (the default) never sees an aborted record, including one that follows another transaction’s commit marker.
- backpressure: A full queue refuses new work; it never drops accepted work.
- A produce that would exceed
queue_buffering_max_messages/queue_buffering_max_kbytesis refused whole withQueueFull, before any of its records is accepted. purge()andflush()release capacity.- Consumer:
pause/resumestop and restart fetch without losing position.
- A produce that would exceed
- rebalance: Callbacks fire in order (
on_revoke/on_lostbefore partitions leave,on_assignafter they arrive), with the consumer released.- A callback may
commit,store_offsets,seek,position,committed,pause/resumeand inspect the consumer it was raised from. - It may acknowledge the phase with
assign/unassign(eager) orincremental_assign/incremental_unassign(cooperative-sticky), naming exactly the partitions it received. subscribe,unsubscribe,close, anypoll_*, andcommitinsideon_lostare refused withRebalanceError; the rebalance still completes.- Offsets staged for revoked partitions are committed before they leave, regardless of
enable_auto_commit(the same rule asclose(),unsubscribe(),unassign()and a replacingsubscribe()). The fetch position commits too only when it isTrue. - Under cooperative-sticky assignment, unrevoked partitions keep flowing.
- A consumer evicted for exceeding
max_poll_interval_ms(callback time included) getsMaxPollIntervalExceededfrompoll_batch, once.
- A callback may
Install and tested versions
Section titled “Install and tested versions”All surfaces: built in. Tested against confluentinc/cp-kafka 7.7.0 (floor); also proven: apache/kafka:4.0.0 (apache-kafka, cluster, 2026-10-04), apache/kafka:4.3.1 (apache-kafka, cluster, 2026-10-04), docker.redpanda.com/redpandadata/redpanda:v25.1.12 (redpanda, single, 2026-10-04).
producer:KafkaProducerandAsyncKafkaProducershare one Rust actor; the async class drives it from the event loop’s executor and inherits every cell here (proven directly:TestProducerConformanceparametrises over both)consumer: the async consumer (AsyncKafkaConsumer) is exercised bytests/broker/test_async_lifecycle.py, not by this matrix’s conformance rowscompat.confluent: A translation over the native rows, proven through the shim’s own API against confluent-kafka 2.15.1’s signatures. Cells hold within the translated config.enable.auto.commitdefaults toTrue, confluent-kafka’s default (native:False).- Under auto-commit, the tick,
close()and a revoke commit whatpoll()handed out, never the buffered batch’s fetch position.enable.auto.offset.storeis honoured (default true); see Migrating from confluent-kafka. client.idis ignored on the consumer; the producer maps it toclient_id.- A full queue raises
BufferError, as confluent-shaped code expects. - confluent’s
Producerhas noclose(), so theexitcell isflush().
Limits
Section titled “Limits”producer- offset commit, rebalance: the producer has no offsets and no group; the column belongs to the consumerconsumer- ordering: the consumer produces nothing; the column belongs to the producercompat.confluent- delivery: delivery callbacks fire onpoll()/flush(), as confluent’s do; the without-polling clause is native-only
Also pinned, outside the matrix
Section titled “Also pinned, outside the matrix”These need fault injection, so a broker test proves them rather than a matrix cell.
- Delivery: a retried batch under
enable_idempotence=Truedoes not duplicate; a batch whose fate is unknown at the deadline or when a finite retry budget runs out fails its receipt with aDeliveryTimeoutthat says so, the producer epoch bumps and later sends continue; a send that partly lands fails its receipt with aProduceErrorwhosereportcounts what landed and names the partitions that failed -tests/broker/test_idempotent_producer.py. That count is exact on every path:enable_idempotence=True,acks="all",acks=1,acks=0and inside a transaction. A produce that fails on some partitions retries those partitions only; records the broker already acknowledged are not sent again -tests/broker/test_produce_partial_failure.py. - Ordering: order holds after a retried/duplicate-sequence-number ack, not only across
concurrent in-flight batches -
tests/broker/test_idempotent_producer.py::test_idempotent_duplicate_sequence_number_acks_cleanly. - Exit: the same bound
flush()drains under (message_timeout_ms, within which a record is retried up tomessage_send_max_retriestimes) bounds the drain, and an undelivered record’s receipt fails (logged at ERROR if nobody holds it) -tests/broker/test_producer_close.py. - Transactions: after
abort_transaction()the producer is reusable and the next transaction commits cleanly -tests/broker/test_transactions.py::test_abort_then_begin_new_transaction,tests/broker/test_eos_edge_cases.py::test_txn_abort_does_not_wedge_next_transaction.
confluent-kafka behavior
Section titled “confluent-kafka behavior”This table compares pyrula-kafka with confluent-kafka-python 2.15.1 and names the contract that verifies each behavior.
| You are used to | Here | Proven by |
|---|---|---|
Records still queued when the producer is destroyed are dropped unless you flush() first | Leaving the context manager drains, under the same retry budget flush() uses; what cannot be delivered fails its receipt or is logged; purge() is the opt-out | producer - exit |
Delivery callbacks fire only when you call poll(0) or flush() | Receipts resolve on the broker’s ack without polling; the shim keeps confluent’s poll-driven callbacks | producer - delivery |
BufferError on a full queue, and it is on you to retry | The same refusal, as QueueFull natively and BufferError through the shim; nothing already accepted is dropped | producer - backpressure |
A batch that expires after it was sent (delivery.timeout.ms) fails its callback, librdkafka bumps the producer epoch and carries on | The same KIP-360 behaviour: the unknown-fate batch’s receipt fails with a DeliveryTimeout saying it may have landed, the epoch bumps, later sends continue on the same producer; the error’s report says what landed | producer - delivery |
Committing in on_revoke yourself when auto-commit is off, because only auto-commit commits stored offsets on revoke | Offsets staged with store_offsets are committed before the partition leaves, whether or not auto-commit is on; callbacks fire in order | consumer - rebalance |
Calling commit(), position() on the consumer from inside a rebalance callback | Parity: the callback receives the consumer it was registered on and runs with it released, so commit, position, committed, pause/resume, assignment, seek and the acknowledgement calls (assign/unassign eager, incremental_* cooperative-sticky) all work inside it, through the shim too | consumer / compat.confluent - rebalance |
assign() / unassign() / incremental_assign() / incremental_unassign() / seek(), standalone or inside a callback, with the OFFSET_BEGINNING/OFFSET_END/OFFSET_STORED/OFFSET_INVALID sentinels on the TopicPartitions you hand them | The same five calls, resolving the same sentinels; a seek(), assign() or unassign() also drops any record the shim had already read ahead of the old position, so nothing pre-seek is ever handed out and nothing commits below it, unless you staged a lower offset with store_offsets(), which stays in force across a seek as it does in confluent | consumer / compat.confluent - rebalance |
_MAX_POLL_EXCEEDED arriving as an error Message you must check msg.error() for | The same eviction, surfaced as a typed error from poll_batch instead of a message you inspect; through the shim, poll() hands it back as an error Message and consume() raises, with KafkaError._MAX_POLL_EXCEEDED as the wrapped error’s code() | consumer / compat.confluent - rebalance |
err.retriable() / err.txn_requires_abort() / err.fatal() to decide what to do with a transaction failure | The same three, on the native error and on the shim’s KafkaError: a failed transactional produce, partition registration or send_offsets_to_transaction leaves the transaction abortable, as librdkafka’s does, and commit_transaction then fails with that failure’s code, txn_requires_abort() true and neither fatal nor retriable, until you abort; a fenced producer is fatal. A client-side refusal is not abortable, as in confluent | producer - transactions |
A delivery callback’s msg.partition() / msg.offset(), or extra bookkeeping to learn where a record landed | The broker’s real partition and offset - the same values ProduceReport.deliveries[i] names for that record | producer - delivery |
close() commits when enable.auto.commit is on and not otherwise | Under auto-commit the same; with it off, offsets you staged with store_offsets are still committed at close(), a deliberate deviation in the safe direction - confluent would drop them | consumer - exit |
Not yet through the shim, or that behaves differently by default
Section titled “Not yet through the shim, or that behaves differently by default”These confluent-kafka surfaces are unavailable or have different defaults.
group.protocol=consumer(KIP-848).- The Protobuf Schema Registry serializer (
schema_registry.protobuf); the nativeProtobufRegistrySerdeexists.SerializingProducer/DeserializingConsumer, the Schema Registry client object and the Avro / JSON Schema serializers are through, with the limits the migration page lists.