Skip to main content

ruststream_rdkafka/
distribution.rs

1//! Producer-side distribution policies built on the explicit-partition header.
2//!
3//! librdkafka's partitioner families (random, consistent, murmur2, fnv1a) cannot express
4//! per-message round-robin, and keyless distribution may batch-stick to one partition. For
5//! workloads with long, near-constant per-message processing times that unevenness turns into
6//! one hot consumer and idle peers; [`RoundRobin`] stamps each outgoing reply with the next
7//! partition in the cycle instead.
8
9use std::sync::atomic::{AtomicU64, Ordering};
10
11use ruststream::runtime::{Outgoing, PublishContext, PublishTransform};
12
13use crate::message::{PARTITION_HEADER, PARTITION_KEY_HEADER};
14
15/// A [`PublishTransform`] distributing replies round-robin across the first `count` partitions.
16///
17/// Each reply gets the [`PARTITION_HEADER`] of an incrementing counter modulo `count`, so the
18/// publisher targets partitions 0..count in a cycle, one message each - the evenest possible
19/// spread for long, near-constant-cost messages. A reply that already carries an explicit
20/// partition or a record key is left alone: keys exist for ordering, and overriding either
21/// would silently break the caller's placement.
22///
23/// The count is explicit on purpose (cheap and predictable); it must match the destination
24/// topic's partition count, or the tail partitions simply receive nothing (a smaller count)
25/// or publishes fail (a larger one).
26///
27/// # Examples
28///
29/// ```
30/// use ruststream::runtime::TypedPublisher;
31/// use ruststream_rdkafka::{KafkaPublish, RoundRobin};
32///
33/// # #[cfg(feature = "json")]
34/// # fn wire() {
35/// let replies =
36///     TypedPublisher::new(KafkaPublish::default()).transform(RoundRobin::partitions(8));
37/// # let _ = replies;
38/// # }
39/// ```
40#[derive(Debug)]
41pub struct RoundRobin {
42    count: u64,
43    next: AtomicU64,
44}
45
46impl RoundRobin {
47    /// A round-robin cycle over partitions `0..count`.
48    ///
49    /// # Panics
50    ///
51    /// Panics when `count` is zero: a cycle over no partitions cannot place anything.
52    #[must_use]
53    pub fn partitions(count: i32) -> Self {
54        assert!(
55            count > 0,
56            "a round-robin cycle needs at least one partition"
57        );
58        Self {
59            #[allow(clippy::cast_sign_loss)] // asserted positive above
60            count: count as u64,
61            next: AtomicU64::new(0),
62        }
63    }
64}
65
66impl<C> PublishTransform<C> for RoundRobin {
67    fn apply(&self, out: &mut Outgoing<'_>, _cx: &PublishContext<'_, C>) {
68        if out.headers().get(PARTITION_HEADER).is_some()
69            || out.headers().get(PARTITION_KEY_HEADER).is_some()
70        {
71            return;
72        }
73        let slot = self.next.fetch_add(1, Ordering::Relaxed) % self.count;
74        out.headers_mut().insert(PARTITION_HEADER, slot.to_string());
75    }
76}