krabka_log/log/active_producers.rs
1//! The producer state that `DescribeProducers` reports and that log compaction
2//! keeps, and the expiry of the producers that stopped writing.
3//!
4//! Kafka keeps one producer state for each partition log. Every replica
5//! updates it for each batch that it appends: a leader for a client batch, and
6//! a follower for a batch that it replicates. `ReplicaManager
7//! .activeProducerState` answers `DescribeProducers` from that state on every
8//! replica that hosts the partition, through `Partition.activeProducerState`
9//! and `UnifiedLog.activeProducers`. The log cleaner of every replica reads the
10//! same state: `Cleaner.cleanSegments` takes `UnifiedLog
11//! .lastRecordsOfActiveProducers`. `ProducerStateManager
12//! .removeExpiredProducers` removes a producer when `producer.id.expiration.ms`
13//! has passed since its last write and it has no open transaction.
14
15use {
16 super::Log,
17 crate::compact::ProducerLastRecord,
18 krabka_ids::{Offset, ProducerId},
19 std::collections::HashMap,
20};
21
22/// One producer as `DescribeProducers` reports it.
23///
24/// This is Kafka's `DescribeProducersResponseData.ProducerState`, which
25/// `UnifiedLog.activeProducers` fills from a `ProducerStateEntry`.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct ActiveProducer {
28 pub producer_id: ProducerId,
29 pub producer_epoch: i16,
30 /// The last sequence of the last data batch of the producer at
31 /// `producer_epoch`, or `-1` when the log holds no such batch.
32 pub last_sequence: i32,
33 /// The max timestamp of the last data batch of the producer, or the
34 /// timestamp of the transaction marker that came after it.
35 pub last_timestamp: i64,
36 /// The coordinator epoch of the last transaction marker of the producer,
37 /// or `-1` before its first marker.
38 pub coordinator_epoch: i32,
39 /// The first offset of the open transaction of the producer, or `None`
40 /// when it has no open transaction.
41 pub current_txn_start_offset: Option<Offset>,
42}
43
44impl Log {
45 /// Every producer that this log holds state for, in producer id order.
46 ///
47 /// This is Kafka's `UnifiedLog.activeProducers`. An append from a client,
48 /// a replicated append and a reopen all update the same state, so a
49 /// leader and its followers give the same answer at the same log end.
50 #[must_use]
51 pub fn active_producers(&self) -> Vec<ActiveProducer> {
52 let mut producers: Vec<ActiveProducer> = self
53 .producer_state
54 .values()
55 .map(|entry| ActiveProducer {
56 producer_id: entry.producer_id,
57 producer_epoch: entry.producer_epoch,
58 last_sequence: entry.last_sequence,
59 last_timestamp: entry.timestamp,
60 coordinator_epoch: entry.coordinator_epoch,
61 current_txn_start_offset: entry.current_txn_first_offset,
62 })
63 .collect();
64 producers.sort_unstable_by_key(|producer| producer.producer_id);
65 producers
66 }
67
68 /// The last record of every producer that this log holds state for. A
69 /// compaction pass keeps that record, so that the producer state
70 /// survives the pass.
71 ///
72 /// This is Kafka's `UnifiedLog.lastRecordsOfActiveProducers`: the last
73 /// offset of the last data batch of the producer, and its epoch. A
74 /// producer has no last data offset when a transaction marker at a new
75 /// epoch has cleared its batches (transaction version 2), or when it has
76 /// written only markers. Every append path updates this state, so a
77 /// leader and a follower give the same answer at the same log end.
78 pub(crate) fn last_records_of_active_producers(
79 &self,
80 ) -> HashMap<ProducerId, ProducerLastRecord> {
81 self.producer_state
82 .values()
83 .map(|entry| {
84 let last_record = ProducerLastRecord {
85 last_data_offset: entry.last_batch().map(|batch| batch.last_offset),
86 producer_epoch: entry.producer_epoch,
87 };
88 (entry.producer_id, last_record)
89 })
90 .collect()
91 }
92
93 /// Remove every producer that has no open transaction and whose last
94 /// timestamp is `expiration_ms` or more before `now_ms`. Also remove the
95 /// transaction verification state that is that old.
96 ///
97 /// This is Kafka's `ProducerStateManager.removeExpiredProducers` with
98 /// `isProducerExpired`, for `producer.id.expiration.ms`. The next
99 /// producer-state snapshot does not hold a removed producer.
100 pub fn remove_expired_producers(&mut self, now_ms: i64, expiration_ms: i64) {
101 let expired: Vec<ProducerId> = self
102 .producer_state
103 .values()
104 .filter(|entry| {
105 entry.current_txn_first_offset.is_none()
106 && now_ms.saturating_sub(entry.timestamp) >= expiration_ms
107 })
108 .map(|entry| entry.producer_id)
109 .collect();
110 for producer_id in expired {
111 self.producer_state.remove(&producer_id);
112 self.earlier_batches.remove(&producer_id);
113 // Kafka keeps the coordinator epoch in the `ProducerStateEntry`,
114 // so it goes with the expired producer.
115 self.coordinator_epochs.remove(&producer_id);
116 }
117 self.verification_states
118 .retain(|_, state| now_ms.saturating_sub(state.created_ms) < expiration_ms);
119 }
120}
121
122#[cfg(test)]
123mod tests;