-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathclock_ring.rs
More file actions
4342 lines (3965 loc) · 143 KB
/
clock_ring.rs
File metadata and controls
4342 lines (3965 loc) · 143 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! Clock-sweep ring for second-chance eviction.
//!
//! Uses a fixed-size slot array and a hand pointer to evict the first
//! unreferenced entry encountered. Accesses set a referenced bit that
//! grants a second chance before eviction.
//!
//! ## Architecture
//!
//! ```text
//! ┌───────────────────────────────────────────────────────────────────────────┐
//! │ ClockRing<K, V> │
//! │ │
//! │ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │
//! │ │ index: HashMap<K, usize> │ │ slots: Vec<Option<Entry>> │ │
//! │ │ │ │ │ │
//! │ │ ┌───────────┬──────────┐ │ │ ┌─────┬─────┬─────┬─────┐ │ │
//! │ │ │ Key │ Index │ │ │ │ 0 │ 1 │ 2 │ 3 │ │ │
//! │ │ ├───────────┼──────────┤ │ │ ├─────┼─────┼─────┼─────┤ │ │
//! │ │ │ "key_a" │ 0 │ ──┼───┼──►│ A,1 │ B,0 │ C,1 │None │ │ │
//! │ │ │ "key_b" │ 1 │ ──┼───┼──►│ │ │ │ │ │ │
//! │ │ │ "key_c" │ 2 │ ──┼───┼──►│ │ │ │ │ │ │
//! │ │ └───────────┴──────────┘ │ │ └─────┴─────┴─────┴─────┘ │ │
//! │ └─────────────────────────────┘ │ ▲ │ │
//! │ │ │ hand = 1 │ │
//! │ └────────────────┼────────────────┘ │
//! │ │ │
//! │ Entry: { key, value } + referenced: Vec<bool> │ │
//! │ "A,1" = slot 0: Entry { key: "key_a" }, ref[0]=1 │ │
//! │ "B,0" = slot 1: Entry { key: "key_b" }, ref[1]=0 ◄┘ (next victim) │
//! │ │
//! └───────────────────────────────────────────────────────────────────────────┘
//!
//! Clock Sweep Algorithm
//! ─────────────────────
//!
//! Insert "key_d" when full (hand at slot 1):
//!
//! Step 1: Check slot[1] ("B", ref=0)
//! ref=0 → EVICT, insert here
//!
//! Result: slot[1] = Entry { key: "key_d", ref=false }
//! hand advances to 2
//! Return: Some(("key_b", value_b))
//!
//! Insert "key_e" when full (hand at slot 2):
//!
//! Step 1: Check slot[2] ("C", ref=1)
//! ref=1 → clear ref, advance hand
//! Step 2: Check slot[3] (None)
//! Empty → insert here
//!
//! Result: slot[3] = Entry { key: "key_e", ref=false }
//! hand advances to 0
//! Return: None (used empty slot)
//!
//! Second-Chance Behavior
//! ──────────────────────
//!
//! ┌────────────────────────────────────────────────────────────────────────┐
//! │ Access via get()/touch() → Sets referenced = true │
//! │ │
//! │ Eviction scan: │
//! │ ref=1 → Clear to ref=0, skip (second chance granted) │
//! │ ref=0 → Evict this entry │
//! │ │
//! │ Effect: Recently accessed entries survive longer │
//! └────────────────────────────────────────────────────────────────────────┘
//! ```
//!
//! ## Key Components
//!
//! - [`ClockRing`]: Single-threaded CLOCK cache
//! - [`ConcurrentClockRing`]: Thread-safe wrapper with `RwLock`
//! - [`Iter`], [`IterMut`], [`IntoIter`]: Iterators over entries
//! - [`Keys`], [`Values`], [`ValuesMut`]: Iterators over keys or values
//!
//! ## Operations
//!
//! | Operation | Time | Notes |
//! |-----------------|-------------|----------------------------------------|
//! | [`insert`] | O(1) amort. | Bounded scan with reference clearing |
//! | [`get`] | O(1) | Returns value, sets reference bit |
//! | [`peek`] | O(1) | Returns value, does NOT set ref bit |
//! | [`touch`] | O(1) | Sets reference bit only |
//! | [`remove`] | O(1) | Clears slot + index entry |
//! | [`pop_victim`] | O(1) amort. | Evicts next unreferenced entry |
//! | [`peek_victim`] | O(n) worst | Finds next victim without modifying |
//! | [`iter`] / [`keys`] / [`values`] | O(n) | Borrowed iteration over entries |
//! | [`into_iter`] | O(n) | Consuming iteration over entries |
//!
//! [`insert`]: ClockRing::insert
//! [`get`]: ClockRing::get
//! [`peek`]: ClockRing::peek
//! [`touch`]: ClockRing::touch
//! [`remove`]: ClockRing::remove
//! [`pop_victim`]: ClockRing::pop_victim
//! [`peek_victim`]: ClockRing::peek_victim
//! [`iter`]: ClockRing::iter
//! [`keys`]: ClockRing::keys
//! [`values`]: ClockRing::values
//! [`into_iter`]: ClockRing#impl-IntoIterator
//!
//! ## Use Cases
//!
//! - **Page replacement**: Classic use case for CLOCK algorithm
//! - **Buffer pool**: Database buffer management
//! - **Web cache**: Approximate LRU with lower overhead
//!
//! ## Example Usage
//!
//! ```
//! use cachekit::ds::ClockRing;
//!
//! let mut cache = ClockRing::new(3);
//!
//! // Insert entries
//! cache.insert("page_1", "content_1");
//! cache.insert("page_2", "content_2");
//! cache.insert("page_3", "content_3");
//!
//! // Access sets the reference bit (second chance)
//! cache.get(&"page_1"); // page_1 now has ref=1
//!
//! // Insert when full - evicts unreferenced entry
//! let evicted = cache.insert("page_4", "content_4");
//! // page_2 or page_3 evicted (page_1 protected by ref bit)
//!
//! assert!(cache.contains(&"page_1")); // Survived due to reference bit
//! assert!(cache.contains(&"page_4")); // Newly inserted
//! ```
//!
//! ## Use Case: Page Buffer
//!
//! ```
//! use cachekit::ds::ClockRing;
//!
//! struct PageBuffer {
//! cache: ClockRing<u64, Vec<u8>>, // page_id -> page_data
//! }
//!
//! impl PageBuffer {
//! fn new(capacity: usize) -> Self {
//! Self { cache: ClockRing::new(capacity) }
//! }
//!
//! fn read_page(&mut self, page_id: u64) -> Option<&Vec<u8>> {
//! // get() sets reference bit, giving this page a second chance
//! self.cache.get(&page_id)
//! }
//!
//! fn load_page(&mut self, page_id: u64, data: Vec<u8>) {
//! if let Some((evicted_id, _evicted_data)) = self.cache.insert(page_id, data) {
//! // Could write back dirty page here
//! println!("Evicted page {}", evicted_id);
//! }
//! }
//! }
//!
//! let mut buffer = PageBuffer::new(100);
//! buffer.load_page(1, vec![0; 4096]);
//! buffer.load_page(2, vec![0; 4096]);
//! assert!(buffer.read_page(1).is_some());
//! ```
//!
//! ## Thread Safety
//!
//! - [`ClockRing`]: Not thread-safe, use in single-threaded contexts
//! - [`ConcurrentClockRing`]: Thread-safe via `parking_lot::RwLock`
//!
//! ## Implementation Notes
//!
//! - Fixed-size slot array; no reallocation during operation
//! - Reference bits stored in a separate `Vec<bool>` for cache-friendly sweeps
//! - Keys mapped to slot indices via [`std::collections::HashMap`]; the default
//! DoS-resistant [`RandomState`] hasher is used unless the caller opts into
//! another via [`ClockRing::with_hasher`]. Key is cloned once per new insertion.
//! - Hand pointer advances after each insert/eviction
//! - [`insert`] is O(1) amortized: each access sets at most one ref bit, so the
//! total clearing work across N inserts is bounded by N
//! - `debug_validate_invariants()` available in debug/test builds
//!
//! ## Memory Budgeting
//!
//! `ClockRing` bounds the *number* of entries (`capacity`), not the
//! aggregate byte footprint of stored values. [`ClockRing::approx_bytes`]
//! reports only the in-line cost of `Option<Entry<K, V>>` slots plus
//! the index map; it counts `size_of::<V>()` and **does not follow
//! heap pointers owned by `V`**. A ring of `Vec<u8>` or `String` values
//! can therefore consume orders of magnitude more memory than
//! `approx_bytes` suggests.
//!
//! For workloads with variable-sized values, enforce a byte budget
//! at the call site (e.g. track `value.len()` on insert and evict via
//! [`ClockRing::pop_victim`] until under budget) rather than relying
//! on slot count alone.
//!
//! ## Timing Side Channels
//!
//! Lookup methods ([`contains`](ClockRing::contains),
//! [`peek`](ClockRing::peek), [`get`](ClockRing::get),
//! [`touch`](ClockRing::touch), …) take time that depends on whether
//! the key is present and on the hasher's probe chain length. This is
//! standard for any [`HashMap`]-backed cache and is **not** mitigated
//! here — the default [`RandomState`] randomizes the probe chain
//! across processes (defeating cross-process collision attacks) but
//! does not produce constant-time lookups.
//!
//! Do not use `ClockRing` as the backing store for data structures
//! whose key set must remain confidential against a co-located
//! attacker that can measure operation latency (e.g. session-token
//! caches in a multi-tenant process where the tenant boundary is the
//! key space). For such use cases, gate access behind an explicit
//! constant-time membership check or use a constant-time data
//! structure.
//!
//! [`RandomState`]: std::collections::hash_map::RandomState
//! [`HashMap`]: std::collections::HashMap
use std::borrow::Borrow;
use std::collections::HashMap;
use std::collections::hash_map::RandomState;
use std::hash::{BuildHasher, Hash};
#[cfg(feature = "concurrency")]
use parking_lot::RwLock;
/// Coarse upper bound on `capacity` accepted by [`ClockRing::new`] /
/// [`ClockRing::try_new`].
///
/// This is a *first* guard that rejects obviously pathological values
/// cheaply. It is intentionally permissive: for any key/value size
/// larger than a few bytes, allocating `MAX_CAPACITY` slots would still
/// exhaust memory. The constructors defend against that separately by
/// using [`Vec::try_reserve_exact`] / [`HashMap::try_reserve`], so
/// out-of-memory conditions surface as
/// [`ClockRingError::AllocationFailed`] rather than aborting the
/// process.
pub const MAX_CAPACITY: usize = isize::MAX as usize / 64;
/// Error returned by [`ClockRing::try_new`] / [`ClockRing::try_with_hasher`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClockRingError {
/// The requested capacity exceeds [`MAX_CAPACITY`].
CapacityTooLarge {
/// The capacity that was requested.
requested: usize,
/// The configured upper bound.
max: usize,
},
/// The allocator could not satisfy the per-slot reservation for the
/// requested capacity.
///
/// Returned instead of aborting the process when
/// `capacity * size_of::<Option<Entry<K, V>>>()` exceeds what the
/// allocator can provide (including the case where the byte count
/// overflows `isize::MAX`).
AllocationFailed {
/// The capacity whose allocation failed.
requested: usize,
},
}
impl std::fmt::Display for ClockRingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClockRingError::CapacityTooLarge { requested, max } => {
write!(f, "ClockRing capacity {requested} exceeds maximum {max}")
},
ClockRingError::AllocationFailed { requested } => {
write!(
f,
"ClockRing failed to allocate backing storage for capacity {requested}"
)
},
}
}
}
impl std::error::Error for ClockRingError {}
/// Acknowledges that the caller is supplying a hasher to
/// [`ClockRing::with_hasher`] / [`ClockRing::try_with_hasher`] (and
/// the [`ConcurrentClockRing`] equivalents) under the assumption that
/// keys are **not attacker-controlled**.
///
/// Required at every call site as a deliberate, code-reviewable
/// signal: a non-randomized hasher (e.g. `rustc_hash::FxBuildHasher`,
/// unseeded `ahash::AHasher`, FNV) reintroduces hash-collision DoS —
/// adversarial keys can degrade [`HashMap`]-backed lookups to O(n)
/// regardless of the CLOCK sweep cost.
///
/// Acceptable inputs to a `KeysAreTrusted`-tagged ring include:
/// authenticated user/row IDs, opaque handles minted internally,
/// values already passed through a keyed hash (HMAC), or any source
/// the application's threat model treats as integrity-protected.
/// For attacker-controlled keys, use [`ClockRing::new`] /
/// [`ClockRing::try_new`], which default to the DoS-resistant
/// [`RandomState`] hasher.
///
/// # Example
///
/// ```
/// use cachekit::ds::{ClockRing, KeysAreTrusted};
/// use std::collections::hash_map::RandomState;
///
/// let ring: ClockRing<u64, &str> =
/// ClockRing::with_hasher(64, RandomState::new(), KeysAreTrusted::new());
/// assert_eq!(ring.capacity(), 64);
/// ```
///
/// [`HashMap`]: std::collections::HashMap
/// [`RandomState`]: std::collections::hash_map::RandomState
#[derive(Debug, Clone, Copy, Default)]
pub struct KeysAreTrusted(());
impl KeysAreTrusted {
/// Constructs the acknowledgement.
///
/// See the [type-level docs](KeysAreTrusted) for the security
/// implications of passing this to a hasher-configurable
/// constructor.
#[inline]
pub const fn new() -> Self {
Self(())
}
}
/// Advance an index by one position modulo `cap`, overflow-safe.
///
/// Using `checked_add` + fallback to `wrapping_add % cap` avoids the
/// `(idx + 1) % cap` overflow that is theoretically reachable on
/// pathologically large `cap` values.
///
/// Returns `0` when `cap == 0` so that the function is total: current
/// callers all guard on non-zero capacity, but making `step` itself
/// safe to call with any input prevents a release-mode division-by-zero
/// panic if a future call site forgets the guard.
#[inline]
fn step(idx: usize, cap: usize) -> usize {
if cap == 0 {
return 0;
}
match idx.checked_add(1) {
Some(next) => next % cap,
None => 0,
}
}
/// Clock entry holding key and value.
///
/// Reference bits are stored separately in [`ClockRing::referenced`] for
/// cache-friendly sweep access.
#[derive(Debug, Clone)]
struct Entry<K, V> {
key: K,
value: V,
}
/// Fixed-size ring implementing the CLOCK (second-chance) eviction algorithm.
///
/// Provides O(1) amortized insertion with automatic eviction of unreferenced
/// entries. Accessed entries receive a "second chance" via a reference bit.
///
/// Lookup methods ([`get`](Self::get), [`peek`](Self::peek),
/// [`contains`](Self::contains), etc.) accept any borrowed form of the key
/// via [`Borrow`], so a `ClockRing<String, V>` can be
/// queried with `&str`.
///
/// Implements [`Clone`], [`Debug`], [`Extend`]`<(K, V)>`, and
/// [`IntoIterator`]. See [`iter`](Self::iter), [`keys`](Self::keys),
/// [`values`](Self::values) for borrowed iteration.
///
/// # Type Parameters
///
/// - `K`: Key type, must be `Eq + Hash + Clone`
/// - `V`: Value type
/// - `S`: Hash builder. Defaults to [`RandomState`], a DoS-resistant
/// randomized hasher. Swap via [`ClockRing::with_hasher`] for a faster
/// non-randomized hasher when keys are fully trusted.
///
/// # Hash-collision resistance
///
/// `ClockRing` defaults to the standard library's randomized
/// [`RandomState`]. This makes it safe to use as a cache keyed by
/// attacker-controlled data (URL paths, user IDs, header values, etc.)
/// without worrying about adversarial hash collisions degrading lookups
/// to O(n). Callers that fully trust their key source can opt into a
/// faster non-randomized hasher (e.g. `rustc_hash::FxBuildHasher`) via
/// [`ClockRing::with_hasher`] / [`ClockRing::try_with_hasher`], which
/// require a [`KeysAreTrusted`] acknowledgement to make the trade-off
/// visible at every call site.
///
/// # Example
///
/// ```
/// use cachekit::ds::ClockRing;
///
/// let mut ring = ClockRing::new(2);
///
/// // Insert two entries
/// ring.insert("a", 1);
/// ring.insert("b", 2);
///
/// // Access "a" - sets reference bit
/// ring.get(&"a");
///
/// // Insert "c" - "b" is evicted (no reference bit)
/// let evicted = ring.insert("c", 3);
/// assert_eq!(evicted, Some(("b", 2)));
///
/// assert!(ring.contains(&"a"));
/// assert!(ring.contains(&"c"));
/// ```
///
/// # Use Case: Simple Cache with Eviction Callback
///
/// ```
/// use cachekit::ds::ClockRing;
///
/// let mut cache = ClockRing::new(3);
/// let mut eviction_count = 0;
///
/// for i in 0..10 {
/// if let Some((_key, _value)) = cache.insert(i, format!("value_{}", i)) {
/// eviction_count += 1;
/// }
/// }
///
/// assert_eq!(cache.len(), 3);
/// assert_eq!(eviction_count, 7); // 10 inserts - 3 capacity = 7 evictions
/// ```
#[must_use]
pub struct ClockRing<K, V, S = RandomState> {
slots: Vec<Option<Entry<K, V>>>,
referenced: Vec<bool>,
index: HashMap<K, usize, S>,
hand: usize,
len: usize,
#[cfg(feature = "metrics")]
sweep_hand_advances: u64,
#[cfg(feature = "metrics")]
sweep_ref_bit_resets: u64,
}
/// Hand-written so stored keys and values never appear in debug
/// output. A derived `Debug` would recurse through every `Entry<K,
/// V>` and `HashMap<K, usize, S>` entry, making `tracing::debug!`,
/// `dbg!`, and panic-unwind backtraces into a secret-exposure channel
/// for caches that hold session tokens, API keys, or any other
/// sensitive material. Callers that need full contents can iterate
/// via [`ClockRing::iter`] and print entries they've vetted.
impl<K, V, S> std::fmt::Debug for ClockRing<K, V, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut dbg = f.debug_struct("ClockRing");
dbg.field("len", &self.len)
.field("capacity", &self.slots.len())
.field("hand", &self.hand);
#[cfg(feature = "metrics")]
{
dbg.field("sweep_hand_advances", &self.sweep_hand_advances)
.field("sweep_ref_bit_resets", &self.sweep_ref_bit_resets);
}
dbg.finish_non_exhaustive()
}
}
/// Thread-safe wrapper around [`ClockRing`] using `parking_lot::RwLock`.
///
/// Provides the same functionality as [`ClockRing`] but safe for concurrent
/// access. Uses closure-based value access since references cannot outlive
/// lock guards. Lookup methods accept any borrowed form of the key via
/// [`Borrow`].
///
/// # Example
///
/// ```
/// use std::sync::Arc;
/// use std::thread;
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = Arc::new(ConcurrentClockRing::new(100));
///
/// let handles: Vec<_> = (0..4).map(|t| {
/// let cache = Arc::clone(&cache);
/// thread::spawn(move || {
/// for i in 0..25 {
/// let key = t * 25 + i;
/// cache.insert(key, key * 10);
/// }
/// })
/// }).collect();
///
/// for h in handles {
/// h.join().unwrap();
/// }
///
/// assert!(cache.len() <= 100);
/// ```
///
/// # Iteration
///
/// `ConcurrentClockRing` does not directly expose iterators (holding
/// a lock for the duration of iteration would hurt concurrency). Call
/// [`into_inner`](Self::into_inner) to unwrap the inner [`ClockRing`]
/// and iterate it.
///
/// # Non-blocking Operations
///
/// All operations have `try_*` variants that return `None` if the lock
/// cannot be acquired immediately:
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(10);
/// cache.insert("key", 42);
///
/// // Non-blocking read
/// if let Some(Some(val)) = cache.try_get_with(&"key", |v| *v) {
/// assert_eq!(val, 42);
/// }
/// ```
///
/// # Closures run under the lock
///
/// Every `*_with` / `try_*_with` method invokes the caller-supplied
/// closure **while holding the internal `RwLock` guard**. This has
/// important consequences:
///
/// - The closure **must not** call back into the same
/// `ConcurrentClockRing` (or any structure that might). The lock is
/// *not* reentrant, so re-entry will either deadlock (`*_with`) or
/// spuriously fail by returning `None` (`try_*_with`).
/// - A slow closure stalls every other reader/writer of this cache.
/// Keep closures short; clone data out if you need to perform I/O
/// or further work.
/// - `get_with` / `get_mut_with` / `touch` / `update` /
/// `remove` / `pop_victim` and their `try_*` variants hold the
/// **write** lock across the closure because they mutate the
/// reference bits; `peek_with` / `peek_victim_with` hold the read
/// lock.
/// - If the closure panics the lock is released cleanly
/// (`parking_lot::RwLock` does not poison), but any observable state
/// changes performed *before* the closure (e.g. setting the
/// reference bit in `get_with`) are retained.
///
/// # `Drop for V` runs outside the lock
///
/// [`insert`](Self::insert) / [`try_insert`](Self::try_insert) release
/// the internal write lock before dropping the value replaced by an
/// update (and the caller is responsible for dropping the evicted
/// entry after the method returns). This keeps an expensive or
/// reentrant `Drop for V` from stalling other threads or deadlocking
/// by calling back into the same cache. The same is true for
/// [`remove`](Self::remove): the returned value is dropped on the
/// caller's side of the lock.
#[cfg(feature = "concurrency")]
#[must_use]
pub struct ConcurrentClockRing<K, V, S = RandomState> {
inner: RwLock<ClockRing<K, V, S>>,
}
/// See the [`ClockRing`] `Debug` impl for the rationale: stored keys
/// and values are redacted. Acquires the inner read lock, so calling
/// `{:?}` under a held write guard will deadlock on the same thread
/// that holds the guard — matching the behavior of
/// `RwLock::read()` elsewhere in this module.
#[cfg(feature = "concurrency")]
impl<K, V, S> std::fmt::Debug for ConcurrentClockRing<K, V, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let inner = self.inner.read();
let mut dbg = f.debug_struct("ConcurrentClockRing");
dbg.field("len", &inner.len)
.field("capacity", &inner.slots.len())
.field("hand", &inner.hand);
#[cfg(feature = "metrics")]
{
dbg.field("sweep_hand_advances", &inner.sweep_hand_advances)
.field("sweep_ref_bit_resets", &inner.sweep_ref_bit_resets);
}
dbg.finish_non_exhaustive()
}
}
#[cfg(feature = "concurrency")]
impl<K, V> ConcurrentClockRing<K, V, RandomState>
where
K: Eq + Hash + Clone,
{
/// Creates a new ring with `capacity` slots and the default
/// DoS-resistant hasher ([`RandomState`]).
///
/// # Panics
///
/// Panics if `capacity > MAX_CAPACITY`. Use
/// [`try_new`](Self::try_new) for a fallible version.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache: ConcurrentClockRing<String, i32> = ConcurrentClockRing::new(100);
/// assert_eq!(cache.capacity(), 100);
/// assert!(cache.is_empty());
/// ```
pub fn new(capacity: usize) -> Self {
Self {
inner: RwLock::new(ClockRing::new(capacity)),
}
}
/// Fallible version of [`new`](Self::new).
///
/// Returns [`ClockRingError::CapacityTooLarge`] if
/// `capacity > MAX_CAPACITY`.
pub fn try_new(capacity: usize) -> Result<Self, ClockRingError> {
Ok(Self {
inner: RwLock::new(ClockRing::try_new(capacity)?),
})
}
}
#[cfg(feature = "concurrency")]
impl<K, V, S> ConcurrentClockRing<K, V, S>
where
K: Eq + Hash + Clone,
S: BuildHasher,
{
/// Creates a new ring with `capacity` slots using the provided
/// `hash_builder`.
///
/// The [`KeysAreTrusted`] argument is a deliberate acknowledgement
/// that a non-randomized hasher is safe for this workload — see
/// its type-level docs for the DoS trade-off. Use
/// [`new`](Self::new) / [`try_new`](Self::try_new) for the default
/// DoS-resistant [`RandomState`] hasher.
///
/// # Panics
///
/// Panics if `capacity > MAX_CAPACITY`. Use
/// [`try_with_hasher`](Self::try_with_hasher) for a fallible version.
pub fn with_hasher(capacity: usize, hash_builder: S, ack: KeysAreTrusted) -> Self {
Self {
inner: RwLock::new(ClockRing::with_hasher(capacity, hash_builder, ack)),
}
}
/// Fallible version of [`with_hasher`](Self::with_hasher).
///
/// The [`KeysAreTrusted`] argument is a deliberate acknowledgement
/// that a non-randomized hasher is safe for this workload — see
/// its type-level docs for the DoS trade-off.
pub fn try_with_hasher(
capacity: usize,
hash_builder: S,
ack: KeysAreTrusted,
) -> Result<Self, ClockRingError> {
Ok(Self {
inner: RwLock::new(ClockRing::try_with_hasher(capacity, hash_builder, ack)?),
})
}
/// Returns the configured capacity (number of slots).
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache: ConcurrentClockRing<&str, i32> = ConcurrentClockRing::new(50);
/// assert_eq!(cache.capacity(), 50);
/// ```
pub fn capacity(&self) -> usize {
let ring = self.inner.read();
ring.capacity()
}
/// Returns the number of occupied slots.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(10);
/// assert_eq!(cache.len(), 0);
///
/// cache.insert("a", 1);
/// cache.insert("b", 2);
/// assert_eq!(cache.len(), 2);
/// ```
pub fn len(&self) -> usize {
let ring = self.inner.read();
ring.len()
}
/// Returns `true` if there are no entries.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache: ConcurrentClockRing<&str, i32> = ConcurrentClockRing::new(10);
/// assert!(cache.is_empty());
///
/// cache.insert("key", 42);
/// assert!(!cache.is_empty());
/// ```
pub fn is_empty(&self) -> bool {
let ring = self.inner.read();
ring.is_empty()
}
/// Returns `true` if `key` is present.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(10);
/// cache.insert("key", 42);
///
/// assert!(cache.contains(&"key"));
/// assert!(!cache.contains(&"missing"));
/// ```
pub fn contains<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let ring = self.inner.read();
ring.contains(key)
}
/// Accesses value without setting the reference bit.
///
/// Unlike [`get_with`](Self::get_with), this does not grant a second chance.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(10);
/// cache.insert("key", vec![1, 2, 3]);
///
/// let sum = cache.peek_with(&"key", |v| v.iter().sum::<i32>());
/// assert_eq!(sum, Some(6));
/// ```
pub fn peek_with<Q, R>(&self, key: &Q, f: impl FnOnce(&V) -> R) -> Option<R>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let ring = self.inner.read();
ring.peek(key).map(f)
}
/// Accesses value and sets the reference bit (grants second chance).
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(2);
/// cache.insert("a", 1);
/// cache.insert("b", 2);
///
/// // Access "a" - sets reference bit
/// cache.get_with(&"a", |v| *v);
///
/// // Insert "c" - "b" evicted (no ref bit), "a" survives
/// cache.insert("c", 3);
/// assert!(cache.contains(&"a"));
/// assert!(!cache.contains(&"b"));
/// ```
pub fn get_with<Q, R>(&self, key: &Q, f: impl FnOnce(&V) -> R) -> Option<R>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let mut ring = self.inner.write();
ring.get(key).map(f)
}
/// Accesses value mutably and sets the reference bit (grants second chance).
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(10);
/// cache.insert("key", vec![1, 2, 3]);
///
/// cache.get_mut_with(&"key", |v| v.push(4));
/// let sum = cache.peek_with(&"key", |v| v.iter().sum::<i32>());
/// assert_eq!(sum, Some(10));
/// ```
pub fn get_mut_with<Q, R>(&self, key: &Q, f: impl FnOnce(&mut V) -> R) -> Option<R>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let mut ring = self.inner.write();
ring.get_mut(key).map(f)
}
/// Sets the reference bit for `key`; returns `false` if missing.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(10);
/// cache.insert("key", 42);
///
/// assert!(cache.touch(&"key")); // Sets reference bit
/// assert!(!cache.touch(&"missing")); // Key not found
/// ```
pub fn touch<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let mut ring = self.inner.write();
ring.touch(key)
}
/// Updates the value for an existing key, returning the old value.
///
/// Sets the reference bit on update. Returns `None` if the key doesn't exist.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(2);
/// cache.insert("a", 1);
///
/// assert_eq!(cache.update(&"a", 10), Some(1));
/// assert_eq!(cache.update(&"missing", 99), None);
/// ```
pub fn update<Q>(&self, key: &Q, value: V) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let mut ring = self.inner.write();
ring.update(key, value)
}
/// Inserts or updates `key`, evicting if necessary.
///
/// Returns `Some((evicted_key, evicted_value))` if an entry was evicted.
///
/// Any value replaced by an **update** (same key already present)
/// is dropped *after* the internal write lock has been released, so
/// a slow or reentrant `Drop for V` cannot stall other
/// readers/writers or deadlock by calling back into this cache.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(2);
/// assert_eq!(cache.insert("a", 1), None); // No eviction
/// assert_eq!(cache.insert("b", 2), None); // No eviction
///
/// // Full - must evict
/// let evicted = cache.insert("c", 3);
/// assert!(evicted.is_some());
/// ```
pub fn insert(&self, key: K, value: V) -> Option<(K, V)> {
let (replaced, evicted) = {
let mut ring = self.inner.write();
ring.insert_swap(key, value)
};
drop(replaced);
evicted
}
/// Removes `key` and returns its value, if present.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(10);
/// cache.insert("key", 42);
///
/// assert_eq!(cache.remove(&"key"), Some(42));
/// assert_eq!(cache.remove(&"key"), None); // Already removed
/// ```
pub fn remove<Q>(&self, key: &Q) -> Option<V>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
let mut ring = self.inner.write();
ring.remove(key)
}
/// Peeks the next eviction candidate without modifying state.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(2);
/// cache.insert("a", 1);
/// cache.insert("b", 2);
/// cache.touch(&"a"); // "a" now has ref bit
///
/// // "b" is next victim (no ref bit)
/// let victim_key = cache.peek_victim_with(|k, _v| *k);
/// assert_eq!(victim_key, Some("b"));
/// ```
pub fn peek_victim_with<R>(&self, f: impl FnOnce(&K, &V) -> R) -> Option<R> {
let ring = self.inner.read();
ring.peek_victim().map(|(key, value)| f(key, value))
}
/// Evicts the next candidate and returns it.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(3);
/// cache.insert("a", 1);
/// cache.insert("b", 2);
/// cache.insert("c", 3);
///
/// let evicted = cache.pop_victim();
/// assert!(evicted.is_some());
/// assert_eq!(cache.len(), 2);
/// ```
pub fn pop_victim(&self) -> Option<(K, V)> {
let mut ring = self.inner.write();
ring.pop_victim()
}
/// Clears all entries without releasing capacity.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(10);
/// cache.insert("a", 1);
/// cache.insert("b", 2);
///
/// cache.clear();
/// assert!(cache.is_empty());
/// ```
pub fn clear(&self) {
let mut ring = self.inner.write();
ring.clear();
}
/// Clears all entries and shrinks internal storage.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache = ConcurrentClockRing::new(100);
/// cache.insert("a", 1);
///
/// cache.clear_shrink();
/// assert!(cache.is_empty());
/// ```
pub fn clear_shrink(&self) {
let mut ring = self.inner.write();
ring.clear_shrink();
}
/// Returns an approximate memory footprint in bytes.
///
/// # Example
///
/// ```
/// use cachekit::ds::ConcurrentClockRing;
///
/// let cache: ConcurrentClockRing<u64, u64> = ConcurrentClockRing::new(100);
/// assert!(cache.approx_bytes() > 0);
/// ```