Skip to main content

kafka_produce/
lib.rs

1//! The write path: encode a record batch, route it to the partition leader,
2//! and report where it landed.
3//!
4//! This and `kafka-consume` are what make kaas-lib a general-purpose client
5//! rather than an admin one. The read path answers "show me what is in this
6//! partition"; this one answers "put this there, and tell me the truth about
7//! whether it worked".
8//!
9//! ```no_run
10//! # async fn example(cluster: kafka_meta::Cluster) -> kafka_produce::Result<()> {
11//! use kafka_produce::{Producer, ProducerConfig, ProducerRecord};
12//!
13//! let producer = Producer::new(cluster, ProducerConfig::new());
14//! let meta = producer
15//!     .send(
16//!         ProducerRecord::new("orders")
17//!             .with_key("customer-7")
18//!             .with_value("{\"total\":42}")
19//!             .with_header("content-type", "application/json"),
20//!     )
21//!     .await?;
22//! println!("landed at {}:{}", meta.partition, meta.offset);
23//! # Ok(())
24//! # }
25//! ```
26//!
27//! # `acks=0` is not offered, and that is a decision rather than an omission
28//!
29//! PLAN.md M12 requires this to be settled before any encoder code exists,
30//! because the failure mode is silent: `acks=0` is a request the broker sends
31//! **no response to at all**. [`kafka_conn::Connection`] correlates every
32//! in-flight request on a `HashMap<i32, oneshot::Sender<_>>`, so an `acks=0`
33//! produce would register a waiter nothing ever resolves — and every
34//! *successful* write would surface to the caller as a timeout.
35//!
36//! The two ways out were a fire-and-forget path on the connection that drops
37//! the correlation entry at send time, or refusing the mode. This crate
38//! refuses it, and refuses it at the type level: [`Acks`] has no `None`
39//! variant, so the unsupported state cannot be constructed rather than being
40//! constructed and rejected. Three reasons, in order of weight:
41//!
42//! 1. A second send path punches a hole in the connection actor's invariant
43//!    that every in-flight request has a waiter, and would have to be held to
44//!    rule 5 (cancel safety) independently and forever.
45//! 2. `acks=0` gives the caller no delivery signal whatsoever. A library whose
46//!    stated contract is that partial failure is a *result* should not ship a
47//!    mode whose entire character is discarding results.
48//! 3. It is incompatible with idempotence (M14), which needs the response to
49//!    advance its per-partition sequence numbers. Offering the mode now would
50//!    mean withdrawing it there.
51//!
52//! What the mode actually buys — not waiting on the leader — is what the
53//! accumulator in M13 provides safely, by batching rather than by throwing the
54//! acknowledgement away.
55//!
56//! # Batching, and how to get it
57//!
58//! Records are buffered per partition and sent together. [`Producer::send`]
59//! accepts one record and waits for it, which means a loop of `send().await`
60//! keeps exactly one record in flight and batches nothing. To get the
61//! throughput, use [`Producer::enqueue`], which returns as soon as the record
62//! is buffered, and await the [`Delivery`] handles together:
63//!
64//! ```no_run
65//! # async fn example(producer: &kafka_produce::Producer) -> kafka_produce::Result<()> {
66//! # use kafka_produce::ProducerRecord;
67//! let mut pending = Vec::new();
68//! for i in 0..10_000 {
69//!     pending.push(producer.enqueue(ProducerRecord::new("t").with_value(format!("{i}"))).await?);
70//! }
71//! for delivery in pending {
72//!     delivery.await?;
73//! }
74//! # Ok(())
75//! # }
76//! ```
77//!
78//! `linger` defaults to zero and that is not a reason to raise it: a partition
79//! holds one batch on the wire at a time, so records arriving during a round
80//! trip accumulate into the next batch on their own. Batching scales with load
81//! rather than with the setting.
82//!
83//! # Idempotence, and what it changes
84//!
85//! On by default. The producer claims a producer id and numbers every record,
86//! so the broker recognises a re-sent batch and answers with the original
87//! offsets instead of appending it twice.
88//!
89//! That is what makes an **ambiguous** failure retriable. Without it, a
90//! timeout or a connection that died in flight can never be re-sent — the
91//! records may already be in the log — so an ordinary leader election surfaces
92//! to the caller as a delivery failure. With it, the producer rides the
93//! election out. [`ProducerConfig::idempotent`] turns it off for brokers that
94//! cannot issue a producer id; it does not make the producer faster, it makes
95//! it lossier.
96//!
97//! At most one batch per partition is on the wire regardless, so ordering does
98//! not depend on [`Producer::max_in_flight`]. That clamp — one without
99//! idempotence, five with it — is defence for the connection layer rather than
100//! the mechanism that keeps the log in order.
101//!
102//! # What this milestone is, and is not
103//!
104//! M13 is batching, bounded buffer memory and per-record delivery futures.
105//! M14 is idempotence. Transactions are M15: there is no `transactional_id`
106//! here, no `AddPartitionsToTxn`, and `kafka-read`'s
107//! `Visibility::CommittedOnly` still has nothing in this workspace that can
108//! produce an aborted transaction to test it against.
109
110#![cfg_attr(
111    test,
112    allow(
113        clippy::unwrap_used,
114        clippy::expect_used,
115        clippy::panic,
116        clippy::indexing_slicing
117    )
118)]
119
120mod accumulator;
121mod config;
122mod dispatch;
123mod encode;
124mod idempotence;
125mod partition;
126mod producer;
127mod record;
128mod transactions;
129
130pub use config::{Acks, Compression, ProducerConfig};
131pub use partition::{Partitioner, murmur2, partition_for_key};
132pub use producer::{Delivery, Producer};
133pub use record::{ProducerRecord, RecordMetadata};
134
135/// Encode records into one v2 batch, for the round-trip fuzz target.
136///
137/// Not part of the supported surface: the encoder is an internal detail of the
138/// producer, and the only reason it is reachable at all is that M19 fuzzes what
139/// this crate *writes* through the decoder in `kafka-read` that has to read it
140/// back. A mutual misreading of the spec by both halves round-trips cleanly and
141/// is still wrong on the wire, so this exists to catch the narrower thing: the
142/// pair disagreeing with itself.
143#[doc(hidden)]
144pub fn encode_for_fuzzing(
145    records: &[ProducerRecord],
146    compression: Compression,
147) -> Result<bytes::Bytes> {
148    encode::encode_batch(records, compression, 0, None)
149}
150
151pub use kafka_conn::{Error, Result};
152pub use kafka_meta::{Cluster, ClusterConfig};