1use std::{collections::BTreeMap, fmt, sync::Arc};
2
3use crate::{MqError, MqResult};
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct TopicPartition {
8 pub topic: String,
9 pub partition: i32,
10}
11
12impl TopicPartition {
13 #[must_use]
14 pub fn new(topic: impl Into<String>, partition: i32) -> Self {
15 Self {
16 topic: topic.into(),
17 partition,
18 }
19 }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct OffsetCommit {
25 pub topic: String,
26 pub partition: i32,
27 pub offset: i64,
28}
29
30impl OffsetCommit {
31 #[must_use]
32 pub fn topic_partition(&self) -> TopicPartition {
33 TopicPartition::new(self.topic.clone(), self.partition)
34 }
35}
36
37#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub(crate) struct OffsetPosition {
39 pub(crate) topic: String,
40 pub(crate) partition: i32,
41 pub(crate) offset: i64,
42}
43
44impl OffsetPosition {
45 pub(crate) fn new(topic: impl Into<String>, partition: i32, offset: i64) -> Self {
46 Self {
47 topic: topic.into(),
48 partition,
49 offset,
50 }
51 }
52
53 pub(crate) fn topic_partition(&self) -> TopicPartition {
54 TopicPartition::new(self.topic.clone(), self.partition)
55 }
56}
57
58pub(crate) trait OffsetCommitter: Send + Sync {
59 fn stage_offset(&self, position: OffsetPosition) -> MqResult<()>;
60 fn commit_offset(&self, position: OffsetPosition) -> MqResult<()>;
61}
62
63#[derive(Clone)]
65pub struct KafkaOffset {
66 position: OffsetPosition,
67 committer: Option<Arc<dyn OffsetCommitter>>,
68}
69
70impl KafkaOffset {
71 pub(crate) fn new(position: OffsetPosition, committer: Arc<dyn OffsetCommitter>) -> Self {
72 Self {
73 position,
74 committer: Some(committer),
75 }
76 }
77
78 #[must_use]
79 pub fn detached(topic: impl Into<String>, partition: i32, offset: i64) -> Self {
80 Self {
81 position: OffsetPosition::new(topic, partition, offset),
82 committer: None,
83 }
84 }
85
86 #[must_use]
87 pub fn topic(&self) -> &str {
88 &self.position.topic
89 }
90
91 #[must_use]
92 pub fn partition(&self) -> i32 {
93 self.position.partition
94 }
95
96 #[must_use]
97 pub fn offset(&self) -> i64 {
98 self.position.offset
99 }
100
101 #[must_use]
102 pub fn next_offset(&self) -> i64 {
103 self.position.offset + 1
104 }
105
106 #[must_use]
107 pub fn topic_partition(&self) -> TopicPartition {
108 self.position.topic_partition()
109 }
110
111 pub fn commit(&self) -> MqResult<()> {
114 let Some(committer) = &self.committer else {
115 return Err(MqError::ControlUnavailable);
116 };
117 committer.commit_offset(self.position.clone())
118 }
119
120 pub fn mark_processed(&self) -> MqResult<()> {
126 let Some(committer) = &self.committer else {
127 return Err(MqError::ControlUnavailable);
128 };
129 committer.stage_offset(self.position.clone())
130 }
131}
132
133impl fmt::Debug for KafkaOffset {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 f.debug_struct("KafkaOffset")
136 .field("topic", &self.position.topic)
137 .field("partition", &self.position.partition)
138 .field("offset", &self.position.offset)
139 .finish_non_exhaustive()
140 }
141}
142
143#[derive(Debug, Clone, Default)]
145pub struct KafkaOffsetBatch {
146 offsets: Vec<KafkaOffset>,
147}
148
149impl KafkaOffsetBatch {
150 #[must_use]
151 pub fn new() -> Self {
152 Self::default()
153 }
154
155 #[must_use]
156 pub fn from_offsets<I>(offsets: I) -> Self
157 where
158 I: IntoIterator<Item = KafkaOffset>,
159 {
160 Self {
161 offsets: offsets.into_iter().collect(),
162 }
163 }
164
165 pub fn push(&mut self, offset: KafkaOffset) {
166 self.offsets.push(offset);
167 }
168
169 #[must_use]
170 pub fn len(&self) -> usize {
171 self.offsets.len()
172 }
173
174 #[must_use]
175 pub fn is_empty(&self) -> bool {
176 self.offsets.is_empty()
177 }
178
179 pub fn commit(&self) -> MqResult<()> {
180 let mut by_partition = BTreeMap::<TopicPartition, &KafkaOffset>::new();
181 for offset in &self.offsets {
182 by_partition
183 .entry(offset.topic_partition())
184 .and_modify(|current| {
185 if offset.offset() > current.offset() {
186 *current = offset;
187 }
188 })
189 .or_insert(offset);
190 }
191 for offset in by_partition.values() {
192 offset.commit()?;
193 }
194 Ok(())
195 }
196}
197
198#[derive(Clone)]
205pub struct KafkaBatchOffset {
206 positions: Vec<OffsetPosition>,
207 committer: KafkaBatchCommitter,
208}
209
210#[derive(Clone)]
211enum KafkaBatchCommitter {
212 Offset(Option<Arc<dyn OffsetCommitter>>),
213 Custom(Arc<dyn Fn() -> MqResult<()> + Send + Sync>),
214}
215
216impl KafkaBatchOffset {
217 pub(crate) fn custom<F>(positions: Vec<OffsetPosition>, commit: F) -> Self
218 where
219 F: Fn() -> MqResult<()> + Send + Sync + 'static,
220 {
221 Self {
222 positions,
223 committer: KafkaBatchCommitter::Custom(Arc::new(commit)),
224 }
225 }
226
227 #[must_use]
228 pub fn detached<I>(positions: I) -> Self
229 where
230 I: IntoIterator<Item = OffsetCommit>,
231 {
232 Self {
233 positions: positions
234 .into_iter()
235 .map(|position| {
236 OffsetPosition::new(position.topic, position.partition, position.offset - 1)
237 })
238 .collect(),
239 committer: KafkaBatchCommitter::Offset(None),
240 }
241 }
242
243 #[must_use]
244 pub fn len(&self) -> usize {
245 self.positions.len()
246 }
247
248 #[must_use]
249 pub fn is_empty(&self) -> bool {
250 self.positions.is_empty()
251 }
252
253 pub fn commit(&self) -> MqResult<()> {
256 match &self.committer {
257 KafkaBatchCommitter::Offset(Some(committer)) => {
258 for position in &self.positions {
259 committer.commit_offset(position.clone())?;
260 }
261 Ok(())
262 }
263 KafkaBatchCommitter::Offset(None) => Err(MqError::ControlUnavailable),
264 KafkaBatchCommitter::Custom(commit) => commit(),
265 }
266 }
267}
268
269impl fmt::Debug for KafkaBatchOffset {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 f.debug_struct("KafkaBatchOffset")
272 .field("positions", &self.positions)
273 .finish_non_exhaustive()
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use std::sync::Mutex;
281
282 fn pos(offset: i64) -> OffsetPosition {
283 OffsetPosition::new("topic", 0, offset)
284 }
285
286 #[derive(Debug, Default)]
287 struct FakeCommitter {
288 staged: Mutex<Vec<OffsetPosition>>,
289 committed: Mutex<Vec<OffsetPosition>>,
290 }
291
292 impl OffsetCommitter for FakeCommitter {
293 fn stage_offset(&self, position: OffsetPosition) -> MqResult<()> {
294 self.staged.lock().expect("staged lock").push(position);
295 Ok(())
296 }
297
298 fn commit_offset(&self, position: OffsetPosition) -> MqResult<()> {
299 self.committed
300 .lock()
301 .expect("committed lock")
302 .push(position);
303 Ok(())
304 }
305 }
306
307 #[test]
308 fn offset_batch_commits_last_offset_per_partition() {
309 let committer = Arc::new(FakeCommitter::default());
310 let offsets = [
311 KafkaOffset::new(pos(0), committer.clone()),
312 KafkaOffset::new(OffsetPosition::new("topic", 1, 0), committer.clone()),
313 KafkaOffset::new(pos(1), committer.clone()),
314 KafkaOffset::new(OffsetPosition::new("topic", 1, 1), committer.clone()),
315 ];
316
317 KafkaOffsetBatch::from_offsets(offsets)
318 .commit()
319 .expect("batch commits");
320
321 let staged = committer.staged.lock().expect("staged lock");
322 let committed = committer.committed.lock().expect("committed lock");
323 assert_eq!(staged.len(), 0);
324 assert_eq!(committed.len(), 2);
325 assert!(committed.iter().all(|position| position.offset == 1));
326 }
327}