reifydb_flow/operator/state/reaper/
mod.rs1use std::{collections::HashMap, ops::Bound};
5
6use reifydb_codec::row::pod::EncodedPodRow;
7use reifydb_core::{
8 key::{
9 operator::{
10 keyspace::expiry::{ReapQueue, ReapQueueKey},
11 state::{GroupId, GroupStateKey, OperatorStateKey},
12 },
13 typed::direction::Desc,
14 },
15 state::{
16 timer::{StateStore, sweep_order},
17 typed::{TypedStateStore, typed_key},
18 },
19};
20use reifydb_value::{Result, reifydb_assertions};
21
22use crate::operator::state::reclaim::ReclaimOutcome;
23
24#[cfg(test)]
25mod tests;
26
27pub trait Reaper {
28 fn reap(&mut self, store: &mut dyn StateStore, key: &GroupStateKey) -> Result<()>;
29}
30
31pub trait IdentityReclaim: StateStore {
32 fn reclaim_identity(&mut self, group: GroupId, limit: usize) -> Result<ReclaimOutcome>;
33
34 fn reclaim_identity_keys(&mut self, group: GroupId, keys: &[GroupStateKey]) -> Result<ReclaimOutcome>;
35}
36
37pub struct StoreReaper;
38
39impl Reaper for StoreReaper {
40 fn reap(&mut self, store: &mut dyn StateStore, key: &GroupStateKey) -> Result<()> {
41 store.state_remove(key)
42 }
43}
44
45pub fn queue_key(group: GroupId) -> GroupStateKey {
46 typed_key::<ReapQueue>(
47 GroupId::ROOT,
48 &ReapQueueKey {
49 group: Desc(group),
50 },
51 )
52}
53
54pub fn enqueue(store: &mut dyn StateStore, group: GroupId) -> Result<()> {
55 store.state_set(&queue_key(group), EncodedPodRow::new(&[]))
56}
57
58pub struct Queued {
59 pub groups: Vec<GroupId>,
60 pub more: bool,
61}
62
63pub struct DrainOutcome {
64 pub freed: usize,
65 pub still_queued: Vec<GroupId>,
66 pub more: bool,
67}
68
69impl DrainOutcome {
70 pub fn queue_is_empty(&self) -> bool {
71 self.still_queued.is_empty() && !self.more
72 }
73}
74
75pub fn queued(store: &mut dyn StateStore, limit: usize) -> Result<Queued> {
76 let page = store.state_scan_in::<ReapQueue>(GroupId::ROOT, Bound::Unbounded, Some(limit.saturating_add(1)))?;
77 let more = page.len() > limit;
78 let groups = page.into_iter().take(limit).map(|(suffix, _)| suffix.group.0).collect();
79 Ok(Queued {
80 groups,
81 more,
82 })
83}
84
85pub struct GroupDrain {
86 pub freed: usize,
87 pub still_queued: bool,
88}
89
90#[derive(Default)]
91struct GroupScan {
92 identity: Vec<GroupStateKey>,
93 data: Vec<(GroupStateKey, EncodedPodRow)>,
94}
95
96fn bucket(rows: Vec<(GroupStateKey, EncodedPodRow)>) -> (HashMap<GroupId, GroupScan>, Option<GroupId>) {
97 let mut buckets: HashMap<GroupId, GroupScan> = HashMap::new();
98 let mut last = None;
99 for (key, row) in rows {
100 let Some((group, keyspace, _)) = OperatorStateKey::decode_inner(key.as_encoded().as_bytes()) else {
101 continue;
102 };
103 last = Some(group);
104 let bucket = buckets.entry(group).or_default();
105 match keyspace.is_data() {
106 true => bucket.data.push((key, row)),
107 false => bucket.identity.push(key),
108 }
109 }
110 (buckets, last)
111}
112
113fn scan_group(store: &mut dyn StateStore, group: GroupId, budget: usize) -> Result<Option<GroupScan>> {
114 let mut identity = Vec::new();
115 let mut data = Vec::new();
116 let swept = store.group_sweep(group, false, Some(budget.saturating_add(1)))?;
117 if swept.len() > budget {
118 return Ok(None);
119 }
120 for (key, row) in swept {
121 match OperatorStateKey::decode_inner(key.as_encoded().as_bytes()) {
122 Some((_, keyspace, _)) if keyspace.is_data() => data.push((key, row)),
123 Some(_) => identity.push(key),
124 None => {}
125 }
126 }
127 Ok(Some(GroupScan {
128 identity,
129 data,
130 }))
131}
132
133pub fn drain_group<R>(
134 store: &mut dyn IdentityReclaim,
135 group: GroupId,
136 reaper: &mut R,
137 budget: usize,
138) -> Result<GroupDrain>
139where
140 R: Reaper,
141{
142 let Some(scan) = scan_group(store, group, budget)? else {
143 return drain_group_scanning(store, group, reaper, budget);
144 };
145 Ok(GroupDrain {
146 freed: reap_scanned(store, group, scan, reaper)?,
147 still_queued: false,
148 })
149}
150
151fn reap_scanned<R>(store: &mut dyn IdentityReclaim, group: GroupId, scan: GroupScan, reaper: &mut R) -> Result<usize>
152where
153 R: Reaper,
154{
155 store.remove_root_siblings(&scan.data)?;
156 for (key, _) in &scan.data {
157 reaper.reap(store, key)?;
158 }
159 reifydb_assertions! {
160 let leftover = store.group_sweep(group, true, None)?.len();
161 assert!(
162 leftover == 0,
163 "group {} still holds {leftover} data rows in its own partition; forgetting its dictionary \
164 entry now would orphan them behind a group id nothing can resolve again",
165 group
166 );
167 }
168 let freed = scan.data.len();
169 let outcome = store.reclaim_identity_keys(group, &scan.identity)?;
170 store.state_remove(&queue_key(group))?;
171 Ok(freed + outcome.removed.as_u64() as usize)
172}
173
174fn drain_group_scanning<R>(
175 store: &mut dyn IdentityReclaim,
176 group: GroupId,
177 reaper: &mut R,
178 budget: usize,
179) -> Result<GroupDrain>
180where
181 R: Reaper,
182{
183 let freed = reap_group(store, group, reaper, budget)?;
184 if freed >= budget {
185 return Ok(GroupDrain {
186 freed,
187 still_queued: true,
188 });
189 }
190 let outcome = store.reclaim_identity(group, budget - freed)?;
191 let freed = freed + outcome.removed.as_u64() as usize;
192 if outcome.more {
193 return Ok(GroupDrain {
194 freed,
195 still_queued: true,
196 });
197 }
198 store.state_remove(&queue_key(group))?;
199 Ok(GroupDrain {
200 freed,
201 still_queued: false,
202 })
203}
204
205pub fn drain<R>(store: &mut dyn IdentityReclaim, reaper: &mut R, budget: usize) -> Result<DrainOutcome>
206where
207 R: Reaper,
208{
209 let scan = queued(store, budget)?;
210 let ordered = sweep_order(&scan.groups);
211 let sweep = store.group_sweep_many(&ordered, budget)?;
212 let (mut buckets, last) = bucket(sweep.rows);
213 let cut = match sweep.complete {
214 true => None,
215 false => last,
216 };
217
218 let mut spent = 0usize;
219 let mut still_queued: Vec<GroupId> = Vec::new();
220 let mut pending = ordered.into_iter();
221 while let Some(group) = pending.next() {
222 if cut == Some(group) {
223 match spent {
224 0 => {
225 let outcome = drain_group_scanning(store, group, reaper, budget)?;
226 spent += outcome.freed;
227 if outcome.still_queued {
228 still_queued.push(group);
229 }
230 }
231 _ => still_queued.push(group),
232 }
233 still_queued.extend(pending);
234 break;
235 }
236 spent += reap_scanned(store, group, buckets.remove(&group).unwrap_or_default(), reaper)?;
237 }
238 Ok(DrainOutcome {
239 freed: spent,
240 still_queued,
241 more: scan.more,
242 })
243}
244
245pub fn reap_group<R>(store: &mut dyn StateStore, group: GroupId, reaper: &mut R, budget: usize) -> Result<usize>
246where
247 R: Reaper,
248{
249 let doomed = store.group_sweep(group, true, Some(budget))?;
250 store.remove_root_siblings(&doomed)?;
251 for (key, _) in &doomed {
252 reaper.reap(store, key)?;
253 }
254 Ok(doomed.len())
255}