Skip to main content

kafko/
producer.rs

1use crate::error::{KafkoError, Result};
2use crate::position::RecordPosition;
3use crate::record::Record;
4use crate::topic::Topic;
5use bytes::Bytes;
6use std::sync::Arc;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9/// Cheap handle that appends records to a topic, routing each to a partition.
10///
11/// `Producer` is `Clone` (wraps `Arc<Topic>`); cloning is free and every clone
12/// writes to the same topic. Records are timestamped at `send`-time. Routing:
13/// a keyed record goes to `hash(key) % partition_count` (so same-key records
14/// keep their order); a keyless record is spread round-robin across partitions.
15#[derive(Clone)]
16pub struct Producer {
17    topic: Arc<Topic>,
18}
19
20impl Producer {
21    /// Wraps a [`Topic`] in a `Producer`. Prefer [`Kafko::producer_for`] which
22    /// looks the topic up by name.
23    ///
24    /// [`Kafko::producer_for`]: crate::Kafko::producer_for
25    pub fn new(topic: Arc<Topic>) -> Self {
26        Self { topic }
27    }
28
29    /// Number of partitions on the topic this producer writes to.
30    pub fn partition_count(&self) -> u32 {
31        self.topic.partition_count()
32    }
33
34    /// Appends a record, routing by `key`, and returns its [`RecordPosition`]
35    /// (the partition it landed on plus its offset within that partition).
36    ///
37    /// Resolves once the bytes are in the OS file (page cache) — the same
38    /// durability contract as Kafka `acks=1`.
39    ///
40    /// # Example
41    ///
42    /// ```no_run
43    /// use bytes::Bytes;
44    /// use kafko::Kafko;
45    ///
46    /// # async fn run() -> kafko::Result<()> {
47    /// let broker = Kafko::open("./data").await?;
48    /// broker.create_topic("orders").await?;
49    /// let producer = broker.producer_for("orders").await?;
50    ///
51    /// let pos = producer.send(Some(Bytes::from("cust-1")), Bytes::from("order-1")).await?;
52    /// println!("partition {} offset {}", pos.partition(), pos.offset());
53    /// # broker.shutdown().await?;
54    /// # Ok(())
55    /// # }
56    /// ```
57    #[cfg_attr(feature = "hotpath", hotpath::measure)]
58    pub async fn send(&self, key: Option<Bytes>, value: Bytes) -> Result<RecordPosition> {
59        let partition = self.route(key.as_deref());
60        let record = Record::new(current_timestamp_ms(), key, value);
61        self.append_to(partition, record).await
62    }
63
64    /// Appends a record to an explicit `partition`, ignoring key-based routing.
65    /// Errors with [`KafkoError::InvalidPartitionCount`] if `partition` is out
66    /// of range for the topic.
67    #[cfg_attr(feature = "hotpath", hotpath::measure)]
68    pub async fn send_to(
69        &self,
70        partition: u32,
71        key: Option<Bytes>,
72        value: Bytes,
73    ) -> Result<RecordPosition> {
74        let record = Record::new(current_timestamp_ms(), key, value);
75        self.append_to(partition, record).await
76    }
77
78    /// Appends an already-constructed [`Record`] (preserving its timestamp),
79    /// routing by the record's own key, and returns its [`RecordPosition`].
80    #[cfg_attr(feature = "hotpath", hotpath::measure)]
81    pub async fn send_record(&self, record: Record) -> Result<RecordPosition> {
82        let partition = self.route(record.key().map(|k| k.as_ref()));
83        self.append_to(partition, record).await
84    }
85
86    /// Appends a batch of records, timestamping each at the moment of the call,
87    /// and returns their [`RecordPosition`]s in input order.
88    ///
89    /// Records are grouped by their routed partition and each group is written
90    /// as one atomic append. **Atomicity is per partition**: a batch whose
91    /// records span partitions is atomic within each partition, not across them.
92    /// For a single-partition topic this is one fully-atomic append, identical
93    /// to a single `Log::append_batch`.
94    ///
95    /// An empty input is a no-op returning `Ok(Vec::new())`.
96    #[cfg_attr(feature = "hotpath", hotpath::measure)]
97    pub async fn send_batch(
98        &self,
99        items: Vec<(Option<Bytes>, Bytes)>,
100    ) -> Result<Vec<RecordPosition>> {
101        if items.is_empty() {
102            return Ok(Vec::new());
103        }
104        let timestamp_ms = current_timestamp_ms();
105        let records = items
106            .into_iter()
107            .map(|(key, value)| Record::new(timestamp_ms, key, value));
108        self.append_batch_routed(records).await
109    }
110
111    /// Like [`send_batch`], but takes already-constructed records so callers can
112    /// preserve per-record timestamps. Each record routes by its own key; the
113    /// per-partition atomicity contract is identical.
114    ///
115    /// [`send_batch`]: Producer::send_batch
116    #[cfg_attr(feature = "hotpath", hotpath::measure)]
117    pub async fn send_batch_records(&self, records: Vec<Record>) -> Result<Vec<RecordPosition>> {
118        if records.is_empty() {
119            return Ok(Vec::new());
120        }
121        self.append_batch_routed(records.into_iter()).await
122    }
123
124    fn route(&self, key: Option<&[u8]>) -> u32 {
125        match key {
126            Some(k) => self.topic.partition_for_key(k),
127            None => self.topic.next_round_robin(),
128        }
129    }
130
131    async fn append_to(&self, partition: u32, record: Record) -> Result<RecordPosition> {
132        let target = self
133            .topic
134            .partition(partition)
135            .ok_or(KafkoError::InvalidPartitionCount(partition))?;
136        let offset = target.append(record).await?;
137        Ok(RecordPosition::new(partition, offset))
138    }
139
140    async fn append_batch_routed(
141        &self,
142        records: impl Iterator<Item = Record>,
143    ) -> Result<Vec<RecordPosition>> {
144        let n = self.topic.partition_count() as usize;
145        // Bucket records by target partition, remembering each record's original
146        // input index so we can scatter the assigned offsets back in order.
147        let mut buckets: Vec<Vec<(usize, Record)>> = vec![Vec::new(); n];
148        let mut total = 0usize;
149        for (idx, record) in records.enumerate() {
150            let p = self.route(record.key().map(|k| k.as_ref())) as usize;
151            buckets[p].push((idx, record));
152            total += 1;
153        }
154
155        let mut positions = vec![RecordPosition::new(0, 0); total];
156        for (p, bucket) in buckets.into_iter().enumerate() {
157            if bucket.is_empty() {
158                continue;
159            }
160            let (indices, recs): (Vec<usize>, Vec<Record>) = bucket.into_iter().unzip();
161            let partition = self
162                .topic
163                .partition(p as u32)
164                .expect("bucket index is always a valid partition");
165            let offsets = partition.append_batch(recs).await?;
166            for (input_idx, offset) in indices.into_iter().zip(offsets) {
167                positions[input_idx] = RecordPosition::new(p as u32, offset);
168            }
169        }
170        Ok(positions)
171    }
172}
173
174fn current_timestamp_ms() -> i64 {
175    SystemTime::now()
176        .duration_since(UNIX_EPOCH)
177        .map(|d| d.as_millis() as i64)
178        .unwrap_or(0)
179}