crafty_core/
compaction.rs1use crafty_proto::{EntryPayload, LogEntry, LogIndex};
7
8use crate::RaftNode;
9
10pub const DEFAULT_COMPACT_ENTRIES: u64 = 1024;
12
13pub const DEFAULT_COMPACT_BYTES: u64 = 4 * 1024 * 1024;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct CompactionPolicy {
19 pub max_entries: Option<u64>,
22 pub max_bytes: Option<u64>,
25}
26
27impl CompactionPolicy {
28 #[must_use]
30 pub fn disabled() -> Self {
31 Self {
32 max_entries: None,
33 max_bytes: None,
34 }
35 }
36
37 #[must_use]
39 pub fn default_auto() -> Self {
40 Self {
41 max_entries: Some(DEFAULT_COMPACT_ENTRIES),
42 max_bytes: Some(DEFAULT_COMPACT_BYTES),
43 }
44 }
45
46 #[must_use]
48 pub fn entries(entries: u64) -> Self {
49 Self {
50 max_entries: Some(entries),
51 max_bytes: None,
52 }
53 }
54
55 #[must_use]
57 pub fn is_disabled(&self) -> bool {
58 self.max_entries.is_none() && self.max_bytes.is_none()
59 }
60}
61
62impl Default for CompactionPolicy {
63 fn default() -> Self {
64 Self::default_auto()
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct CompactionStats {
71 pub snapshot_index: LogIndex,
73 pub last_applied: LogIndex,
75 pub compactable_entries: u64,
77 pub compactable_bytes: u64,
79}
80
81#[must_use]
83pub fn compaction_stats(node: &RaftNode) -> CompactionStats {
84 CompactionStats {
85 snapshot_index: node.snapshot_index(),
86 last_applied: node.last_applied(),
87 compactable_entries: node.compactable_entries(),
88 compactable_bytes: node.compactable_log_bytes(),
89 }
90}
91
92#[must_use]
94pub fn should_compact(policy: &CompactionPolicy, stats: &CompactionStats) -> bool {
95 if policy.is_disabled() || stats.compactable_entries == 0 {
96 return false;
97 }
98 if policy
99 .max_entries
100 .is_some_and(|limit| stats.compactable_entries >= limit)
101 {
102 return true;
103 }
104 policy
105 .max_bytes
106 .is_some_and(|limit| stats.compactable_bytes >= limit)
107}
108
109#[must_use]
111pub fn entry_estimated_bytes(entry: &LogEntry) -> u64 {
112 const FIXED: u64 = 16; FIXED
114 + match &entry.payload {
115 EntryPayload::Noop => 1,
116 EntryPayload::Command(bytes) => bytes.len() as u64,
117 EntryPayload::Membership(m) => {
118 ((m.voters.len() + m.voters_outgoing.len() + m.learners.len()) * 8) as u64 + 8
119 }
120 EntryPayload::Catalog(c) => match c {
121 crafty_proto::CatalogCommand::AddGroups { new_groups, .. } => {
122 new_groups.len() as u64 * 4 + 8
123 }
124 },
125 EntryPayload::SagaJournal(c) => c.record.len() as u64 + 32,
126 EntryPayload::TwoPhasePrepare(c) => {
127 c.tx_id.len() as u64 + c.route_key.len() as u64 + c.command.len() as u64 + 32
128 }
129 EntryPayload::TwoPhaseAbort(c) => c.tx_id.len() as u64 + c.route_key.len() as u64 + 32,
130 EntryPayload::TwoPhaseJournal(c) => c.record.len() as u64 + 32,
131 EntryPayload::QueueAutoscalePolicy(c) => {
132 c.stream.len() as u64
133 + c.worker
134 .as_ref()
135 .map_or(0, |w| w.worker_group.len() as u64 + 32)
136 + c.membership.as_ref().map_or(0, |_| 32)
137 + 16
138 }
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use crate::Config;
146 use crafty_proto::NodeId;
147
148 fn node(id: u64, members: &[u64]) -> RaftNode {
149 RaftNode::new(
150 NodeId(id),
151 members.iter().copied().map(NodeId),
152 Config::default(),
153 )
154 }
155
156 #[test]
157 fn disabled_policy_never_triggers() {
158 let n = node(1, &[1]);
159 let stats = compaction_stats(&n);
160 assert!(!should_compact(&CompactionPolicy::disabled(), &stats));
161 }
162
163 #[test]
164 fn entry_threshold_triggers() {
165 let policy = CompactionPolicy::entries(3);
166 assert!(should_compact(
167 &policy,
168 &CompactionStats {
169 snapshot_index: LogIndex(0),
170 last_applied: LogIndex(3),
171 compactable_entries: 3,
172 compactable_bytes: 0,
173 }
174 ));
175 assert!(!should_compact(
176 &policy,
177 &CompactionStats {
178 snapshot_index: LogIndex(0),
179 last_applied: LogIndex(2),
180 compactable_entries: 2,
181 compactable_bytes: 0,
182 }
183 ));
184 }
185
186 #[test]
187 fn byte_threshold_triggers() {
188 let policy = CompactionPolicy {
189 max_entries: None,
190 max_bytes: Some(100),
191 };
192 assert!(should_compact(
193 &policy,
194 &CompactionStats {
195 snapshot_index: LogIndex(1),
196 last_applied: LogIndex(2),
197 compactable_entries: 1,
198 compactable_bytes: 100,
199 }
200 ));
201 }
202
203 #[test]
204 fn zero_compactable_never_triggers() {
205 let mut n = node(1, &[1]);
206 n.campaign();
207 let _ = n.take_outputs();
208 let stats = compaction_stats(&n);
209 assert_eq!(stats.compactable_entries, 1); assert!(!should_compact(&CompactionPolicy::entries(1024), &stats));
211 }
212}