1use std::collections::{BTreeMap, HashMap};
48use std::sync::{Mutex, MutexGuard, PoisonError};
49
50use super::codec::{self, DedupRecord};
51use super::{ClaimEvidence, DedupCache, DedupEntry};
52use crate::durability::DurabilityError;
53use crate::durability::head_checkpoint::READ_BATCH_SIZE;
54
55pub(super) const IN_FLIGHT_CHECKPOINT_EVERY: u64 = 256;
58
59const CLAIM_PAGE: usize = 2;
62
63const INDEX_APPEND_ATTEMPTS: usize = 64;
65
66#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
69struct OpenClaim {
70 row: u64,
71 claim: u64,
72}
73
74#[derive(Debug)]
76struct IndexState {
77 start: u64,
79 tail: u64,
81 open: HashMap<String, OpenClaim>,
83 reserved: BTreeMap<u64, usize>,
86 carrying: bool,
88 recorded: bool,
91}
92
93impl IndexState {
94 fn empty() -> Self {
96 Self {
97 start: 0,
98 tail: 0,
99 open: HashMap::new(),
100 reserved: BTreeMap::new(),
101 carrying: false,
102 recorded: false,
103 }
104 }
105
106 fn floor(&self) -> u64 {
108 let open = self.open.values().map(|claim| claim.row).min();
109 let reserved = self.reserved.keys().next().copied();
110 [open, reserved]
111 .into_iter()
112 .flatten()
113 .fold(self.tail, u64::min)
114 }
115
116 fn reserve(&mut self) -> u64 {
117 let floor = self.tail;
118 *self.reserved.entry(floor).or_insert(0) += 1;
119 floor
120 }
121
122 fn unreserve(&mut self, floor: u64) {
123 if let Some(count) = self.reserved.get_mut(&floor) {
124 *count = count.saturating_sub(1);
125 if *count == 0 {
126 self.reserved.remove(&floor);
127 }
128 }
129 }
130
131 fn appended(&mut self, after: u64) {
132 self.tail = self.tail.max(after);
133 }
134
135 fn leave(&mut self, key: &str, settled: u64) -> bool {
136 if self.open.get(key).is_some_and(|open| open.claim <= settled) {
137 self.open.remove(key);
138 return true;
139 }
140 false
141 }
142
143 fn plan(&mut self) -> Option<Advance> {
144 if self.carrying {
145 return None;
146 }
147 let floor = self.floor();
148 if floor >= self.start.saturating_add(IN_FLIGHT_CHECKPOINT_EVERY) {
149 return Some(Advance::Record(floor));
150 }
151 if self.tail < self.start.saturating_add(2 * IN_FLIGHT_CHECKPOINT_EVERY) {
152 return None;
153 }
154 let horizon = self.tail.saturating_sub(IN_FLIGHT_CHECKPOINT_EVERY);
155 let mut keys: Vec<(String, OpenClaim)> = self
156 .open
157 .iter()
158 .filter(|(_, open)| open.row < horizon)
159 .map(|(key, open)| (key.clone(), *open))
160 .collect();
161 if keys.is_empty() {
162 return None;
163 }
164 keys.sort_unstable();
165 self.carrying = true;
166 let at = self.reserve();
167 Some(Advance::Carry { keys, at })
168 }
169
170 fn carried(
171 &mut self,
172 keys: &[(String, OpenClaim)],
173 first: Option<u64>,
174 at: u64,
175 ) -> Option<u64> {
176 if let Some(first) = first {
177 let mut row = first;
178 for (key, carried) in keys {
179 if let Some(open) = self.open.get_mut(key) {
180 if open.row == carried.row {
181 open.row = row;
182 }
183 }
184 row = row.saturating_add(1);
185 }
186 self.appended(row);
187 }
188 self.unreserve(at);
189 self.carrying = false;
190 let floor = self.floor();
191 (floor > self.start).then_some(floor)
192 }
193}
194
195enum Advance {
197 Record(u64),
199 Carry {
202 keys: Vec<(String, OpenClaim)>,
203 at: u64,
204 },
205}
206
207#[derive(Debug, Default)]
209pub(super) struct InFlightIndex(Mutex<Option<IndexState>>);
210
211impl InFlightIndex {
212 pub(super) fn empty() -> Self {
215 Self(Mutex::new(Some(IndexState::empty())))
216 }
217
218 fn state(&self) -> MutexGuard<'_, Option<IndexState>> {
219 self.0.lock().unwrap_or_else(PoisonError::into_inner)
222 }
223
224 fn is_loaded(&self) -> bool {
225 self.state().is_some()
226 }
227
228 fn install(&self, loaded: IndexState) -> bool {
230 let mut state = self.state();
231 if state.is_some() {
232 return false;
233 }
234 *state = Some(loaded);
235 true
236 }
237
238 fn reserve(&self) -> Option<u64> {
239 self.state().as_mut().map(IndexState::reserve)
240 }
241
242 fn unreserve(&self, floor: u64) {
243 if let Some(state) = self.state().as_mut() {
244 state.unreserve(floor);
245 }
246 }
247
248 fn joined(&self, key: &str, row: u64, claim: u64) {
249 if let Some(state) = self.state().as_mut() {
250 state.appended(row.saturating_add(1));
251 state.open.insert(key.to_owned(), OpenClaim { row, claim });
252 }
253 }
254
255 fn unjoin(&self, key: &str, row: u64) {
257 if let Some(state) = self.state().as_mut() {
258 if state.open.get(key).is_some_and(|open| open.row == row) {
259 state.open.remove(key);
260 }
261 }
262 }
263
264 fn leave(&self, key: &str, settled: u64) -> bool {
267 self.with_state(|state| state.leave(key, settled))
268 .unwrap_or(false)
269 }
270
271 fn open_claims(&self) -> Vec<(String, u64)> {
273 self.state()
274 .as_ref()
275 .map(|state| {
276 state
277 .open
278 .iter()
279 .map(|(key, open)| (key.clone(), open.claim))
280 .collect()
281 })
282 .unwrap_or_default()
283 }
284
285 fn awaits_record(&self) -> bool {
287 self.state().as_ref().is_some_and(|state| !state.recorded)
288 }
289
290 fn start_recorded(&self) {
291 if let Some(state) = self.state().as_mut() {
292 state.recorded = true;
293 }
294 }
295
296 fn start(&self) -> Option<u64> {
297 self.state().as_ref().map(|state| state.start)
298 }
299
300 fn recorded(&self, start: u64) {
301 if let Some(state) = self.state().as_mut() {
302 state.start = state.start.max(start);
303 }
304 }
305
306 fn plan(&self) -> Option<Advance> {
307 self.with_state(IndexState::plan).flatten()
308 }
309
310 fn carried(&self, keys: &[(String, OpenClaim)], first: Option<u64>, at: u64) -> Option<u64> {
313 self.with_state(|state| state.carried(keys, first, at))
314 .flatten()
315 }
316
317 fn with_state<T>(&self, apply: impl FnOnce(&mut IndexState) -> T) -> Option<T> {
318 self.state().as_mut().map(apply)
319 }
320}
321
322impl DedupCache {
323 fn index_stream(&self) -> String {
327 format!("{}/in-flight", self.namespace)
328 }
329
330 fn index_start_key(&self) -> String {
333 format!("{}/in-flight-start", self.namespace)
334 }
335
336 pub async fn in_flight_claims(
345 &self,
346 ) -> Result<Vec<(String, Option<ClaimEvidence>)>, DurabilityError> {
347 if !self.in_flight.is_loaded() {
348 let (state, claims) = self.load_in_flight_index().await?;
349 if self.in_flight.install(state) {
350 self.advance_in_flight().await;
351 return Ok(claims);
352 }
353 }
354 let mut held = self.in_flight.open_claims();
355 held.sort_unstable();
356 let mut claims = Vec::new();
357 for (key, claim) in held {
358 if let Some(entry) = self.claim_at(&key, claim).await? {
359 claims.push((key, entry.evidence().cloned()));
360 }
361 }
362 Ok(claims)
363 }
364
365 pub(super) async fn ensure_in_flight_index(&self) -> Result<(), DurabilityError> {
367 if self.in_flight.is_loaded() {
368 return Ok(());
369 }
370 let (state, _) = self.load_in_flight_index().await?;
371 if self.in_flight.install(state) {
372 self.advance_in_flight().await;
373 }
374 Ok(())
375 }
376
377 pub(super) async fn join_in_flight(
381 &self,
382 key: &str,
383 claim: u64,
384 ) -> Result<u64, DurabilityError> {
385 self.ensure_in_flight_index().await?;
386 self.record_empty_start().await?;
387 let floor = self.in_flight.reserve().ok_or_else(|| {
388 DurabilityError::ConfigError("in-flight index is not loaded".to_owned())
389 })?;
390 let appended = self
391 .append_index_rows(vec![codec::encode_join(key, claim)?], floor)
392 .await;
393 if let Ok(row) = appended {
394 self.in_flight.joined(key, row, claim);
395 }
396 self.in_flight.unreserve(floor);
397 appended
398 }
399
400 pub(super) async fn record_empty_start(&self) -> Result<(), DurabilityError> {
405 if !self.in_flight.awaits_record() {
406 return Ok(());
407 }
408 match self.store.cas(&self.index_start_key(), 0, 1).await {
409 Ok(()) | Err(DurabilityError::CursorRegression { .. }) => {
411 self.in_flight.start_recorded();
412 Ok(())
413 }
414 Err(error) => Err(error),
415 }
416 }
417
418 pub(super) fn unjoin_in_flight(&self, key: &str, row: u64) {
420 self.in_flight.unjoin(key, row);
421 }
422
423 pub(super) async fn leave_in_flight(&self, key: &str, settled: u64) {
426 if self.in_flight.leave(key, settled) {
427 self.advance_in_flight().await;
428 }
429 }
430
431 async fn advance_in_flight(&self) {
435 if let Err(error) = self.try_advance_in_flight().await {
436 tracing::warn!(
437 namespace = %self.namespace,
438 %error,
439 "the in-flight index start could not be recorded; the next start reads the \
440 rows since the previous record"
441 );
442 }
443 }
444
445 async fn try_advance_in_flight(&self) -> Result<(), DurabilityError> {
446 let target = match self.in_flight.plan() {
447 None => return Ok(()),
448 Some(Advance::Record(target)) => target,
449 Some(Advance::Carry { keys, at }) => {
450 let rows = keys
451 .iter()
452 .map(|(key, open)| codec::encode_join(key, open.claim))
453 .collect::<Result<Vec<_>, _>>();
454 let appended = match rows {
455 Ok(rows) => self.append_index_rows(rows, at).await,
456 Err(error) => Err(error),
457 };
458 let target = self
459 .in_flight
460 .carried(&keys, appended.as_ref().ok().copied(), at);
461 appended?;
462 let Some(target) = target else {
463 return Ok(());
464 };
465 target
466 }
467 };
468 let Some(start) = self.in_flight.start() else {
469 return Ok(());
470 };
471 match self
472 .store
473 .cas(
474 &self.index_start_key(),
475 start.saturating_add(1),
476 target.saturating_add(1),
477 )
478 .await
479 {
480 Ok(()) => self.in_flight.recorded(target),
481 Err(DurabilityError::CursorRegression { stored, .. }) => {
483 self.in_flight.recorded(stored.saturating_sub(1));
484 }
485 Err(error) => return Err(error),
486 }
487 Ok(())
488 }
489
490 async fn append_index_rows(
493 &self,
494 rows: Vec<Vec<u8>>,
495 expected: u64,
496 ) -> Result<u64, DurabilityError> {
497 let stream = self.index_stream();
498 let mut expected = expected;
499 let mut last_conflict = None;
500 for _ in 0..INDEX_APPEND_ATTEMPTS {
501 match self.store.append_all(&stream, rows.clone(), expected).await {
502 Ok(_) => return Ok(expected),
503 Err(DurabilityError::SequenceConflict { actual, .. }) if actual > expected => {
506 last_conflict = Some(actual);
507 expected = actual;
508 }
509 Err(error) => return Err(error),
510 }
511 }
512 Err(DurabilityError::SequenceConflict {
513 expected,
514 actual: last_conflict.unwrap_or(expected),
515 })
516 }
517
518 async fn load_in_flight_index(
523 &self,
524 ) -> Result<(IndexState, Vec<(String, Option<ClaimEvidence>)>), DurabilityError> {
525 let start = match self.store.read_value(&self.index_start_key()).await? {
526 Some(recorded) if recorded > 0 => recorded - 1,
527 _ => self.migrate_in_flight_index().await?,
528 };
529 let (named, tail) = self.read_index_rows(start).await?;
530 let mut open = HashMap::new();
531 let mut claims = Vec::new();
532 for (key, (row, claim)) in named {
533 let held = match claim {
534 Some(claim) => self
535 .claim_at(&key, claim)
536 .await?
537 .map(|entry| (claim, entry)),
538 None => self.claim_without_sequence(&key).await?,
539 };
540 let Some((claim, entry)) = held else {
541 continue;
542 };
543 open.insert(key.clone(), OpenClaim { row, claim });
544 claims.push((key, entry.evidence().cloned()));
545 }
546 let state = IndexState {
547 start,
548 tail,
549 open,
550 reserved: BTreeMap::new(),
551 carrying: false,
552 recorded: true,
553 };
554 Ok((state, claims))
555 }
556
557 async fn read_index_rows(
561 &self,
562 start: u64,
563 ) -> Result<(BTreeMap<String, (u64, Option<u64>)>, u64), DurabilityError> {
564 let stream = self.index_stream();
565 let mut named = BTreeMap::new();
566 let mut tail = start;
567 loop {
568 let page = self.store.read_from(&stream, tail, READ_BATCH_SIZE).await?;
569 for row in &page {
570 if row.sequence < tail {
571 return Err(DurabilityError::ConfigError(format!(
572 "stream {stream} answered sequence {} to a read from {tail}",
573 row.sequence
574 )));
575 }
576 let (key, claim) = codec::decode_join(&row.payload)?;
577 named.insert(key, (row.sequence, claim));
578 tail = row.sequence.checked_add(1).ok_or_else(|| {
579 DurabilityError::ConfigError(format!("stream {stream} sequence overflow"))
580 })?;
581 }
582 if page.len() < READ_BATCH_SIZE {
583 break;
584 }
585 }
586 Ok((named, tail))
587 }
588
589 async fn claim_at(&self, key: &str, claim: u64) -> Result<Option<DedupEntry>, DurabilityError> {
602 let stream = self.stream_key_for(key);
603 let rows = self.store.read_from(&stream, claim, CLAIM_PAGE).await?;
604 let mut expected = claim;
605 for row in &rows {
606 if row.sequence != expected {
607 return Err(DurabilityError::ConfigError(format!(
608 "stream {stream} answered sequence {} where {expected} was read",
609 row.sequence
610 )));
611 }
612 expected = expected.saturating_add(1);
613 }
614 let Some(claimed) = rows.first() else {
615 return Ok(None);
616 };
617 let record = DedupRecord::deserialize(&claimed.payload)?;
618 if record.idempotency_key() != key {
619 return Err(DurabilityError::DedupCollision {
620 key: key.to_owned(),
621 });
622 }
623 if rows.len() > 1 {
624 return Ok(None);
625 }
626 Ok(record
627 .into_active()
628 .filter(|entry| entry.receipt().is_none()))
629 }
630
631 async fn claim_without_sequence(
634 &self,
635 key: &str,
636 ) -> Result<Option<(u64, DedupEntry)>, DurabilityError> {
637 let snapshot = self.load_snapshot(&self.stream_key_for(key), key).await?;
638 let claim = match snapshot.current.as_ref() {
639 Some(entry) if entry.receipt().is_none() => snapshot.claim_sequence()?,
640 _ => return Ok(None),
641 };
642 Ok(snapshot.current.map(|entry| (claim, entry)))
643 }
644
645 async fn migrate_in_flight_index(&self) -> Result<u64, DurabilityError> {
649 let mut keys = Vec::new();
650 let mut walk = self.latest_records();
651 while let Some(batch) = walk.next_batch().await? {
652 for candidate in batch {
653 if let DedupRecord::Active(entry) = candidate.record {
654 if entry.receipt().is_none() {
655 keys.push((entry.idempotency_key, candidate.sequence));
656 }
657 }
658 }
659 }
660 let start = if keys.is_empty() {
661 0
664 } else {
665 let rows = keys
666 .iter()
667 .map(|(key, claim)| codec::encode_join(key, *claim))
668 .collect::<Result<Vec<_>, _>>()?;
669 self.append_index_rows(rows, 0).await?
670 };
671 let recorded = match self
672 .store
673 .cas(&self.index_start_key(), 0, start.saturating_add(1))
674 .await
675 {
676 Ok(()) => start,
677 Err(DurabilityError::CursorRegression { stored, .. }) if stored > 0 => stored - 1,
679 Err(error) => return Err(error),
680 };
681 tracing::info!(
682 namespace = %self.namespace,
683 claims = keys.len(),
684 scanned = walk.scanned(),
685 "the dedup namespace was walked once to build its in-flight index"
686 );
687 Ok(recorded)
688 }
689}