1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! Producer-side distribution policies built on the per-record partition setting.
//!
//! librdkafka's partitioner families (random, consistent, murmur2, fnv1a) cannot express
//! per-message round-robin, and keyless distribution may batch-stick to one partition. For
//! workloads with long, near-constant per-message processing times that unevenness turns into
//! one hot consumer and idle peers; [`RoundRobin`] pins each outgoing record to the next
//! partition in the cycle instead.
use ;
use ;
use cratePARTITION_KEY_HEADER;
use crateKafkaOptions;
/// A [`PublishTransform`] distributing records round-robin across the first `count` partitions.
///
/// Each record is pinned to an incrementing counter modulo `count`, so the publisher targets
/// partitions 0..count in a cycle, one message each - the evenest possible spread for long,
/// near-constant-cost messages. A record that already names a partition or carries a record key
/// is left alone: keys exist for ordering, and overriding either would silently break the
/// caller's placement.
///
/// The cycle writes the same per-record setting the builder's
/// [`partition`](crate::KafkaPublishSteps::partition) step writes, so it mounts only over a
/// publisher whose [`Publisher::Options`](ruststream::Publisher::Options) are [`KafkaOptions`].
/// On an `Out` slot that setting arrives carrying whatever the call site chose, and the cycle
/// steps aside for it; a reply has no call site, so there the cycle always places.
///
/// The count is explicit on purpose (cheap and predictable); it must match the destination
/// topic's partition count, or the tail partitions simply receive nothing (a smaller count)
/// or publishes fail (a larger one).
///
/// # Examples
///
/// The transform is a step of the mount site's chain, right after the publish policy:
///
/// ```
/// # #[cfg(feature = "json")]
/// # mod demo {
/// use ruststream_rdkafka::prelude::*;
/// # #[derive(serde::Deserialize)]
/// # struct Order { id: u64 }
/// # #[derive(serde::Serialize, Outgoing)]
/// # #[outgoing(name = "work-items")]
/// # struct WorkItem { order_id: u64 }
///
/// #[subscriber("orders", reply)]
/// async fn plan(order: &Order) -> WorkItem {
/// WorkItem { order_id: order.id }
/// }
///
/// fn app() -> RustStream {
/// RustStream::new(AppInfo::new("planner", "0.1.0"))
/// .with_broker(KafkaBroker::new(["localhost:9092"]), |b| {
/// b.include(plan)
/// .out_reply(Publish::default())
/// .transform(RoundRobin::partitions(8));
/// })
/// }
/// # }
/// ```