1use std::collections::{BTreeMap, HashMap};
52use std::sync::{Mutex, MutexGuard, PoisonError};
53
54use super::codec::{self, DedupRecord};
55use super::{DedupCache, DedupEntry, InFlightClaim};
56use crate::durability::DurabilityError;
57use crate::durability::head_checkpoint::READ_BATCH_SIZE;
58
59pub(super) const IN_FLIGHT_CHECKPOINT_EVERY: u64 = 256;
62
63const CLAIM_PAGE: usize = 2;
66
67const INDEX_APPEND_ATTEMPTS: usize = 64;
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
73struct OpenClaim {
74 row: u64,
75 claim: u64,
76}
77
78#[derive(Debug)]
80struct IndexState {
81 start: u64,
83 tail: u64,
85 open: HashMap<String, OpenClaim>,
87 reserved: BTreeMap<u64, usize>,
90 carrying: bool,
92 recorded: bool,
95}
96
97impl IndexState {
98 fn empty() -> Self {
100 Self {
101 start: 0,
102 tail: 0,
103 open: HashMap::new(),
104 reserved: BTreeMap::new(),
105 carrying: false,
106 recorded: false,
107 }
108 }
109
110 fn floor(&self) -> u64 {
112 let open = self.open.values().map(|claim| claim.row).min();
113 let reserved = self.reserved.keys().next().copied();
114 [open, reserved]
115 .into_iter()
116 .flatten()
117 .fold(self.tail, u64::min)
118 }
119
120 fn reserve(&mut self) -> u64 {
121 let floor = self.tail;
122 *self.reserved.entry(floor).or_insert(0) += 1;
123 floor
124 }
125
126 fn unreserve(&mut self, floor: u64) {
127 if let Some(count) = self.reserved.get_mut(&floor) {
128 *count = count.saturating_sub(1);
129 if *count == 0 {
130 self.reserved.remove(&floor);
131 }
132 }
133 }
134
135 fn appended(&mut self, after: u64) {
136 self.tail = self.tail.max(after);
137 }
138
139 fn leave(&mut self, key: &str, settled: u64) -> bool {
140 if self.open.get(key).is_some_and(|open| open.claim <= settled) {
141 self.open.remove(key);
142 return true;
143 }
144 false
145 }
146
147 fn plan(&mut self) -> Option<Advance> {
148 if self.carrying {
149 return None;
150 }
151 let floor = self.floor();
152 if floor >= self.start.saturating_add(IN_FLIGHT_CHECKPOINT_EVERY) {
153 return Some(Advance::Record(floor));
154 }
155 if self.tail < self.start.saturating_add(2 * IN_FLIGHT_CHECKPOINT_EVERY) {
156 return None;
157 }
158 let horizon = self.tail.saturating_sub(IN_FLIGHT_CHECKPOINT_EVERY);
159 let mut keys: Vec<(String, OpenClaim)> = self
160 .open
161 .iter()
162 .filter(|(_, open)| open.row < horizon)
163 .map(|(key, open)| (key.clone(), *open))
164 .collect();
165 if keys.is_empty() {
166 return None;
167 }
168 keys.sort_unstable();
169 self.carrying = true;
170 let at = self.reserve();
171 Some(Advance::Carry { keys, at })
172 }
173
174 fn carried(
175 &mut self,
176 keys: &[(String, OpenClaim)],
177 first: Option<u64>,
178 at: u64,
179 ) -> Option<u64> {
180 if let Some(first) = first {
181 let mut row = first;
182 for (key, carried) in keys {
183 if let Some(open) = self.open.get_mut(key) {
184 if open.row == carried.row {
185 open.row = row;
186 }
187 }
188 row = row.saturating_add(1);
189 }
190 self.appended(row);
191 }
192 self.unreserve(at);
193 self.carrying = false;
194 let floor = self.floor();
195 (floor > self.start).then_some(floor)
196 }
197}
198
199enum Advance {
201 Record(u64),
203 Carry {
206 keys: Vec<(String, OpenClaim)>,
207 at: u64,
208 },
209}
210
211#[derive(Debug, Default)]
213pub(super) struct InFlightIndex(Mutex<Option<IndexState>>);
214
215impl InFlightIndex {
216 pub(super) fn empty() -> Self {
219 Self(Mutex::new(Some(IndexState::empty())))
220 }
221
222 fn state(&self) -> MutexGuard<'_, Option<IndexState>> {
223 self.0.lock().unwrap_or_else(PoisonError::into_inner)
226 }
227
228 fn is_loaded(&self) -> bool {
229 self.state().is_some()
230 }
231
232 fn install(&self, loaded: IndexState) -> bool {
234 let mut state = self.state();
235 if state.is_some() {
236 return false;
237 }
238 *state = Some(loaded);
239 true
240 }
241
242 fn reserve(&self) -> Option<u64> {
243 self.state().as_mut().map(IndexState::reserve)
244 }
245
246 fn unreserve(&self, floor: u64) {
247 if let Some(state) = self.state().as_mut() {
248 state.unreserve(floor);
249 }
250 }
251
252 fn joined(&self, key: &str, row: u64, claim: u64) {
253 if let Some(state) = self.state().as_mut() {
254 state.appended(row.saturating_add(1));
255 state.open.insert(key.to_owned(), OpenClaim { row, claim });
256 }
257 }
258
259 fn unjoin(&self, key: &str, row: u64) {
261 if let Some(state) = self.state().as_mut() {
262 if state.open.get(key).is_some_and(|open| open.row == row) {
263 state.open.remove(key);
264 }
265 }
266 }
267
268 fn leave(&self, key: &str, settled: u64) -> bool {
271 self.with_state(|state| state.leave(key, settled))
272 .unwrap_or(false)
273 }
274
275 fn open_claims(&self) -> Vec<(String, u64)> {
277 self.state()
278 .as_ref()
279 .map(|state| {
280 state
281 .open
282 .iter()
283 .map(|(key, open)| (key.clone(), open.claim))
284 .collect()
285 })
286 .unwrap_or_default()
287 }
288
289 fn awaits_record(&self) -> bool {
291 self.state().as_ref().is_some_and(|state| !state.recorded)
292 }
293
294 fn start_recorded(&self) {
295 if let Some(state) = self.state().as_mut() {
296 state.recorded = true;
297 }
298 }
299
300 fn start(&self) -> Option<u64> {
301 self.state().as_ref().map(|state| state.start)
302 }
303
304 fn recorded(&self, start: u64) {
305 if let Some(state) = self.state().as_mut() {
306 state.start = state.start.max(start);
307 }
308 }
309
310 fn plan(&self) -> Option<Advance> {
311 self.with_state(IndexState::plan).flatten()
312 }
313
314 fn carried(&self, keys: &[(String, OpenClaim)], first: Option<u64>, at: u64) -> Option<u64> {
317 self.with_state(|state| state.carried(keys, first, at))
318 .flatten()
319 }
320
321 fn with_state<T>(&self, apply: impl FnOnce(&mut IndexState) -> T) -> Option<T> {
322 self.state().as_mut().map(apply)
323 }
324}
325
326impl DedupCache {
327 fn index_stream(&self) -> String {
331 format!("{}/in-flight", self.namespace)
332 }
333
334 fn index_start_key(&self) -> String {
337 format!("{}/in-flight-start", self.namespace)
338 }
339
340 pub async fn in_flight_claims(&self) -> Result<Vec<InFlightClaim>, DurabilityError> {
349 if !self.in_flight.is_loaded() {
350 let (state, claims) = self.load_in_flight_index().await?;
351 if self.in_flight.install(state) {
352 self.advance_in_flight().await;
353 return Ok(claims);
354 }
355 }
356 let mut held = self.in_flight.open_claims();
357 held.sort_unstable();
358 let mut claims = Vec::new();
359 for (key, claim) in held {
360 if let Some(entry) = self.claim_at(&key, claim).await? {
361 claims.push(InFlightClaim::of(key, &entry));
362 }
363 }
364 Ok(claims)
365 }
366
367 pub(super) async fn ensure_in_flight_index(&self) -> Result<(), DurabilityError> {
369 if self.in_flight.is_loaded() {
370 return Ok(());
371 }
372 let (state, _) = self.load_in_flight_index().await?;
373 if self.in_flight.install(state) {
374 self.advance_in_flight().await;
375 }
376 Ok(())
377 }
378
379 pub(super) async fn join_in_flight(
383 &self,
384 key: &str,
385 claim: u64,
386 ) -> Result<u64, DurabilityError> {
387 self.ensure_in_flight_index().await?;
388 self.record_empty_start().await?;
389 let floor = self.in_flight.reserve().ok_or_else(|| {
390 DurabilityError::ConfigError("in-flight index is not loaded".to_owned())
391 })?;
392 let appended = self
393 .append_index_rows(vec![codec::encode_join(key, claim)?], floor)
394 .await;
395 if let Ok(row) = appended {
396 self.in_flight.joined(key, row, claim);
397 }
398 self.in_flight.unreserve(floor);
399 appended
400 }
401
402 pub async fn record_empty_start(&self) -> Result<(), DurabilityError> {
422 if !self.in_flight.awaits_record() {
423 return Ok(());
424 }
425 match self.store.cas(&self.index_start_key(), 0, 1).await {
426 Ok(()) | Err(DurabilityError::CursorRegression { .. }) => {
428 self.in_flight.start_recorded();
429 Ok(())
430 }
431 Err(error) => Err(error),
432 }
433 }
434
435 pub(super) fn unjoin_in_flight(&self, key: &str, row: u64) {
437 self.in_flight.unjoin(key, row);
438 }
439
440 pub(super) async fn leave_in_flight(&self, key: &str, settled: u64) {
443 if self.in_flight.leave(key, settled) {
444 self.advance_in_flight().await;
445 }
446 }
447
448 async fn advance_in_flight(&self) {
452 if let Err(error) = self.try_advance_in_flight().await {
453 tracing::warn!(
454 namespace = %self.namespace,
455 %error,
456 "the in-flight index start could not be recorded; the next start reads the \
457 rows since the previous record"
458 );
459 }
460 }
461
462 async fn try_advance_in_flight(&self) -> Result<(), DurabilityError> {
463 let target = match self.in_flight.plan() {
464 None => return Ok(()),
465 Some(Advance::Record(target)) => target,
466 Some(Advance::Carry { keys, at }) => {
467 let rows = keys
468 .iter()
469 .map(|(key, open)| codec::encode_join(key, open.claim))
470 .collect::<Result<Vec<_>, _>>();
471 let appended = match rows {
472 Ok(rows) => self.append_index_rows(rows, at).await,
473 Err(error) => Err(error),
474 };
475 let target = self
476 .in_flight
477 .carried(&keys, appended.as_ref().ok().copied(), at);
478 appended?;
479 let Some(target) = target else {
480 return Ok(());
481 };
482 target
483 }
484 };
485 let Some(start) = self.in_flight.start() else {
486 return Ok(());
487 };
488 match self
489 .store
490 .cas(
491 &self.index_start_key(),
492 start.saturating_add(1),
493 target.saturating_add(1),
494 )
495 .await
496 {
497 Ok(()) => self.in_flight.recorded(target),
498 Err(DurabilityError::CursorRegression { stored, .. }) => {
500 self.in_flight.recorded(stored.saturating_sub(1));
501 }
502 Err(error) => return Err(error),
503 }
504 Ok(())
505 }
506
507 async fn append_index_rows(
510 &self,
511 rows: Vec<Vec<u8>>,
512 expected: u64,
513 ) -> Result<u64, DurabilityError> {
514 let stream = self.index_stream();
515 let mut expected = expected;
516 let mut last_conflict = None;
517 for _ in 0..INDEX_APPEND_ATTEMPTS {
518 match self.store.append_all(&stream, rows.clone(), expected).await {
519 Ok(_) => return Ok(expected),
520 Err(DurabilityError::SequenceConflict { actual, .. }) if actual > expected => {
523 last_conflict = Some(actual);
524 expected = actual;
525 }
526 Err(error) => return Err(error),
527 }
528 }
529 Err(DurabilityError::SequenceConflict {
530 expected,
531 actual: last_conflict.unwrap_or(expected),
532 })
533 }
534
535 async fn load_in_flight_index(
540 &self,
541 ) -> Result<(IndexState, Vec<InFlightClaim>), DurabilityError> {
542 let start = match self.store.read_value(&self.index_start_key()).await? {
543 Some(recorded) if recorded > 0 => recorded - 1,
544 _ => self.migrate_in_flight_index().await?,
545 };
546 let (named, tail) = self.read_index_rows(start).await?;
547 let mut open = HashMap::new();
548 let mut claims = Vec::new();
549 for (key, (row, claim)) in named {
550 let held = match claim {
551 Some(claim) => self
552 .claim_at(&key, claim)
553 .await?
554 .map(|entry| (claim, entry)),
555 None => self.claim_without_sequence(&key).await?,
556 };
557 let Some((claim, entry)) = held else {
558 continue;
559 };
560 open.insert(key.clone(), OpenClaim { row, claim });
561 claims.push(InFlightClaim::of(key, &entry));
562 }
563 let state = IndexState {
564 start,
565 tail,
566 open,
567 reserved: BTreeMap::new(),
568 carrying: false,
569 recorded: true,
570 };
571 Ok((state, claims))
572 }
573
574 async fn read_index_rows(
578 &self,
579 start: u64,
580 ) -> Result<(BTreeMap<String, (u64, Option<u64>)>, u64), DurabilityError> {
581 let stream = self.index_stream();
582 let mut named = BTreeMap::new();
583 let mut tail = start;
584 loop {
585 let page = self.store.read_from(&stream, tail, READ_BATCH_SIZE).await?;
586 for row in &page {
587 if row.sequence < tail {
588 return Err(DurabilityError::ConfigError(format!(
589 "stream {stream} answered sequence {} to a read from {tail}",
590 row.sequence
591 )));
592 }
593 let (key, claim) = codec::decode_join(&row.payload)?;
594 named.insert(key, (row.sequence, claim));
595 tail = row.sequence.checked_add(1).ok_or_else(|| {
596 DurabilityError::ConfigError(format!("stream {stream} sequence overflow"))
597 })?;
598 }
599 if page.len() < READ_BATCH_SIZE {
600 break;
601 }
602 }
603 Ok((named, tail))
604 }
605
606 async fn claim_at(&self, key: &str, claim: u64) -> Result<Option<DedupEntry>, DurabilityError> {
619 let stream = self.stream_key_for(key);
620 let rows = self.store.read_from(&stream, claim, CLAIM_PAGE).await?;
621 let mut expected = claim;
622 for row in &rows {
623 if row.sequence != expected {
624 return Err(DurabilityError::ConfigError(format!(
625 "stream {stream} answered sequence {} where {expected} was read",
626 row.sequence
627 )));
628 }
629 expected = expected.saturating_add(1);
630 }
631 let Some(claimed) = rows.first() else {
632 return Ok(None);
633 };
634 let record = DedupRecord::deserialize(&claimed.payload)?;
635 if record.idempotency_key() != key {
636 return Err(DurabilityError::DedupCollision {
637 key: key.to_owned(),
638 });
639 }
640 if rows.len() > 1 {
641 return Ok(None);
642 }
643 Ok(record
644 .into_active()
645 .filter(|entry| entry.receipt().is_none()))
646 }
647
648 async fn claim_without_sequence(
651 &self,
652 key: &str,
653 ) -> Result<Option<(u64, DedupEntry)>, DurabilityError> {
654 let snapshot = self.load_snapshot(&self.stream_key_for(key), key).await?;
655 let claim = match snapshot.current.as_ref() {
656 Some(entry) if entry.receipt().is_none() => snapshot.claim_sequence()?,
657 _ => return Ok(None),
658 };
659 Ok(snapshot.current.map(|entry| (claim, entry)))
660 }
661
662 async fn migrate_in_flight_index(&self) -> Result<u64, DurabilityError> {
666 let mut keys = Vec::new();
667 let mut walk = self.latest_records();
668 while let Some(batch) = walk.next_batch().await? {
669 for candidate in batch {
670 if let DedupRecord::Active(entry) = candidate.record {
671 if entry.receipt().is_none() {
672 keys.push((entry.idempotency_key, candidate.sequence));
673 }
674 }
675 }
676 }
677 let start = if keys.is_empty() {
678 0
681 } else {
682 let rows = keys
683 .iter()
684 .map(|(key, claim)| codec::encode_join(key, *claim))
685 .collect::<Result<Vec<_>, _>>()?;
686 self.append_index_rows(rows, 0).await?
687 };
688 let recorded = match self
689 .store
690 .cas(&self.index_start_key(), 0, start.saturating_add(1))
691 .await
692 {
693 Ok(()) => start,
694 Err(DurabilityError::CursorRegression { stored, .. }) if stored > 0 => stored - 1,
696 Err(error) => return Err(error),
697 };
698 tracing::info!(
699 namespace = %self.namespace,
700 claims = keys.len(),
701 scanned = walk.scanned(),
702 "the dedup namespace was walked once to build its in-flight index"
703 );
704 Ok(recorded)
705 }
706}