Skip to main content

kvbm_engine/offload/
cancel.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Cancellation protocol for offload transfers.
5//!
6//! The cancellation protocol ensures clean release of all blocks with confirmation
7//! that no outstanding operations remain:
8//!
9//! 1. `cancel()` called → sets `CancelState::Requested`
10//! 2. Each stage checks at safe points (between items, not during ops)
11//! 3. If in-flight ops: `CancelState::Draining` → wait for completion
12//! 4. Drop all `ImmutableBlock` guards → blocks released
13//! 5. `CancelState::Confirmed` → `CancelConfirmation` resolves
14
15use std::sync::Arc;
16
17use tokio::sync::watch;
18
19/// State of a cancellation request.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum CancelState {
22    /// Transfer is active, not cancelled
23    Active,
24    /// Cancel requested, waiting for checkpoint
25    Requested,
26    /// Draining in-flight operations
27    Draining {
28        /// Number of in-flight operations remaining
29        in_flight: usize,
30    },
31    /// All operations complete, blocks released, confirmed
32    Confirmed,
33}
34
35impl CancelState {
36    /// Check if cancellation has been requested (including draining/confirmed states).
37    pub fn is_cancelled(&self) -> bool {
38        !matches!(self, CancelState::Active)
39    }
40
41    /// Check if we're in the draining phase.
42    pub fn is_draining(&self) -> bool {
43        matches!(self, CancelState::Draining { .. })
44    }
45
46    /// Check if cancellation is fully confirmed.
47    pub fn is_confirmed(&self) -> bool {
48        matches!(self, CancelState::Confirmed)
49    }
50}
51
52/// Token for requesting and tracking cancellation.
53///
54/// The token is shared between the `TransferHandle` (user-facing) and the
55/// pipeline stages (internal). When `request()` is called, stages will
56/// check at safe points and transition through draining to confirmed.
57#[derive(Clone)]
58pub struct CancellationToken {
59    /// Sender for cancellation requests
60    request_tx: Arc<watch::Sender<bool>>,
61    /// Receiver for cancel state updates
62    state_rx: watch::Receiver<CancelState>,
63}
64
65impl CancellationToken {
66    /// Create a new cancellation token pair.
67    ///
68    /// Returns `(token, state_tx)` where:
69    /// - `token`: Clone and give to TransferHandle for user access
70    /// - `state_tx`: Keep in pipeline for updating state
71    pub fn new() -> (Self, CancelStateUpdater) {
72        let (request_tx, request_rx) = watch::channel(false);
73        let (state_tx, state_rx) = watch::channel(CancelState::Active);
74
75        let token = CancellationToken {
76            request_tx: Arc::new(request_tx),
77            state_rx,
78        };
79
80        let updater = CancelStateUpdater {
81            request_rx,
82            state_tx,
83        };
84
85        (token, updater)
86    }
87
88    /// Request cancellation.
89    ///
90    /// This signals all pipeline stages to stop processing at the next safe point.
91    /// Returns immediately - use `wait_confirmed()` to await full confirmation.
92    pub fn request(&self) {
93        let _ = self.request_tx.send(true);
94    }
95
96    /// Check if cancellation has been requested.
97    pub fn is_requested(&self) -> bool {
98        *self.request_tx.borrow()
99    }
100
101    /// Get the current cancellation state.
102    pub fn state(&self) -> CancelState {
103        *self.state_rx.borrow()
104    }
105
106    /// Check if cancellation is fully confirmed.
107    pub fn is_confirmed(&self) -> bool {
108        self.state().is_confirmed()
109    }
110
111    /// Create a future that resolves when cancellation is confirmed.
112    ///
113    /// This is the primary way to await clean release of all blocks.
114    pub fn wait_confirmed(&self) -> CancelConfirmation {
115        CancelConfirmation {
116            state_rx: self.state_rx.clone(),
117        }
118    }
119}
120
121/// Internal updater for cancellation state.
122///
123/// Held by pipeline stages to update state and check for cancel requests.
124pub struct CancelStateUpdater {
125    /// Receiver for cancellation requests
126    request_rx: watch::Receiver<bool>,
127    /// Sender for state updates
128    state_tx: watch::Sender<CancelState>,
129}
130
131impl CancelStateUpdater {
132    /// Check if cancellation has been requested.
133    pub fn is_requested(&self) -> bool {
134        *self.request_rx.borrow()
135    }
136
137    /// Wait for a cancellation request (async).
138    pub async fn wait_for_request(&mut self) {
139        while !*self.request_rx.borrow() {
140            if self.request_rx.changed().await.is_err() {
141                // Channel closed, treat as cancelled
142                break;
143            }
144        }
145    }
146
147    /// Get the current state.
148    pub fn state(&self) -> CancelState {
149        *self.state_tx.borrow()
150    }
151
152    /// Transition to Requested state.
153    pub fn set_requested(&self) {
154        let _ = self.state_tx.send(CancelState::Requested);
155    }
156
157    /// Transition to Draining state with count of in-flight operations.
158    pub fn set_draining(&self, in_flight: usize) {
159        let _ = self.state_tx.send(CancelState::Draining { in_flight });
160    }
161
162    /// Update the in-flight count during draining.
163    pub fn update_draining(&self, in_flight: usize) {
164        if in_flight == 0 {
165            self.set_confirmed();
166        } else {
167            let _ = self.state_tx.send(CancelState::Draining { in_flight });
168        }
169    }
170
171    /// Transition to Confirmed state (all blocks released).
172    pub fn set_confirmed(&self) {
173        let _ = self.state_tx.send(CancelState::Confirmed);
174    }
175
176    /// Subscribe to state changes.
177    pub fn subscribe(&self) -> watch::Receiver<CancelState> {
178        self.state_tx.subscribe()
179    }
180}
181
182/// Future that resolves when cancellation is fully confirmed.
183///
184/// Obtained via `CancellationToken::wait_confirmed()` or `TransferHandle::cancel()`.
185pub struct CancelConfirmation {
186    state_rx: watch::Receiver<CancelState>,
187}
188
189impl CancelConfirmation {
190    /// Wait for confirmation (async).
191    ///
192    /// This is the recommended way to await cancellation confirmation.
193    pub async fn wait(mut self) {
194        loop {
195            // Check current state
196            if self.state_rx.borrow().is_confirmed() {
197                return;
198            }
199
200            // Wait for state change
201            if self.state_rx.changed().await.is_err() {
202                // Channel closed, treat as confirmed
203                return;
204            }
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_cancel_state_transitions() {
215        let state = CancelState::Active;
216        assert!(!state.is_cancelled());
217        assert!(!state.is_draining());
218        assert!(!state.is_confirmed());
219
220        let state = CancelState::Requested;
221        assert!(state.is_cancelled());
222        assert!(!state.is_draining());
223        assert!(!state.is_confirmed());
224
225        let state = CancelState::Draining { in_flight: 5 };
226        assert!(state.is_cancelled());
227        assert!(state.is_draining());
228        assert!(!state.is_confirmed());
229
230        let state = CancelState::Confirmed;
231        assert!(state.is_cancelled());
232        assert!(!state.is_draining());
233        assert!(state.is_confirmed());
234    }
235
236    #[test]
237    fn test_cancellation_token_request() {
238        let (token, _updater) = CancellationToken::new();
239
240        assert!(!token.is_requested());
241        assert_eq!(token.state(), CancelState::Active);
242
243        token.request();
244
245        assert!(token.is_requested());
246    }
247
248    #[test]
249    fn test_cancellation_updater_state() {
250        let (token, updater) = CancellationToken::new();
251
252        assert_eq!(token.state(), CancelState::Active);
253
254        updater.set_requested();
255        assert_eq!(token.state(), CancelState::Requested);
256
257        updater.set_draining(3);
258        assert_eq!(token.state(), CancelState::Draining { in_flight: 3 });
259
260        updater.update_draining(1);
261        assert_eq!(token.state(), CancelState::Draining { in_flight: 1 });
262
263        updater.update_draining(0);
264        assert_eq!(token.state(), CancelState::Confirmed);
265    }
266
267    #[tokio::test]
268    async fn test_cancel_confirmation_immediate() {
269        let (token, updater) = CancellationToken::new();
270
271        // Set confirmed before waiting
272        updater.set_confirmed();
273
274        // Should resolve immediately
275        token.wait_confirmed().wait().await;
276        assert!(token.is_confirmed());
277    }
278
279    #[tokio::test]
280    async fn test_cancel_confirmation_delayed() {
281        let (token, updater) = CancellationToken::new();
282
283        let confirmation = token.wait_confirmed();
284
285        // Spawn task to confirm after short delay
286        let updater_clone = updater.state_tx.clone();
287        tokio::spawn(async move {
288            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
289            let _ = updater_clone.send(CancelState::Confirmed);
290        });
291
292        // Wait for confirmation
293        tokio::time::timeout(tokio::time::Duration::from_millis(100), confirmation.wait())
294            .await
295            .expect("Should complete within timeout");
296
297        assert!(token.is_confirmed());
298    }
299
300    /// Test that confirmation does NOT resolve while in-flight > 0.
301    /// This is a critical invariant: cancellation only completes after draining.
302    #[tokio::test]
303    async fn test_confirmation_blocked_during_draining() {
304        let (token, updater) = CancellationToken::new();
305
306        token.request();
307        updater.set_draining(2);
308
309        // Confirmation should NOT resolve while draining
310        let confirmation = token.wait_confirmed();
311        let result =
312            tokio::time::timeout(tokio::time::Duration::from_millis(30), confirmation.wait()).await;
313        assert!(result.is_err(), "Should timeout while in_flight > 0");
314
315        // Still draining
316        assert_eq!(token.state(), CancelState::Draining { in_flight: 2 });
317    }
318
319    /// Test that update_draining(0) transitions directly to Confirmed.
320    #[test]
321    fn test_draining_zero_confirms() {
322        let (token, updater) = CancellationToken::new();
323
324        token.request();
325        updater.set_draining(1);
326        assert_eq!(token.state(), CancelState::Draining { in_flight: 1 });
327
328        // Drain to 0 should confirm
329        updater.update_draining(0);
330        assert_eq!(token.state(), CancelState::Confirmed);
331    }
332
333    /// Test the full draining sequence: Requested → Draining(n) → ... → Confirmed.
334    #[test]
335    fn test_full_draining_sequence() {
336        let (token, updater) = CancellationToken::new();
337
338        // Start active
339        assert_eq!(token.state(), CancelState::Active);
340
341        // Request
342        token.request();
343        assert!(token.is_requested());
344
345        // Set draining
346        updater.set_draining(3);
347        assert_eq!(token.state(), CancelState::Draining { in_flight: 3 });
348
349        // Drain one by one
350        updater.update_draining(2);
351        assert_eq!(token.state(), CancelState::Draining { in_flight: 2 });
352
353        updater.update_draining(1);
354        assert_eq!(token.state(), CancelState::Draining { in_flight: 1 });
355
356        // Final drain confirms
357        updater.update_draining(0);
358        assert!(token.is_confirmed());
359    }
360}