1use std::collections::HashMap;
40use std::io::Write;
41use std::path::{Path, PathBuf};
42use std::sync::atomic::{AtomicU64, Ordering};
43use std::sync::mpsc;
44use std::sync::{Arc, Condvar, Mutex};
45use std::time::Duration;
46
47use crc::{Crc, CRC_32_ISCSI};
48
49use crate::encryption::{AesCipher, Cipher};
50use crate::engine::{LookupMetrics, LookupMetricsSnapshot};
51
52pub const FRAME_MAGIC: [u8; 4] = *b"MLCP";
54pub const FRAME_FORMAT_VERSION: u16 = 1;
56pub(crate) const CLEAR_KEY: u64 = u64::MAX;
59const FRAME_PREFIX_LEN: usize = 4 + 2 + 2 + 8 + 8 + 8 + 8 + 8;
61const FRAME_TRAILER_LEN: usize = 4;
63const FRAME_HEADER_LEN: usize = FRAME_PREFIX_LEN + 4;
65const FRAME_FULL_OVERHEAD: usize = FRAME_HEADER_LEN + FRAME_TRAILER_LEN;
66const NONCE_LEN: usize = 12;
68const CRC32C: Crc<u32> = Crc::<u32>::new(&CRC_32_ISCSI);
69
70#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct PersistedHeader {
74 pub format_version: u16,
75 pub table_id: u64,
76 pub schema_id: u64,
77 pub run_generation: u64,
78 pub cache_key: u64,
79 pub entry_generation: u64,
80 pub payload_len: u32,
81}
82
83#[derive(Debug, Clone)]
85pub struct PersistedFrame {
86 pub header: PersistedHeader,
87 pub payload: Vec<u8>,
89}
90
91pub fn encode_frame(frame: &PersistedFrame) -> Vec<u8> {
94 let mut out = Vec::with_capacity(FRAME_FULL_OVERHEAD + frame.payload.len());
95 out.extend_from_slice(&FRAME_MAGIC);
96 out.extend_from_slice(&frame.header.format_version.to_le_bytes());
97 out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&frame.header.table_id.to_le_bytes());
99 out.extend_from_slice(&frame.header.schema_id.to_le_bytes());
100 out.extend_from_slice(&frame.header.run_generation.to_le_bytes());
101 out.extend_from_slice(&frame.header.cache_key.to_le_bytes());
102 out.extend_from_slice(&frame.header.entry_generation.to_le_bytes());
103 out.extend_from_slice(&frame.header.payload_len.to_le_bytes());
104 out.extend_from_slice(&frame.payload);
105 let crc = CRC32C.checksum(&out[0..FRAME_PREFIX_LEN + 4 + frame.payload.len()]);
106 out.extend_from_slice(&crc.to_le_bytes());
107 out
108}
109
110pub fn decode_frame(bytes: &[u8]) -> Option<PersistedFrame> {
112 if bytes.len() < FRAME_FULL_OVERHEAD {
113 return None;
114 }
115 let body_len = bytes.len() - FRAME_TRAILER_LEN;
117 let stored_crc = u32::from_le_bytes(bytes[body_len..].try_into().ok()?);
118 let actual_crc = CRC32C.checksum(&bytes[..body_len]);
119 if actual_crc != stored_crc {
120 return None;
121 }
122 let mut p = &bytes[..body_len];
123 let mut magic = [0u8; 4];
124 magic.copy_from_slice(&p[..4]);
125 if magic != FRAME_MAGIC {
126 return None;
127 }
128 p = &p[4..];
129 let format_version = u16::from_le_bytes(p[..2].try_into().ok()?);
130 p = &p[2..];
131 let _reserved = u16::from_le_bytes(p[..2].try_into().ok()?);
132 p = &p[2..];
133 let table_id = u64::from_le_bytes(p[..8].try_into().ok()?);
134 p = &p[8..];
135 let schema_id = u64::from_le_bytes(p[..8].try_into().ok()?);
136 p = &p[8..];
137 let run_generation = u64::from_le_bytes(p[..8].try_into().ok()?);
138 p = &p[8..];
139 let cache_key = u64::from_le_bytes(p[..8].try_into().ok()?);
140 p = &p[8..];
141 let entry_generation = u64::from_le_bytes(p[..8].try_into().ok()?);
142 p = &p[8..];
143 let payload_len = u32::from_le_bytes(p[..4].try_into().ok()?) as usize;
144 p = &p[4..];
145 if p.len() < payload_len {
146 return None;
147 }
148 let payload = p[..payload_len].to_vec();
149 Some(PersistedFrame {
150 header: PersistedHeader {
151 format_version,
152 table_id,
153 schema_id,
154 run_generation,
155 cache_key,
156 entry_generation,
157 payload_len: payload_len as u32,
158 },
159 payload,
160 })
161}
162
163pub fn read_header_only(bytes: &[u8]) -> Option<PersistedHeader> {
168 decode_frame(bytes).map(|f| f.header)
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub struct PersistentCacheIdentity {
187 pub table_id: u64,
188 pub schema_id: u64,
189 pub logical_generation: u64,
190}
191
192#[derive(Debug, Clone, Copy)]
195pub struct PersistContext {
196 pub identity: PersistentCacheIdentity,
197 pub key: u64,
198 pub entry_generation: u64,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum PersistenceDisabledReason {
206 NoDirectory,
208 WorkerSpawnFailed,
210 WorkerShutdown,
212 QueueUnavailable,
214}
215
216impl PersistenceDisabledReason {
217 pub fn label(&self) -> &'static str {
219 match self {
220 Self::NoDirectory => "no_directory",
221 Self::WorkerSpawnFailed => "worker_spawn_failed",
222 Self::WorkerShutdown => "worker_shutdown",
223 Self::QueueUnavailable => "queue_unavailable",
224 }
225 }
226}
227
228impl std::fmt::Display for PersistenceDisabledReason {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 f.write_str(self.label())
231 }
232}
233
234pub enum PersistentPublicationState {
241 Async(Arc<PersistentResultCacheWriter>),
243 Disabled(PersistenceDisabledReason),
245}
246
247impl std::fmt::Debug for PersistentPublicationState {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 match self {
250 Self::Async(_) => f.write_str("PersistentPublicationState::Async(..)"),
251 Self::Disabled(reason) => write!(f, "PersistentPublicationState::Disabled({reason:?})"),
252 }
253 }
254}
255
256#[derive(Debug)]
260pub struct CachePersistError {
261 pub message: String,
262}
263
264impl std::fmt::Display for CachePersistError {
265 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266 f.write_str(&self.message)
267 }
268}
269
270impl std::error::Error for CachePersistError {}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
275#[allow(dead_code)] pub enum CacheLoadRejection {
277 LegacyUnframed,
279 Truncated,
281 BadCrc,
283 UnsupportedVersion(u16),
285 TableIdMismatch { expected: u64, found: u64 },
287 SchemaIdMismatch { expected: u64, found: u64 },
289 GenerationMismatch { expected: u64, found: u64 },
292 KeyMismatch { expected: u64, found: u64 },
296 MissingCipher,
298 DecryptionFailed,
300 PayloadInvalid,
302}
303
304impl std::fmt::Display for CacheLoadRejection {
305 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
306 match self {
307 Self::LegacyUnframed => f.write_str("legacy unframed data"),
308 Self::Truncated => f.write_str("truncated frame"),
309 Self::BadCrc => f.write_str("bad crc"),
310 Self::UnsupportedVersion(v) => write!(f, "unsupported format version {v}"),
311 Self::TableIdMismatch { expected, found } => {
312 write!(f, "table id mismatch: expected {expected}, found {found}")
313 }
314 Self::SchemaIdMismatch { expected, found } => {
315 write!(f, "schema id mismatch: expected {expected}, found {found}")
316 }
317 Self::GenerationMismatch { expected, found } => write!(
318 f,
319 "logical generation mismatch: expected {expected}, found {found}"
320 ),
321 Self::KeyMismatch { expected, found } => {
322 write!(f, "cache key mismatch: expected {expected}, found {found}")
323 }
324 Self::MissingCipher => f.write_str("cipher required for encrypted frame"),
325 Self::DecryptionFailed => f.write_str("decryption failed"),
326 Self::PayloadInvalid => f.write_str("payload deserialize failed"),
327 }
328 }
329}
330
331impl std::error::Error for CacheLoadRejection {}
332
333pub fn encode_persisted_entry(
340 context: PersistContext,
341 payload: &[u8],
342 cipher: Option<&AesCipher>,
343) -> Result<Vec<u8>, CachePersistError> {
344 let inner = encrypt_payload(cipher, payload).map_err(|e| CachePersistError {
345 message: format!("encrypt_persisted_entry: {:?}", e),
346 })?;
347 let frame = PersistedFrame {
348 header: PersistedHeader {
349 format_version: FRAME_FORMAT_VERSION,
350 table_id: context.identity.table_id,
351 schema_id: context.identity.schema_id,
352 run_generation: context.identity.logical_generation,
353 cache_key: context.key,
354 entry_generation: context.entry_generation,
355 payload_len: inner.len() as u32,
356 },
357 payload: inner,
358 };
359 Ok(encode_frame(&frame))
360}
361
362pub fn decode_persisted_entry(
367 expected: PersistentCacheIdentity,
368 expected_key: u64,
369 bytes: &[u8],
370 cipher: Option<&AesCipher>,
371) -> Result<Vec<u8>, CacheLoadRejection> {
372 if bytes.len() < FRAME_MAGIC.len() || bytes[..FRAME_MAGIC.len()] != FRAME_MAGIC {
376 return Err(CacheLoadRejection::LegacyUnframed);
377 }
378 let frame = decode_frame(bytes).ok_or(CacheLoadRejection::BadCrc)?;
379 if frame.header.format_version != FRAME_FORMAT_VERSION {
380 return Err(CacheLoadRejection::UnsupportedVersion(
381 frame.header.format_version,
382 ));
383 }
384 if frame.header.table_id != expected.table_id {
385 return Err(CacheLoadRejection::TableIdMismatch {
386 expected: expected.table_id,
387 found: frame.header.table_id,
388 });
389 }
390 if frame.header.schema_id != expected.schema_id {
391 return Err(CacheLoadRejection::SchemaIdMismatch {
392 expected: expected.schema_id,
393 found: frame.header.schema_id,
394 });
395 }
396 if frame.header.run_generation != expected.logical_generation {
397 return Err(CacheLoadRejection::GenerationMismatch {
405 expected: expected.logical_generation,
406 found: frame.header.run_generation,
407 });
408 }
409 if frame.header.cache_key != expected_key {
410 return Err(CacheLoadRejection::KeyMismatch {
411 expected: expected_key,
412 found: frame.header.cache_key,
413 });
414 }
415 let plaintext = decrypt_payload(cipher, &frame.payload).map_err(|_e| {
416 CacheLoadRejection::DecryptionFailed
421 })?;
422 plaintext.ok_or(CacheLoadRejection::DecryptionFailed)
423}
424
425#[derive(Debug)]
433pub enum PendingCacheOp {
434 Store(PersistableEntry),
435 Remove,
436 Clear,
439}
440
441pub struct PersistableEntry {
447 pub key: u64,
448 pub table_id: u64,
449 pub schema_id: u64,
450 pub run_generation: u64,
451 pub entry_generation: u64,
452 pub bytes: usize,
454 pub payload_factory: Box<dyn FnOnce() -> Option<Vec<u8>> + Send + 'static>,
458}
459
460impl std::fmt::Debug for PersistableEntry {
461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462 f.debug_struct("PersistableEntry")
463 .field("key", &self.key)
464 .field("table_id", &self.table_id)
465 .field("schema_id", &self.schema_id)
466 .field("run_generation", &self.run_generation)
467 .field("entry_generation", &self.entry_generation)
468 .field("bytes", &self.bytes)
469 .finish_non_exhaustive()
470 }
471}
472
473#[derive(Debug)]
477pub struct PendingCacheState {
478 pub clear_generation: u64,
479 pub next_key_generation: u64,
480 pub key_generations: HashMap<u64, u64>,
484 pub operations: HashMap<u64, PendingCacheOp>,
485 pub approx_bytes: usize,
489}
490
491impl Default for PendingCacheState {
492 fn default() -> Self {
493 Self {
494 clear_generation: 0,
495 next_key_generation: 1,
496 key_generations: HashMap::new(),
497 operations: HashMap::new(),
498 approx_bytes: 0,
499 }
500 }
501}
502
503#[derive(Debug, Clone)]
506pub struct WriterLimits {
507 pub max_pending_keys: usize,
508 pub max_pending_bytes: usize,
509}
510
511impl Default for WriterLimits {
512 fn default() -> Self {
513 Self {
514 max_pending_keys: 16_384,
515 max_pending_bytes: 64 * 1024 * 1024,
516 }
517 }
518}
519
520struct Inner {
521 state: PendingCacheState,
522 limits: WriterLimits,
523 shutdown: bool,
524}
525
526#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub enum DrainOutcome {
530 StorePublished,
531 StoreStale,
532 StoreErrored,
533 RemoveApplied,
534 RemoveErrored,
535 ClearApplied,
536 ClearErrored,
537 Abandoned,
538}
539
540#[derive(Debug)]
544pub struct DrainedOp {
545 pub key: u64,
546 pub op: PendingCacheOp,
547 pub key_generation: u64,
548 pub clear_generation: u64,
549}
550
551pub struct PersistentResultCacheWriter {
556 inner: Mutex<Inner>,
557 cond: Condvar,
558 metrics: LookupMetrics,
559 enqueued_total: AtomicU64,
560 coalesced_total: AtomicU64,
561 dropped_total: AtomicU64,
562 remove_total: AtomicU64,
563 stale_total: AtomicU64,
564 errors_total: AtomicU64,
565 abandoned_total: AtomicU64,
566 queue_depth: AtomicU64,
567 writes_in_flight: AtomicU64,
575}
576
577impl PersistentResultCacheWriter {
578 pub fn new(metrics: LookupMetrics, limits: WriterLimits) -> Self {
579 Self {
580 inner: Mutex::new(Inner {
581 state: PendingCacheState::default(),
582 limits,
583 shutdown: false,
584 }),
585 cond: Condvar::new(),
586 metrics,
587 enqueued_total: AtomicU64::new(0),
588 coalesced_total: AtomicU64::new(0),
589 dropped_total: AtomicU64::new(0),
590 remove_total: AtomicU64::new(0),
591 stale_total: AtomicU64::new(0),
592 errors_total: AtomicU64::new(0),
593 abandoned_total: AtomicU64::new(0),
594 queue_depth: AtomicU64::new(0),
595 writes_in_flight: AtomicU64::new(0),
596 }
597 }
598
599 pub fn enqueue_store(&self, entry: PersistableEntry) {
604 let mut guard = self.inner.lock().expect("writer mutex poisoned");
605 let key = entry.key;
606 let bytes = entry.bytes;
607 let already_present = guard.state.operations.contains_key(&key);
610 let would_grow = !already_present;
611 let key_cap = guard.limits.max_pending_keys;
612 let byte_cap = guard.limits.max_pending_bytes;
613 if (would_grow && guard.state.operations.len() >= key_cap)
614 || guard.state.approx_bytes.saturating_add(bytes) > byte_cap
615 {
616 self.dropped_total.fetch_add(1, Ordering::Relaxed);
617 self.metrics
618 .result_cache_persist_dropped_store_total
619 .fetch_add(1, Ordering::Relaxed);
620 return;
621 }
622 if let Some(PendingCacheOp::Remove) = guard.state.operations.get(&key) {
626 self.dropped_total.fetch_add(1, Ordering::Relaxed);
627 self.metrics
628 .result_cache_persist_dropped_store_total
629 .fetch_add(1, Ordering::Relaxed);
630 return;
631 }
632 if let Some(PendingCacheOp::Store(prev)) = guard.state.operations.get(&key) {
633 guard.state.approx_bytes = guard.state.approx_bytes.saturating_sub(prev.bytes);
635 self.coalesced_total.fetch_add(1, Ordering::Relaxed);
636 self.metrics
637 .result_cache_persist_coalesced_total
638 .fetch_add(1, Ordering::Relaxed);
639 } else {
640 let gen = guard.state.next_key_generation;
642 guard.state.next_key_generation = guard.state.next_key_generation.wrapping_add(1);
643 guard.state.key_generations.insert(key, gen);
644 }
645 guard
646 .state
647 .operations
648 .insert(key, PendingCacheOp::Store(entry));
649 guard.state.approx_bytes = guard.state.approx_bytes.saturating_add(bytes);
650 let depth = guard.state.operations.len() as u64;
651 drop(guard);
652 self.queue_depth.store(depth, Ordering::Relaxed);
653 self.enqueued_total.fetch_add(1, Ordering::Relaxed);
654 self.metrics
655 .result_cache_persist_enqueued_total
656 .fetch_add(1, Ordering::Relaxed);
657 self.cond.notify_one();
658 }
659
660 pub fn enqueue_remove(&self, key: u64) {
664 let mut guard = self.inner.lock().expect("writer mutex poisoned");
665 let new_gen = guard.state.next_key_generation;
666 guard.state.next_key_generation = guard.state.next_key_generation.wrapping_add(1);
667 guard.state.key_generations.insert(key, new_gen);
668 if let Some(PendingCacheOp::Store(prev)) = guard.state.operations.get(&key) {
670 guard.state.approx_bytes = guard.state.approx_bytes.saturating_sub(prev.bytes);
671 }
672 guard.state.operations.insert(key, PendingCacheOp::Remove);
673 let depth = guard.state.operations.len() as u64;
674 drop(guard);
675 self.remove_total.fetch_add(1, Ordering::Relaxed);
676 self.metrics
677 .result_cache_persist_remove_total
678 .fetch_add(1, Ordering::Relaxed);
679 self.queue_depth.store(depth, Ordering::Relaxed);
680 self.cond.notify_one();
681 }
682
683 pub fn enqueue_clear(&self) {
687 let mut guard = self.inner.lock().expect("writer mutex poisoned");
688 guard.state.clear_generation = guard.state.next_key_generation;
689 guard.state.next_key_generation = guard.state.next_key_generation.wrapping_add(1);
690 let mut to_sub = 0usize;
692 for entry in guard.state.operations.values() {
693 if let PendingCacheOp::Store(s) = entry {
694 to_sub = to_sub.saturating_add(s.bytes);
695 }
696 }
697 guard.state.approx_bytes = guard.state.approx_bytes.saturating_sub(to_sub);
698 guard.state.operations.clear();
699 guard.state.key_generations.clear();
700 guard
704 .state
705 .operations
706 .insert(CLEAR_KEY, PendingCacheOp::Clear);
707 let depth = guard.state.operations.len() as u64;
708 drop(guard);
709 self.queue_depth.store(depth, Ordering::Relaxed);
710 self.cond.notify_all();
711 }
712
713 pub fn drain_one(&self) -> Option<DrainedOp> {
717 let mut guard = self.inner.lock().expect("writer mutex poisoned");
718 loop {
719 if let Some(next_key) = guard.state.operations.keys().next().copied() {
720 let op = guard
721 .state
722 .operations
723 .remove(&next_key)
724 .expect("just observed");
725 let key_gen = guard
726 .state
727 .key_generations
728 .get(&next_key)
729 .copied()
730 .unwrap_or(0);
731 if let PendingCacheOp::Store(s) = &op {
733 guard.state.approx_bytes = guard.state.approx_bytes.saturating_sub(s.bytes);
734 }
735 guard.state.key_generations.remove(&next_key);
739 let clear_gen = guard.state.clear_generation;
740 let depth = guard.state.operations.len() as u64;
741 drop(guard);
742 self.queue_depth.store(depth, Ordering::Relaxed);
743 return Some(DrainedOp {
744 key: next_key,
745 op,
746 key_generation: key_gen,
747 clear_generation: clear_gen,
748 });
749 }
750 if guard.shutdown {
751 return None;
752 }
753 guard = self.cond.wait(guard).expect("condvar wait poisoned");
754 }
755 }
756
757 pub fn try_drain_one(&self) -> Option<DrainedOp> {
761 let mut guard = self.inner.lock().expect("writer mutex poisoned");
762 let next_key = {
763 let mut iter = guard.state.operations.iter();
764 let (k, _) = iter.next()?;
765 *k
766 };
767 let op = guard
768 .state
769 .operations
770 .remove(&next_key)
771 .expect("just observed");
772 let key_gen = guard
773 .state
774 .key_generations
775 .get(&next_key)
776 .copied()
777 .unwrap_or(0);
778 if let PendingCacheOp::Store(s) = &op {
779 guard.state.approx_bytes = guard.state.approx_bytes.saturating_sub(s.bytes);
780 }
781 guard.state.key_generations.remove(&next_key);
782 let clear_gen = guard.state.clear_generation;
783 let depth = guard.state.operations.len() as u64;
784 drop(guard);
785 self.queue_depth.store(depth, Ordering::Relaxed);
786 Some(DrainedOp {
787 key: next_key,
788 op,
789 key_generation: key_gen,
790 clear_generation: clear_gen,
791 })
792 }
793
794 pub fn shutdown(&self) {
798 let mut guard = self.inner.lock().expect("writer mutex poisoned");
799 guard.shutdown = true;
800 drop(guard);
801 self.cond.notify_all();
802 }
803
804 pub fn pending_count(&self) -> u64 {
807 self.queue_depth.load(Ordering::Relaxed)
808 }
809
810 pub fn drain_all_as_abandoned(&self) -> u64 {
815 let mut guard = self.inner.lock().expect("writer mutex poisoned");
816 let mut n = 0u64;
817 let mut bytes = 0usize;
818 for (_, op) in guard.state.operations.drain() {
819 if let PendingCacheOp::Store(s) = op {
820 bytes = bytes.saturating_add(s.bytes);
821 }
822 n += 1;
823 }
824 guard.state.key_generations.clear();
825 guard.state.approx_bytes = 0;
826 drop(guard);
827 if n > 0 {
828 self.abandoned_total.fetch_add(n, Ordering::Relaxed);
829 self.metrics
830 .result_cache_persist_shutdown_abandoned_total
831 .fetch_add(n, Ordering::Relaxed);
832 let _ = bytes; }
834 self.queue_depth.store(0, Ordering::Relaxed);
835 self.cond.notify_all();
836 n
837 }
838
839 pub fn record_outcome(&self, outcome: DrainOutcome) {
840 match outcome {
841 DrainOutcome::StorePublished => {}
842 DrainOutcome::RemoveApplied => {}
843 DrainOutcome::ClearApplied => {}
844 DrainOutcome::StoreStale => {
845 self.stale_total.fetch_add(1, Ordering::Relaxed);
846 self.metrics
847 .result_cache_persist_stale_store_skipped_total
848 .fetch_add(1, Ordering::Relaxed);
849 }
850 DrainOutcome::StoreErrored
851 | DrainOutcome::RemoveErrored
852 | DrainOutcome::ClearErrored => {
853 self.errors_total.fetch_add(1, Ordering::Relaxed);
854 self.metrics
855 .result_cache_persist_errors_total
856 .fetch_add(1, Ordering::Relaxed);
857 }
858 DrainOutcome::Abandoned => {
859 self.abandoned_total.fetch_add(1, Ordering::Relaxed);
860 self.metrics
861 .result_cache_persist_shutdown_abandoned_total
862 .fetch_add(1, Ordering::Relaxed);
863 }
864 }
865 }
866
867 pub fn enqueued_total(&self) -> u64 {
868 self.enqueued_total.load(Ordering::Relaxed)
869 }
870 pub fn coalesced_total(&self) -> u64 {
871 self.coalesced_total.load(Ordering::Relaxed)
872 }
873 pub fn dropped_total(&self) -> u64 {
874 self.dropped_total.load(Ordering::Relaxed)
875 }
876 pub fn remove_total(&self) -> u64 {
877 self.remove_total.load(Ordering::Relaxed)
878 }
879 pub fn stale_total(&self) -> u64 {
880 self.stale_total.load(Ordering::Relaxed)
881 }
882 pub fn errors_total(&self) -> u64 {
883 self.errors_total.load(Ordering::Relaxed)
884 }
885 pub fn abandoned_total(&self) -> u64 {
886 self.abandoned_total.load(Ordering::Relaxed)
887 }
888 pub fn queue_depth(&self) -> usize {
889 self.queue_depth.load(Ordering::Relaxed) as usize
890 }
891 pub fn writes_in_flight(&self) -> u64 {
893 self.writes_in_flight.load(Ordering::Relaxed)
894 }
895
896 pub fn persist_snapshot(&self) -> LookupMetricsSnapshot {
900 LookupMetricsSnapshot {
901 result_cache_persist_enqueued_total: self.enqueued_total.load(Ordering::Relaxed),
902 result_cache_persist_coalesced_total: self.coalesced_total.load(Ordering::Relaxed),
903 result_cache_persist_dropped_store_total: self.dropped_total.load(Ordering::Relaxed),
904 result_cache_persist_remove_total: self.remove_total.load(Ordering::Relaxed),
905 result_cache_persist_stale_store_skipped_total: self
906 .stale_total
907 .load(Ordering::Relaxed),
908 result_cache_persist_errors_total: self.errors_total.load(Ordering::Relaxed),
909 result_cache_persist_shutdown_abandoned_total: self
910 .abandoned_total
911 .load(Ordering::Relaxed),
912 result_cache_persist_queue_depth: self.queue_depth.load(Ordering::Relaxed),
913 ..LookupMetricsSnapshot::default()
914 }
915 }
916
917 pub fn for_test(limits: WriterLimits) -> Self {
921 Self::new(LookupMetrics::default(), limits)
922 }
923
924 pub fn bump_persist_generation(&self, key: u64) -> u64 {
929 let mut guard = self.inner.lock().expect("writer mutex poisoned");
930 let gen = guard.state.next_key_generation;
931 guard.state.next_key_generation = guard.state.next_key_generation.wrapping_add(1);
932 guard.state.key_generations.insert(key, gen);
933 gen
934 }
935}
936
937pub trait PersistentCacheIo: Send + Sync {
945 fn write_atomic(&self, key: u64, frame: &[u8]) -> Result<(), IoError>;
948 fn remove(&self, key: u64) -> Result<(), IoError>;
950 fn clear(&self) -> Result<(), IoError>;
952 fn load(&self, key: u64) -> Result<Option<Vec<u8>>, IoError>;
955 fn exists(&self, key: u64) -> bool;
957}
958
959#[derive(Debug, Clone)]
963pub struct IoError {
964 pub kind: IoErrorKind,
965 pub message: String,
966}
967
968impl IoError {
969 pub fn new(kind: IoErrorKind, message: impl Into<String>) -> Self {
970 Self {
971 kind,
972 message: message.into(),
973 }
974 }
975}
976
977#[derive(Debug, Clone, Copy, PartialEq, Eq)]
980pub enum IoErrorKind {
981 Other,
982}
983
984pub struct RealPersistentCacheIo {
988 dir: PathBuf,
989}
990
991impl RealPersistentCacheIo {
992 pub fn new(dir: PathBuf) -> std::io::Result<Self> {
993 std::fs::create_dir_all(&dir)?;
994 Ok(Self { dir })
995 }
996 fn final_path(&self, key: u64) -> PathBuf {
997 self.dir.join(format!("{key:016x}.bin"))
998 }
999 fn temp_path(&self, key: u64) -> PathBuf {
1000 self.dir.join(format!("{key:016x}.bin.tmp"))
1001 }
1002}
1003
1004impl PersistentCacheIo for RealPersistentCacheIo {
1005 fn write_atomic(&self, key: u64, frame: &[u8]) -> Result<(), IoError> {
1006 let final_path = self.final_path(key);
1007 let tmp_path = self.temp_path(key);
1008 let _ = std::fs::remove_file(&tmp_path);
1010 {
1011 let mut f = std::fs::File::create(&tmp_path).map_err(|e| {
1012 IoError::new(
1013 IoErrorKind::Other,
1014 format!("create {}: {e}", tmp_path.display()),
1015 )
1016 })?;
1017 f.write_all(frame)
1018 .map_err(|e| IoError::new(IoErrorKind::Other, format!("write tmp: {e}")))?;
1019 f.flush()
1020 .map_err(|e| IoError::new(IoErrorKind::Other, format!("flush tmp: {e}")))?;
1021 f.sync_all()
1022 .map_err(|e| IoError::new(IoErrorKind::Other, format!("fsync tmp: {e}")))?;
1023 }
1024 std::fs::rename(&tmp_path, &final_path).map_err(|e| {
1025 let _ = std::fs::remove_file(&tmp_path);
1027 IoError::new(
1028 IoErrorKind::Other,
1029 format!(
1030 "rename {} -> {}: {e}",
1031 tmp_path.display(),
1032 final_path.display()
1033 ),
1034 )
1035 })?;
1036 if let Ok(dir) = std::fs::File::open(&self.dir) {
1038 let _ = dir.sync_all();
1039 }
1040 Ok(())
1041 }
1042 fn remove(&self, key: u64) -> Result<(), IoError> {
1043 let path = self.final_path(key);
1044 match std::fs::remove_file(&path) {
1045 Ok(()) => Ok(()),
1046 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
1047 Err(e) => Err(IoError::new(
1048 IoErrorKind::Other,
1049 format!("remove {}: {e}", path.display()),
1050 )),
1051 }
1052 }
1053 fn clear(&self) -> Result<(), IoError> {
1054 let entries = match std::fs::read_dir(&self.dir) {
1055 Ok(e) => e,
1056 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
1057 Err(e) => {
1058 return Err(IoError::new(
1059 IoErrorKind::Other,
1060 format!("read_dir {}: {e}", self.dir.display()),
1061 ))
1062 }
1063 };
1064 for entry in entries.flatten() {
1065 let path = entry.path();
1066 if path.extension().and_then(|s| s.to_str()) == Some("bin") {
1067 if let Err(e) = std::fs::remove_file(&path) {
1068 if e.kind() != std::io::ErrorKind::NotFound {
1069 return Err(IoError::new(
1070 IoErrorKind::Other,
1071 format!("remove {}: {e}", path.display()),
1072 ));
1073 }
1074 }
1075 }
1076 }
1077 Ok(())
1078 }
1079 fn load(&self, key: u64) -> Result<Option<Vec<u8>>, IoError> {
1080 let path = self.final_path(key);
1081 match std::fs::read(&path) {
1082 Ok(b) => Ok(Some(b)),
1083 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
1084 Err(e) => Err(IoError::new(
1085 IoErrorKind::Other,
1086 format!("read {}: {e}", path.display()),
1087 )),
1088 }
1089 }
1090 fn exists(&self, key: u64) -> bool {
1091 self.final_path(key).exists()
1092 }
1093}
1094
1095pub fn encrypt_payload(cipher: Option<&AesCipher>, plaintext: &[u8]) -> Result<Vec<u8>, IoError> {
1098 let Some(cipher) = cipher else {
1099 return Ok(plaintext.to_vec());
1100 };
1101 let mut nonce = [0u8; NONCE_LEN];
1102 crate::encryption::fill_random(&mut nonce)
1103 .map_err(|e| IoError::new(IoErrorKind::Other, format!("fill_random nonce: {e}")))?;
1104 let ct = cipher
1105 .encrypt_page(&nonce, plaintext)
1106 .map_err(|e| IoError::new(IoErrorKind::Other, format!("aes encrypt: {e}")))?;
1107 let mut out = Vec::with_capacity(NONCE_LEN + ct.len());
1108 out.extend_from_slice(&nonce);
1109 out.extend_from_slice(&ct);
1110 Ok(out)
1111}
1112
1113pub fn decrypt_payload(
1116 cipher: Option<&AesCipher>,
1117 bytes: &[u8],
1118) -> Result<Option<Vec<u8>>, IoError> {
1119 let Some(cipher) = cipher else {
1120 return Ok(Some(bytes.to_vec()));
1121 };
1122 if bytes.len() < NONCE_LEN {
1123 return Ok(None);
1124 }
1125 let nonce: [u8; NONCE_LEN] = bytes[..NONCE_LEN].try_into().expect("checked above");
1126 match cipher.decrypt_page(&nonce, &bytes[NONCE_LEN..]) {
1127 Ok(plaintext) => Ok(Some(plaintext)),
1128 Err(_) => Ok(None),
1129 }
1130}
1131
1132#[cfg(test)]
1133#[allow(clippy::items_after_test_module)]
1134mod tests {
1135 use super::*;
1136
1137 fn entry(key: u64, bytes: usize) -> PersistableEntry {
1138 PersistableEntry {
1139 key,
1140 table_id: 0,
1141 schema_id: 0,
1142 run_generation: 0,
1143 entry_generation: 0,
1144 bytes,
1145 payload_factory: Box::new(|| Some(Vec::new())),
1146 }
1147 }
1148
1149 #[test]
1150 fn frame_round_trip() {
1151 let frame = PersistedFrame {
1152 header: PersistedHeader {
1153 format_version: FRAME_FORMAT_VERSION,
1154 table_id: 7,
1155 schema_id: 9,
1156 run_generation: 11,
1157 cache_key: 13,
1158 entry_generation: 17,
1159 payload_len: 5,
1160 },
1161 payload: b"hello".to_vec(),
1162 };
1163 let encoded = encode_frame(&frame);
1164 let decoded = decode_frame(&encoded).expect("round-trip");
1165 assert_eq!(decoded.header, frame.header);
1166 assert_eq!(decoded.payload, frame.payload);
1167 }
1168
1169 #[test]
1170 fn frame_rejects_bad_crc() {
1171 let frame = PersistedFrame {
1172 header: PersistedHeader {
1173 format_version: FRAME_FORMAT_VERSION,
1174 table_id: 1,
1175 schema_id: 1,
1176 run_generation: 1,
1177 cache_key: 1,
1178 entry_generation: 1,
1179 payload_len: 4,
1180 },
1181 payload: b"test".to_vec(),
1182 };
1183 let mut encoded = encode_frame(&frame);
1184 let last = encoded.len() - 1;
1185 encoded[last] ^= 0xFF;
1186 assert!(decode_frame(&encoded).is_none());
1187 }
1188
1189 #[test]
1190 fn coalescing_replaces_earlier_store() {
1191 let m = LookupMetrics::default();
1192 let w = PersistentResultCacheWriter::new(m, WriterLimits::default());
1193 w.enqueue_store(entry(1, 100));
1194 w.enqueue_store(entry(1, 200));
1195 assert_eq!(w.coalesced_total(), 1);
1196 assert_eq!(w.queue_depth(), 1);
1197 }
1198
1199 #[test]
1200 fn coalescing_does_not_grow_key_count() {
1201 let m = LookupMetrics::default();
1204 let w = PersistentResultCacheWriter::new(
1205 m,
1206 WriterLimits {
1207 max_pending_keys: 1,
1208 max_pending_bytes: 1024 * 1024,
1209 },
1210 );
1211 w.enqueue_store(entry(7, 100));
1212 w.enqueue_store(entry(7, 200));
1213 assert_eq!(w.coalesced_total(), 1);
1214 assert_eq!(w.dropped_total(), 0);
1215 assert_eq!(w.queue_depth(), 1);
1216 }
1217
1218 #[test]
1219 fn remove_supersedes_pending_store() {
1220 let m = LookupMetrics::default();
1221 let w = PersistentResultCacheWriter::new(m, WriterLimits::default());
1222 w.enqueue_store(entry(1, 100));
1223 w.enqueue_remove(1);
1224 w.enqueue_store(entry(1, 100));
1226 assert_eq!(w.dropped_total(), 1);
1227 assert_eq!(w.queue_depth(), 1);
1228 }
1229
1230 #[test]
1231 fn clear_invalidates_every_queued_op() {
1232 let m = LookupMetrics::default();
1233 let w = PersistentResultCacheWriter::new(m, WriterLimits::default());
1234 w.enqueue_store(entry(1, 100));
1235 w.enqueue_store(entry(2, 100));
1236 w.enqueue_clear();
1237 assert_eq!(w.queue_depth(), 1);
1240 let drained = w.drain_one().expect("drain clear");
1241 assert!(matches!(drained.op, PendingCacheOp::Clear));
1242 assert_eq!(w.queue_depth(), 0);
1243 }
1244
1245 #[test]
1246 fn capacity_overflow_drops_new_key() {
1247 let m = LookupMetrics::default();
1248 let w = PersistentResultCacheWriter::new(
1249 m,
1250 WriterLimits {
1251 max_pending_keys: 1,
1252 max_pending_bytes: 1024,
1253 },
1254 );
1255 w.enqueue_store(entry(1, 100));
1256 w.enqueue_store(entry(2, 100));
1257 assert_eq!(w.dropped_total(), 1);
1258 assert_eq!(w.queue_depth(), 1);
1259 }
1260
1261 #[test]
1262 fn byte_accounting_exact() {
1263 let m = LookupMetrics::default();
1264 let w = PersistentResultCacheWriter::new(
1265 m,
1266 WriterLimits {
1267 max_pending_keys: 64,
1268 max_pending_bytes: 1000,
1269 },
1270 );
1271 w.enqueue_store(entry(1, 400));
1272 w.enqueue_store(entry(2, 400));
1273 w.enqueue_store(entry(3, 400));
1275 assert_eq!(w.dropped_total(), 1);
1276 assert_eq!(w.queue_depth(), 2);
1277
1278 w.enqueue_store(entry(1, 200));
1281 w.enqueue_store(entry(3, 400));
1282 assert_eq!(w.dropped_total(), 1);
1283
1284 let m2 = LookupMetrics::default();
1287 let w2 = PersistentResultCacheWriter::new(
1288 m2,
1289 WriterLimits {
1290 max_pending_keys: 64,
1291 max_pending_bytes: 1000,
1292 },
1293 );
1294 w2.enqueue_store(entry(1, 400));
1295 w2.enqueue_store(entry(2, 400));
1296 let _ = w2.drain_one();
1299 w2.enqueue_store(entry(3, 400));
1300 assert_eq!(w2.dropped_total(), 0);
1301 }
1302
1303 #[test]
1304 fn shutdown_drains_pending_ops() {
1305 let m = LookupMetrics::default();
1306 let w = PersistentResultCacheWriter::new(m, WriterLimits::default());
1307 w.enqueue_store(entry(1, 100));
1308 w.enqueue_store(entry(2, 100));
1309 w.shutdown();
1310 assert!(w.drain_one().is_some());
1311 assert!(w.drain_one().is_some());
1312 assert!(w.drain_one().is_none());
1313 }
1314
1315 #[test]
1316 fn drain_all_as_abandoned_clears_queue() {
1317 let m = LookupMetrics::default();
1318 let w = PersistentResultCacheWriter::new(m, WriterLimits::default());
1319 w.enqueue_store(entry(1, 100));
1320 w.enqueue_store(entry(2, 100));
1321 w.enqueue_store(entry(3, 100));
1322 let n = w.drain_all_as_abandoned();
1323 assert_eq!(n, 3);
1324 assert_eq!(w.queue_depth(), 0);
1325 assert_eq!(w.abandoned_total(), 3);
1326 }
1327
1328 fn generation_context(table_id: u64, schema_id: u64, generation: u64) -> PersistContext {
1333 PersistContext {
1334 identity: PersistentCacheIdentity {
1335 table_id,
1336 schema_id,
1337 logical_generation: generation,
1338 },
1339 key: 7,
1340 entry_generation: 1,
1341 }
1342 }
1343
1344 fn assert_exact_generation_identity(cipher: Option<&AesCipher>) {
1345 let expected = PersistentCacheIdentity {
1346 table_id: 1,
1347 schema_id: 1,
1348 logical_generation: 50,
1349 };
1350 let payload = b"cached-rows-payload";
1351
1352 let bytes =
1354 encode_persisted_entry(generation_context(1, 1, 50), payload, cipher).expect("encode");
1355 let decoded = decode_persisted_entry(expected, 7, &bytes, cipher).expect("equal accepted");
1356 assert_eq!(decoded, payload);
1357
1358 let bytes =
1360 encode_persisted_entry(generation_context(1, 1, 49), payload, cipher).expect("encode");
1361 let err = decode_persisted_entry(expected, 7, &bytes, cipher)
1362 .expect_err("older generation rejected");
1363 assert_eq!(
1364 err,
1365 CacheLoadRejection::GenerationMismatch {
1366 expected: 50,
1367 found: 49
1368 }
1369 );
1370
1371 let bytes =
1373 encode_persisted_entry(generation_context(1, 1, 51), payload, cipher).expect("encode");
1374 let err = decode_persisted_entry(expected, 7, &bytes, cipher)
1375 .expect_err("future generation rejected");
1376 assert_eq!(
1377 err,
1378 CacheLoadRejection::GenerationMismatch {
1379 expected: 50,
1380 found: 51
1381 }
1382 );
1383 }
1384
1385 #[test]
1386 fn loader_requires_exact_logical_generation_plaintext() {
1387 assert_exact_generation_identity(None);
1388 }
1389
1390 #[test]
1391 fn loader_requires_exact_logical_generation_encrypted() {
1392 let cipher = AesCipher::new(&[0x42u8; 32]).expect("32-byte key");
1393 assert_exact_generation_identity(Some(&cipher));
1394 }
1395}
1396
1397pub fn real_io_final_path(dir: &Path, key: u64) -> PathBuf {
1401 dir.join(format!("{key:016x}.bin"))
1402}
1403
1404pub struct WorkerConfig {
1410 pub writer: Arc<PersistentResultCacheWriter>,
1412 pub io: Arc<dyn PersistentCacheIo>,
1415 pub cipher: Option<Arc<AesCipher>>,
1419 pub staleness: Arc<dyn StalenessGuard>,
1424 pub max_staleness_retries: u32,
1427 pub completion: Option<mpsc::Sender<()>>,
1432}
1433
1434pub trait StalenessGuard: Send + Sync {
1438 fn is_current(&self, key: u64, key_generation: u64, clear_generation: u64) -> bool;
1442}
1443
1444pub struct WriterStalenessGuard {
1448 writer: Arc<PersistentResultCacheWriter>,
1449}
1450
1451impl WriterStalenessGuard {
1452 pub fn new(writer: Arc<PersistentResultCacheWriter>) -> Self {
1453 Self { writer }
1454 }
1455}
1456
1457impl StalenessGuard for WriterStalenessGuard {
1458 fn is_current(&self, key: u64, key_generation: u64, clear_generation: u64) -> bool {
1459 let guard = self.writer.inner.lock().expect("writer mutex poisoned");
1460 if guard.state.clear_generation != clear_generation {
1461 return false;
1462 }
1463 match guard.state.key_generations.get(&key) {
1464 Some(g) => *g == key_generation,
1465 None => true, }
1467 }
1468}
1469
1470pub fn spawn_persistent_cache_worker(config: WorkerConfig) -> std::thread::JoinHandle<()> {
1473 std::thread::spawn(move || run_persistent_cache_worker(config))
1474}
1475
1476fn run_persistent_cache_worker(config: WorkerConfig) {
1477 let WorkerConfig {
1478 writer,
1479 io,
1480 cipher,
1481 staleness,
1482 max_staleness_retries,
1483 completion,
1484 } = config;
1485 loop {
1486 let drained = match writer.drain_one() {
1487 Some(d) => d,
1488 None => {
1489 if let Some(tx) = completion {
1492 let _ = tx.send(());
1493 }
1494 return;
1495 }
1496 };
1497 let mut attempts = 0u32;
1501 let is_current = loop {
1502 if staleness.is_current(
1503 drained.key,
1504 drained.key_generation,
1505 drained.clear_generation,
1506 ) {
1507 break true;
1508 }
1509 if attempts >= max_staleness_retries {
1510 break false;
1511 }
1512 attempts += 1;
1513 std::thread::yield_now();
1514 };
1515 if !is_current {
1516 if matches!(drained.op, PendingCacheOp::Store(_)) {
1517 writer.record_outcome(DrainOutcome::StoreStale);
1518 }
1519 continue;
1522 }
1523 if !staleness.is_current(
1534 drained.key,
1535 drained.key_generation,
1536 drained.clear_generation,
1537 ) {
1538 if matches!(drained.op, PendingCacheOp::Store(_)) {
1539 writer.record_outcome(DrainOutcome::StoreStale);
1540 }
1541 continue;
1542 }
1543 match drained.op {
1544 PendingCacheOp::Store(entry) => {
1545 writer.writes_in_flight.fetch_add(1, Ordering::Relaxed);
1546 let payload = match (entry.payload_factory)() {
1547 Some(p) => p,
1548 None => {
1549 writer.record_outcome(DrainOutcome::StoreErrored);
1550 writer.writes_in_flight.fetch_sub(1, Ordering::Relaxed);
1551 continue;
1552 }
1553 };
1554 let context = PersistContext {
1555 identity: PersistentCacheIdentity {
1556 table_id: entry.table_id,
1557 schema_id: entry.schema_id,
1558 logical_generation: entry.run_generation,
1559 },
1560 key: entry.key,
1561 entry_generation: entry.entry_generation,
1562 };
1563 let bytes = match encode_persisted_entry(context, &payload, cipher.as_deref()) {
1564 Ok(b) => b,
1565 Err(_) => {
1566 writer.record_outcome(DrainOutcome::StoreErrored);
1567 writer.writes_in_flight.fetch_sub(1, Ordering::Relaxed);
1568 continue;
1569 }
1570 };
1571 let outcome = match io.write_atomic(entry.key, &bytes) {
1572 Ok(()) => DrainOutcome::StorePublished,
1573 Err(_) => DrainOutcome::StoreErrored,
1574 };
1575 writer.record_outcome(outcome);
1576 writer.writes_in_flight.fetch_sub(1, Ordering::Relaxed);
1577 }
1578 PendingCacheOp::Remove => {
1579 writer.writes_in_flight.fetch_add(1, Ordering::Relaxed);
1580 let outcome = match io.remove(drained.key) {
1581 Ok(()) => DrainOutcome::RemoveApplied,
1582 Err(_) => DrainOutcome::RemoveErrored,
1583 };
1584 writer.record_outcome(outcome);
1585 writer.writes_in_flight.fetch_sub(1, Ordering::Relaxed);
1586 }
1587 PendingCacheOp::Clear => {
1588 writer.writes_in_flight.fetch_add(1, Ordering::Relaxed);
1589 let outcome = match io.clear() {
1590 Ok(()) => DrainOutcome::ClearApplied,
1591 Err(_) => DrainOutcome::ClearErrored,
1592 };
1593 writer.record_outcome(outcome);
1594 writer.writes_in_flight.fetch_sub(1, Ordering::Relaxed);
1595 }
1596 }
1597 }
1598}
1599
1600pub fn recv_completion_with_timeout(rx: &mpsc::Receiver<()>, timeout: Duration) -> bool {
1605 matches!(rx.recv_timeout(timeout), Ok(()))
1606}