Skip to main content

kvbm_engine/offload/
pending.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Pending transfer tracking for duplicate prevention.
5//!
6//! This module provides `PendingTracker` and `PendingGuard` types that work together
7//! to track blocks that are currently in-flight through the transfer pipeline.
8//!
9//! # Problem
10//!
11//! When overlapping sequences are enqueued for transfer at roughly the same time,
12//! the presence policy may allow duplicate transfers because:
13//! - The first sequence's blocks haven't completed registration yet
14//! - The second sequence sees the same blocks as "not present"
15//!
16//! # Solution
17//!
18//! The `PendingTracker` maintains a set of sequence hashes currently in the pipeline.
19//! When blocks pass policy evaluation, a `PendingGuard` is created that:
20//! - Adds the sequence hash to the pending set on creation
21//! - Automatically removes it on drop (RAII pattern)
22//!
23//! The `PresenceFilter` can then check both the registry (completed transfers)
24//! AND the pending set (in-flight transfers) to avoid duplicates.
25//!
26//! # Example
27//!
28//! ```ignore
29//! let tracker = Arc::new(PendingTracker::new());
30//!
31//! // Create guard when block passes policy
32//! let guard = tracker.guard(sequence_hash);
33//!
34//! // Guard travels with block through pipeline stages
35//! queued_block.pending_guard = Some(guard);
36//!
37//! // When block completes or is cancelled, guard is dropped
38//! // and hash is automatically removed from pending set
39//! ```
40
41use std::sync::Arc;
42
43use dashmap::DashSet;
44
45use crate::SequenceHash;
46
47/// Tracks sequence hashes that are currently pending transfer.
48///
49/// This is shared between the pipeline and the presence policy via `Arc`.
50/// Thread-safe for concurrent access from multiple pipeline stages.
51#[derive(Debug, Default)]
52pub struct PendingTracker {
53    pending: DashSet<SequenceHash>,
54}
55
56impl PendingTracker {
57    /// Create a new empty pending tracker.
58    pub fn new() -> Self {
59        Self {
60            pending: DashSet::new(),
61        }
62    }
63
64    /// Check if a sequence hash is currently pending transfer.
65    ///
66    /// Used by `PresenceFilter` to skip blocks that are already in-flight.
67    pub fn is_pending(&self, hash: &SequenceHash) -> bool {
68        self.pending.contains(hash)
69    }
70
71    /// Get the number of pending transfers.
72    ///
73    /// Useful for metrics and debugging.
74    pub fn len(&self) -> usize {
75        self.pending.len()
76    }
77
78    /// Check if there are no pending transfers.
79    pub fn is_empty(&self) -> bool {
80        self.pending.is_empty()
81    }
82
83    /// Create a guard that marks a sequence hash as pending until dropped.
84    ///
85    /// The guard uses RAII to ensure the hash is removed when:
86    /// - Transfer completes successfully
87    /// - Transfer is cancelled
88    /// - Block is evicted from pipeline
89    /// - Any error causes the block to be dropped
90    pub fn guard(self: &Arc<Self>, hash: SequenceHash) -> PendingGuard {
91        self.pending.insert(hash);
92        PendingGuard {
93            hash,
94            tracker: Arc::clone(self),
95        }
96    }
97}
98
99/// Extension trait for `Option<Arc<PendingTracker>>` to simplify pending checks.
100///
101/// Reduces the common pattern `self.pending_tracker.as_ref().is_some_and(|t| t.is_pending(&hash))`
102/// to a single method call.
103pub(crate) trait PendingCheck {
104    fn is_hash_pending(&self, hash: &SequenceHash) -> bool;
105}
106
107impl PendingCheck for Option<Arc<PendingTracker>> {
108    fn is_hash_pending(&self, hash: &SequenceHash) -> bool {
109        self.as_ref().is_some_and(|t| t.is_pending(hash))
110    }
111}
112
113/// RAII guard that removes a sequence hash from the pending set on drop.
114///
115/// This guard travels with the block through all pipeline stages and ensures
116/// cleanup happens automatically regardless of how the transfer completes.
117///
118/// # Clone Behavior
119///
120/// Cloning a `PendingGuard` is cheap (Arc clone) but does NOT create a new
121/// pending entry. The hash is only inserted once when the first guard is
122/// created, and removed when ALL clones are dropped.
123///
124/// However, the current implementation removes on first drop, so cloning
125/// should be avoided unless you understand the implications.
126pub struct PendingGuard {
127    hash: SequenceHash,
128    tracker: Arc<PendingTracker>,
129}
130
131impl PendingGuard {
132    /// Get the sequence hash this guard is tracking.
133    pub fn sequence_hash(&self) -> SequenceHash {
134        self.hash
135    }
136}
137
138impl Drop for PendingGuard {
139    fn drop(&mut self) {
140        self.tracker.pending.remove(&self.hash);
141    }
142}
143
144impl std::fmt::Debug for PendingGuard {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        f.debug_struct("PendingGuard")
147            .field("sequence_hash", &self.hash)
148            .finish()
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    /// Helper to create a test SequenceHash with unique values.
157    fn test_hash(id: u64) -> SequenceHash {
158        SequenceHash::new(id, Some(0), id)
159    }
160
161    #[test]
162    fn test_pending_tracker_new() {
163        let tracker = PendingTracker::new();
164        assert!(tracker.is_empty());
165        assert_eq!(tracker.len(), 0);
166    }
167
168    #[test]
169    fn test_pending_guard_inserts_and_removes() {
170        let tracker = Arc::new(PendingTracker::new());
171        let hash = test_hash(12345);
172
173        assert!(!tracker.is_pending(&hash));
174
175        {
176            let _guard = tracker.guard(hash);
177            assert!(tracker.is_pending(&hash));
178            assert_eq!(tracker.len(), 1);
179        }
180
181        // Guard dropped, hash should be removed
182        assert!(!tracker.is_pending(&hash));
183        assert!(tracker.is_empty());
184    }
185
186    #[test]
187    fn test_multiple_guards_different_hashes() {
188        let tracker = Arc::new(PendingTracker::new());
189        let hash1 = test_hash(111);
190        let hash2 = test_hash(222);
191        let hash3 = test_hash(333);
192
193        let guard1 = tracker.guard(hash1);
194        let guard2 = tracker.guard(hash2);
195
196        assert!(tracker.is_pending(&hash1));
197        assert!(tracker.is_pending(&hash2));
198        assert!(!tracker.is_pending(&hash3));
199        assert_eq!(tracker.len(), 2);
200
201        drop(guard1);
202        assert!(!tracker.is_pending(&hash1));
203        assert!(tracker.is_pending(&hash2));
204        assert_eq!(tracker.len(), 1);
205
206        drop(guard2);
207        assert!(tracker.is_empty());
208    }
209
210    #[test]
211    fn test_guard_sequence_hash_accessor() {
212        let tracker = Arc::new(PendingTracker::new());
213        let hash = test_hash(42);
214
215        let guard = tracker.guard(hash);
216        assert_eq!(guard.sequence_hash(), hash);
217    }
218
219    #[test]
220    fn test_tracker_debug() {
221        let tracker = PendingTracker::new();
222        let debug_str = format!("{:?}", tracker);
223        assert!(debug_str.contains("PendingTracker"));
224    }
225
226    #[test]
227    fn test_guard_debug() {
228        let tracker = Arc::new(PendingTracker::new());
229        let hash = test_hash(999);
230        let guard = tracker.guard(hash);
231
232        let debug_str = format!("{:?}", guard);
233        assert!(debug_str.contains("PendingGuard"));
234        assert!(debug_str.contains("sequence_hash"));
235    }
236
237    #[test]
238    fn test_concurrent_access_to_same_hash() {
239        // Test that the same hash being added twice is handled correctly
240        let tracker = Arc::new(PendingTracker::new());
241        let hash = test_hash(555);
242
243        // First guard marks it as pending
244        let guard1 = tracker.guard(hash);
245        assert!(tracker.is_pending(&hash));
246        assert_eq!(tracker.len(), 1);
247
248        // Second guard for same hash - DashSet.insert returns false if already present
249        // but our guard() always inserts (doesn't check first)
250        let guard2 = tracker.guard(hash);
251        assert!(tracker.is_pending(&hash));
252        // DashSet deduplicates, so len is still 1
253        assert_eq!(tracker.len(), 1);
254
255        // Drop first guard - hash removed from set
256        drop(guard1);
257        // DashSet now doesn't have the hash
258        assert!(!tracker.is_pending(&hash));
259
260        // Second guard still exists but hash was already removed
261        // This is expected behavior - the RAII ensures cleanup on any drop
262        drop(guard2);
263        assert!(tracker.is_empty());
264    }
265}