Skip to main content

query_runtime/runtime/gpu/
ledger.rs

1//! Versioned per-device immutable-grant ledger.
2//!
3//! A [`DeviceLedger`] maintains admission accounting for a single CUDA device,
4//! combining an immutable [`DeviceResourceProfile`] with versioned mutable state
5//! for active or cleaning attempt grants, frozen resident charges, device
6//! admission health, and cache-internal allocations.
7//!
8//! The capacity model partitions frozen managed capacity `M` into a cache
9//! share `K` (`cache_cap_bytes`) and a query share `Q = M - K`. Query
10//! admission and cache eviction govern separate shares, preventing conflicting
11//! commitments. Admission invariants evaluate:
12//!
13//! ```text
14//! R + Σ Gᵢ <= Q            (frozen residents + live grants fit the query share)
15//! C <= K                   (cache charges fit the cache share)
16//! active_attempts + 1 <= max_active_attempts
17//! new admission: R + Σ Gᵢ + Gnew <= Q
18//! ```
19//!
20//! The query cap is specified explicitly or computed during the admission transaction
21//! as `min(default_query_limit, query_share - resident_bytes)`. It is
22//! not derived from planner estimates or clamped to transient free capacity
23//! left by concurrent live grants.
24//!
25//! Versioning relies on monotonic state counters. The observation `sequence`
26//! increments on every state transition. A `LedgerSnapshot` records this sequence,
27//! providing in-lock observers with atomic mutation records that allow telemetry
28//! systems to order ledger events without secondary storage. The grant `generation`
29//! is allocated per committed grant, serving as the grant identifier and map key,
30//! and is never reused within a ledger to prevent aliasing with subsequent grants.
31//!
32//! Admission outcomes distinguish queue wait conditions from invariant violations.
33//! `try_admit_for_attempt` returns `Ok(None)` when the device cannot currently admit
34//! the request (unhealthy status, exhausted active attempt slots, or capacity limits),
35//! signalling that the request should await admission. `Err` indicates an invariant
36//! violation rather than a transient capacity constraint, with the `reason` fact
37//! identifying the specific failure. The capacity contract is defined in
38//! `website/docs/design/admission.mdx#device-profiles-and-grants`.
39
40use std::{
41    cell::Cell,
42    collections::BTreeMap,
43    fmt,
44    panic::{AssertUnwindSafe, catch_unwind},
45    sync::{Arc, Mutex, MutexGuard, PoisonError},
46};
47
48use cudf::runtime::{CudaEvent, CudaStream};
49use query_planning::error::{QueryError, QueryErrorCode, Result};
50use serde::Serialize;
51
52use super::DeviceResourceProfile;
53#[cfg(test)]
54use super::{DeviceCapabilities, DeviceResourceEnvelope};
55use crate::{
56    native_error::cudf_query_error,
57    observability::{AdmissionRequestId, AttemptFaultIsolation, AttemptId},
58    runtime::context::QueryServiceResourceEpoch,
59};
60
61const OPERATION: &str = "query_engine.gpu.device_ledger";
62
63/// Admission health of one device ledger.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub(crate) enum DeviceAdmissionHealth {
66    /// New attempts may be admitted.
67    Open,
68    /// New admission is closed; in-flight attempts finish and release.
69    /// `isolation` says whether the fault is confined to this device;
70    /// `reason` is diagnostic text only. `generation` counts closures of this
71    /// ledger, starting at 1, so a report can tell a repeated closure from the
72    /// one it already recorded. Health never reopens.
73    Closed { reason: String, isolation: AttemptFaultIsolation, generation: u64 },
74}
75
76impl DeviceAdmissionHealth {
77    fn as_str(&self) -> &'static str {
78        match self {
79            Self::Open => "open",
80            Self::Closed { .. } => "closed",
81        }
82    }
83}
84
85/// Atomic read of one device ledger at one observation sequence.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub(crate) struct LedgerSnapshot {
88    pub(crate) device_ordinal: usize,
89    /// Observation sequence this snapshot was taken at. Two snapshots with
90    /// the same sequence describe the same state.
91    pub(crate) sequence: u64,
92    pub(crate) managed_capacity_bytes: u64,
93    /// Cache share `K` carved out of the managed capacity.
94    pub(crate) cache_cap_bytes: u64,
95    /// Query share `Q = M - K` promisable to grants plus frozen residents.
96    pub(crate) query_share_bytes: u64,
97    pub(crate) default_query_limit_bytes: u64,
98    pub(crate) max_active_attempts: usize,
99    pub(crate) active_attempts: usize,
100    pub(crate) live_grants_bytes: u64,
101    /// Bytes owned by frozen resident domains.
102    pub(crate) frozen_resident_bytes: u64,
103    /// Aggregate bytes retained by live resident owners.
104    pub(crate) resident_owner_bytes: u64,
105    /// Logical root stream slots still available to a new admitted attempt.
106    ///
107    /// These values are derived from the immutable profile and active grants
108    /// while holding the ledger lock. They are not a separately sampled CUDA
109    /// stream-pool counter, so placement never combines independently read
110    /// resource gauges into an unsound admission fact.
111    pub(crate) root_streams_available: usize,
112    /// Logical auxiliary stream slots still available to admitted attempts.
113    pub(crate) auxiliary_streams_available: usize,
114    /// Logical root worker slots still available to a new admitted attempt.
115    pub(crate) root_worker_slots_available: usize,
116    /// Logical auxiliary worker slots still available to admitted attempts.
117    pub(crate) auxiliary_worker_slots_available: usize,
118    /// Worker slots committed by active attempts under the frozen profile.
119    pub(crate) worker_depth: usize,
120    pub(crate) cache_evictable_resident_bytes: u64,
121    pub(crate) cache_pinned_in_use_bytes: u64,
122    pub(crate) health: DeviceAdmissionHealth,
123}
124
125impl LedgerSnapshot {
126    /// Total logical stream slots available under the immutable profile.
127    pub(crate) fn stream_slots_available(&self) -> usize {
128        self.root_streams_available.saturating_add(self.auxiliary_streams_available)
129    }
130
131    /// Resolves and checks one query cap against this atomic snapshot.
132    ///
133    /// The check is `R + Σ Gᵢ + Gnew <= Q`: frozen residents plus live
134    /// grants plus the candidate grant must fit the query share. The cache
135    /// share never participates, so filling the cache cannot shrink query
136    /// admission and admitting queries cannot spend cache bytes.
137    pub(crate) fn admissible_query_cap_bytes(&self, requested_cap_bytes: Option<u64>) -> Option<u64> {
138        self.active_attempts.checked_add(1).filter(|active| *active <= self.max_active_attempts)?;
139        let resident_capacity = self.query_share_bytes.checked_sub(self.frozen_resident_bytes)?;
140        let query_cap = requested_cap_bytes.unwrap_or_else(|| self.default_query_limit_bytes.min(resident_capacity));
141        if query_cap == 0 {
142            return None;
143        }
144        self.frozen_resident_bytes
145            .checked_add(self.live_grants_bytes)?
146            .checked_add(query_cap)
147            .filter(|total| *total <= self.query_share_bytes)
148            .map(|_| query_cap)
149    }
150
151    /// Whether this snapshot's resident ownership leaves enough query-share
152    /// capacity for the requested cap, ignoring active attempts.
153    pub(crate) fn can_ever_fit_query_cap(&self, requested_cap_bytes: Option<u64>) -> bool {
154        let Some(resident_capacity) = self.query_share_bytes.checked_sub(self.frozen_resident_bytes) else {
155            return false;
156        };
157        let cap = requested_cap_bytes.unwrap_or_else(|| self.default_query_limit_bytes.min(resident_capacity));
158        cap > 0 && cap <= resident_capacity
159    }
160}
161
162/// Public diagnostic report for one device ledger.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
164pub struct DeviceLedgerReport {
165    /// CUDA device whose admission ledger was sampled.
166    pub device_ordinal: usize,
167    /// Observation sequence advanced by each ledger state mutation.
168    pub sequence: u64,
169    /// Managed byte capacity shared by query grants, frozen residents and cache.
170    pub managed_capacity_bytes: u64,
171    pub(crate) default_query_limit_bytes: u64,
172    /// Immutable simultaneous admitted-attempt bound.
173    pub max_active_attempts: usize,
174    /// Attempts whose grant remains active or is being cleaned up.
175    pub active_attempts: usize,
176    /// Sum of immutable active and cleaning grant capacities in bytes.
177    pub live_grants_bytes: u64,
178    /// Query-share bytes retained by frozen resident domains.
179    pub frozen_resident_bytes: u64,
180    /// Aggregate bytes retained by resident owners at this report's sequence.
181    pub resident_owner_bytes: u64,
182    /// Logical root stream slots available at this report's sequence.
183    pub root_streams_available: usize,
184    /// Logical auxiliary stream slots available at this report's sequence.
185    pub(crate) auxiliary_streams_available: usize,
186    /// Logical root worker slots available at this report's sequence.
187    pub root_worker_slots_available: usize,
188    /// Logical auxiliary worker slots available at this report's sequence.
189    pub(crate) auxiliary_worker_slots_available: usize,
190    /// Worker slots committed by active attempts at this report's sequence.
191    pub(crate) worker_depth: usize,
192    /// Query-share bytes not currently covered by live grants or frozen
193    /// residents (`Q - R - Σ Gᵢ`). Cache charges never participate.
194    pub available_bytes: u64,
195    /// Immediately grantable query-share bytes, projected from the same
196    /// snapshot as `available_bytes` (`Q - R - Σ Gᵢ`).
197    pub(crate) free_bytes: u64,
198    /// Cache-internal evictable accounting, reported separately from active
199    /// grants and pinned cache bytes.
200    pub evictable_bytes: u64,
201    /// Resident-cache bytes available for eviction at this ledger sequence.
202    pub cache_evictable_resident_bytes: u64,
203    /// Resident-cache bytes temporarily pinned by an active cache lease.
204    pub cache_pinned_in_use_bytes: u64,
205    /// Admission state label, `open` or `closed`.
206    pub health: String,
207    /// Active grant rows from the same ledger sequence.
208    pub grants: Vec<DeviceLedgerGrantRow>,
209}
210
211/// One active grant row in [`DeviceLedgerReport`].
212#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
213pub struct DeviceLedgerGrantRow {
214    /// Ledger generation minted once for this immutable grant.
215    pub grant_id: u64,
216    /// Request identity whose admission committed the grant.
217    pub admission_request_id: AdmissionRequestId,
218    /// Immutable query grant capacity in bytes.
219    pub grant_bytes: u64,
220    /// Live resident-owner charges associated with this grant, in bytes.
221    pub resident_owner_bytes: u64,
222    pub(crate) frozen_resident_bytes: Option<u64>,
223}
224
225/// Immutable identity assigned when a device ledger commits one attempt grant.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
227pub struct GrantIdentity {
228    pub(crate) attempt_id: AttemptId,
229    pub(crate) generation: u64,
230}
231
232/// The ledger mutation that advanced an observation sequence.
233///
234/// A transition observer receives the matching [`LedgerSnapshot`] while the
235/// ledger state mutex is still held. Observers must therefore treat the
236/// snapshot as their complete input and must not synchronously re-enter the
237/// same ledger.
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
239pub(crate) enum LedgerTransitionKind {
240    AdmissionClosed,
241    GrantAdmitted,
242    GrantFrozenResident,
243    GrantReleased,
244    /// An evictable resident-cache charge was added.
245    EvictableCharged,
246    /// An evictable resident-cache charge was released.
247    EvictableReleased,
248    /// A resident-cache entry became in-use and therefore non-evictable.
249    CachePinned,
250    /// The final in-use reference left a resident-cache entry.
251    CacheUnpinned,
252    ResidentOwnerCharged,
253    ResidentOwnerReleased,
254}
255
256/// One immutable ledger mutation observed atomically with its state change.
257#[derive(Debug, Clone, PartialEq, Eq)]
258pub(crate) struct LedgerTransition {
259    pub(crate) kind: LedgerTransitionKind,
260    pub(crate) snapshot: LedgerSnapshot,
261}
262
263/// In-lock observer of one atomic ledger transition.
264pub(crate) type LedgerTransitionObserver = Arc<dyn Fn(&LedgerTransition) + Send + Sync + 'static>;
265
266/// Post-lock observer of a transition that changes what queued admission can
267/// do: a grant release, an admission close, or a frozen-resident publish.
268///
269/// It runs after the transition committed and the ledger state mutex was
270/// released, so it may wake or revalidate admission queues.
271pub(crate) type LedgerAdmissionObserver = Arc<dyn Fn(&LedgerTransition) + Send + Sync + 'static>;
272
273/// Per-device versioned immutable-grant ledger.
274#[derive(Clone)]
275pub struct DeviceLedger {
276    inner: Arc<DeviceLedgerInner>,
277}
278
279/// Shared by every `DeviceLedger` clone and by every `MemoryGrant` minted
280/// from it; a grant therefore keeps its ledger alive until its final release.
281///
282/// Lock order: the query service takes its admission mutex before calling
283/// into the ledger, and admission observers take that mutex again, so the
284/// ledger releases `state` before it notifies them. Transition observers run
285/// while `state` is held and must not re-enter this ledger; the thread-local
286/// observer stack turns such re-entry into an error or panic, where the
287/// non-reentrant mutex would otherwise self-deadlock.
288struct DeviceLedgerInner {
289    profile: DeviceResourceProfile,
290    state: Mutex<DeviceLedgerState>,
291    // Registration is copy-on-write.  Transition delivery only clones this
292    // Arc while the ledger state mutex is held; it must never clone a Vec or
293    // allocate on an admission/resource update path.
294    transition_observers: Mutex<Arc<[LedgerTransitionObserver]>>,
295    // Admission observers are outside the state mutex, but share the same
296    // allocation-free dispatch rule as transition observers.
297    admission_observers: Mutex<Arc<[LedgerAdmissionObserver]>>,
298}
299
300const MAX_LEDGER_TRANSITION_OBSERVER_DEPTH: usize = 64;
301
302/// Ledgers whose state mutex the current thread holds while delivering a
303/// transition. Observers of one ledger may legitimately act on another
304/// ledger, which a single flag could not distinguish; a lookup for a ledger
305/// that is on the stack means the caller is inside that ledger's own observer
306/// dispatch.
307#[derive(Clone, Copy)]
308struct LedgerTransitionObserverStack {
309    ledgers: [*const DeviceLedgerInner; MAX_LEDGER_TRANSITION_OBSERVER_DEPTH],
310    len: usize,
311}
312
313impl LedgerTransitionObserverStack {
314    const EMPTY: Self = Self { ledgers: [std::ptr::null(); MAX_LEDGER_TRANSITION_OBSERVER_DEPTH], len: 0 };
315
316    fn contains(&self, ledger: *const DeviceLedgerInner) -> bool {
317        self.ledgers[..self.len].contains(&ledger)
318    }
319}
320
321thread_local! {
322    // The reentrancy guard is a fixed stack: in-lock observer dispatch must
323    // remain allocation-free even on a thread's first transition.
324    static LEDGER_TRANSITION_OBSERVER_STACK: Cell<LedgerTransitionObserverStack> = const { Cell::new(LedgerTransitionObserverStack::EMPTY) };
325}
326
327struct LedgerTransitionObserverGuard {
328    ledger: *const DeviceLedgerInner,
329}
330
331impl LedgerTransitionObserverGuard {
332    fn enter(ledger: &Arc<DeviceLedgerInner>) -> Self {
333        let ledger = Arc::as_ptr(ledger);
334        LEDGER_TRANSITION_OBSERVER_STACK.with(|stack| {
335            let mut active = stack.get();
336            assert!(
337                active.len < MAX_LEDGER_TRANSITION_OBSERVER_DEPTH,
338                "ledger transition observer nesting exceeds its fixed bound"
339            );
340            active.ledgers[active.len] = ledger;
341            active.len += 1;
342            stack.set(active);
343        });
344        Self { ledger }
345    }
346}
347
348impl Drop for LedgerTransitionObserverGuard {
349    fn drop(&mut self) {
350        LEDGER_TRANSITION_OBSERVER_STACK.with(|stack| {
351            let mut active = stack.get();
352            debug_assert!(active.len > 0);
353            active.len = active.len.saturating_sub(1);
354            debug_assert_eq!(active.ledgers[active.len], self.ledger);
355            active.ledgers[active.len] = std::ptr::null();
356            stack.set(active);
357        });
358    }
359}
360
361impl fmt::Debug for DeviceLedgerInner {
362    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
363        f.debug_struct("DeviceLedgerInner")
364            .field("device_ordinal", &self.profile.device_ordinal)
365            .field("managed_capacity_bytes", &self.profile.managed_capacity_bytes)
366            .field("state", &self.state)
367            .finish_non_exhaustive()
368    }
369}
370
371#[derive(Debug)]
372struct DeviceLedgerState {
373    /// Advanced by every mutation, including cache charges. Starts at 0; the
374    /// first transition observers see carries sequence 1.
375    sequence: u64,
376    /// Next grant id. Starts at 1 and is never reused within this ledger.
377    next_grant_generation: u64,
378    /// Grants keyed by generation. A grant stays here after it is frozen
379    /// resident, with `frozen_resident_bytes` set, until its final release;
380    /// only grants without that field count as active attempts.
381    grants: BTreeMap<u64, ActiveGrant>,
382    /// `R` in the admission equation: the sum of published frozen charges.
383    frozen_resident_bytes: u64,
384    health: DeviceAdmissionHealth,
385    /// Cache share in use: `evictable_bytes + pinned_bytes <= cache_cap_bytes`.
386    evictable_bytes: u64,
387    pinned_bytes: u64,
388}
389
390#[derive(Debug, Clone, Copy)]
391struct ActiveGrant {
392    admission_request_id: AdmissionRequestId,
393    /// `G_i`: counts toward the query share while the grant is active.
394    grant_bytes: u64,
395    /// Bytes retained by live `ResidentOwnerLease`s under this grant. Reported
396    /// and reconciled at freeze time; never part of the admission equation.
397    resident_owner_bytes: u64,
398    /// Prepared but unpublished frozen charge. While only this is set the
399    /// grant still counts its full `grant_bytes` as active, so the report
400    /// commit can fail without having released any capacity.
401    pending_frozen_resident_bytes: Option<u64>,
402    /// Published frozen charge. From this point the grant is no longer an
403    /// active attempt, its bytes are in `frozen_resident_bytes`, and they
404    /// return only at the grant's final release.
405    frozen_resident_bytes: Option<u64>,
406}
407
408impl DeviceLedgerState {
409    fn live_grants_bytes(&self) -> u64 {
410        self.grants.values().fold(0u64, |sum, grant| {
411            sum.saturating_add(if grant.frozen_resident_bytes.is_some() { 0 } else { grant.grant_bytes })
412        })
413    }
414
415    fn resident_owner_bytes(&self) -> u64 {
416        self.grants.values().fold(0u64, |sum, grant| sum.saturating_add(grant.resident_owner_bytes))
417    }
418}
419
420impl DeviceLedger {
421    /// Creates one ledger over an immutable per-device profile.
422    pub(crate) fn new(profile: DeviceResourceProfile) -> Self {
423        Self {
424            inner: Arc::new(DeviceLedgerInner {
425                profile,
426                state: Mutex::new(DeviceLedgerState {
427                    sequence: 0,
428                    next_grant_generation: 1,
429                    grants: BTreeMap::new(),
430                    frozen_resident_bytes: 0,
431                    health: DeviceAdmissionHealth::Open,
432                    evictable_bytes: 0,
433                    pinned_bytes: 0,
434                }),
435                transition_observers: Mutex::new(Arc::from([])),
436                admission_observers: Mutex::new(Arc::from([])),
437            }),
438        }
439    }
440
441    /// Test constructor deriving a valid profile from raw capacity inputs.
442    #[cfg(test)]
443    pub(crate) fn for_test(device_ordinal: usize, managed_capacity_bytes: u64, max_active_attempts: usize) -> Self {
444        let max_active_attempts = max_active_attempts.max(1);
445        let source_work = 1usize;
446        let slots = max_active_attempts.saturating_mul(2).max(max_active_attempts);
447        let default_query_limit_bytes = DeviceResourceProfile::derive_default_query_limit_bytes(
448            device_ordinal,
449            managed_capacity_bytes,
450            0,
451            max_active_attempts,
452        )
453        .expect("test ledger profile must resolve");
454        let profile = DeviceResourceProfile::resolve(DeviceResourceEnvelope {
455            device_ordinal,
456            device_capabilities: DeviceCapabilities::default(),
457            configured_limit_bytes: managed_capacity_bytes,
458            headroom_bytes: 0,
459            backend_reserve_bytes: 0,
460            default_query_limit_bytes,
461            max_active_attempts,
462            source_work_per_attempt: source_work,
463            stream_pool_size: slots,
464            worker_job_capacity: slots,
465            cache_cap_bytes: 0,
466        })
467        .expect("test ledger profile must resolve");
468        Self::new(profile)
469    }
470
471    /// Test convenience: admit one attempt with an explicit query cap.
472    #[cfg(test)]
473    pub(crate) fn admit_with_configured_floor_and_soft_working_set_hint_for_test(
474        &self,
475        waiter_id: AdmissionRequestId,
476        query_cap_bytes: u64,
477        _unused_soft_working_set_hint_bytes: u64,
478    ) -> Result<Option<MemoryGrant>> {
479        self.try_admit_for_attempt(waiter_id, AttemptId::mint(), Some(query_cap_bytes))
480    }
481
482    /// Immutable per-device resource profile behind this ledger.
483    pub fn profile(&self) -> &DeviceResourceProfile {
484        &self.inner.profile
485    }
486
487    /// CUDA device ordinal owned by this ledger.
488    pub fn device_ordinal(&self) -> usize {
489        self.inner.profile.device_ordinal
490    }
491
492    /// Managed capacity in bytes (immutable).
493    pub(crate) fn managed_capacity_bytes(&self) -> u64 {
494        self.inner.profile.managed_capacity_bytes
495    }
496
497    fn in_transition_observer(&self) -> bool {
498        let ledger = Arc::as_ptr(&self.inner);
499        LEDGER_TRANSITION_OBSERVER_STACK.with(|stack| stack.get().contains(ledger))
500    }
501
502    fn transition_observer_reentrancy_error(&self, action: &'static str) -> QueryError {
503        QueryError::new(
504            QueryErrorCode::Internal,
505            OPERATION,
506            "device ledger transition observer synchronously re-entered the same ledger",
507        )
508        .with_fact("action", action)
509        .with_fact("reason", "ledger_observer_reentrancy")
510    }
511
512    #[cold]
513    fn panic_on_transition_observer_reentrancy(&self, action: &'static str) -> ! {
514        std::panic::panic_any(self.transition_observer_reentrancy_error(action))
515    }
516
517    /// Takes the state mutex for `action`. Fails with reason
518    /// `ledger_observer_reentrancy` when called from one of this ledger's own
519    /// transition observers, and `ledger_mutex_poisoned` after a panic inside
520    /// an earlier critical section; release paths that cannot fail use the
521    /// raw lock instead.
522    fn lock_state(&self, action: &'static str) -> Result<MutexGuard<'_, DeviceLedgerState>> {
523        if self.in_transition_observer() {
524            return Err(self.transition_observer_reentrancy_error(action));
525        }
526        self.inner.state.lock().map_err(|_error| {
527            QueryError::new(QueryErrorCode::Internal, OPERATION, "device ledger mutex was poisoned")
528                .with_fact("action", action)
529                .with_fact("reason", "ledger_mutex_poisoned")
530        })
531    }
532
533    // Registration replaces each observer list as a whole, so a guard
534    // recovered from a poisoned registry still holds a complete list.
535    fn transition_observers(&self) -> Arc<[LedgerTransitionObserver]> {
536        Arc::clone(&self.inner.transition_observers.lock().unwrap_or_else(PoisonError::into_inner))
537    }
538
539    fn admission_observers(&self) -> Arc<[LedgerAdmissionObserver]> {
540        Arc::clone(&self.inner.admission_observers.lock().unwrap_or_else(PoisonError::into_inner))
541    }
542
543    /// Delivers `transition` to the admission observers. The caller has
544    /// released the ledger state mutex.
545    fn notify_admission_observers(&self, transition: &LedgerTransition) {
546        for observer in self.admission_observers().iter() {
547            if catch_unwind(AssertUnwindSafe(|| observer(transition))).is_err() {
548                eprintln!("[query-engine] warning: device ledger admission observer panicked");
549            }
550        }
551    }
552
553    /// Advances the sequence and delivers the transition to the in-lock
554    /// observers. Every mutation ends with this call, so the snapshot the
555    /// observers receive is the state at the new sequence. Observer
556    /// panics are caught; a panicking observer must not undo an admission or
557    /// a release that already took effect.
558    fn bump_sequence_and_notify_locked(
559        &self,
560        state: &mut DeviceLedgerState,
561        kind: LedgerTransitionKind,
562    ) -> LedgerTransition {
563        state.sequence = state.sequence.saturating_add(1);
564        let transition = LedgerTransition { kind, snapshot: self.snapshot_locked(state) };
565        for observer in self.transition_observers().iter() {
566            let _delivery = LedgerTransitionObserverGuard::enter(&self.inner);
567            if catch_unwind(AssertUnwindSafe(|| observer(&transition))).is_err() {
568                eprintln!("[query-engine] warning: device ledger transition observer panicked");
569            }
570        }
571        transition
572    }
573
574    /// Registers an in-lock observer for every ledger sequence transition.
575    ///
576    /// Registration is additive. Each invocation receives a snapshot whose
577    /// sequence equals the transition sequence; observer panics are
578    /// isolated from admission and release.
579    pub(crate) fn register_transition_observer(&self, observer: LedgerTransitionObserver) -> Result<()> {
580        let mut registered = self.inner.transition_observers.lock().map_err(|_error| {
581            QueryError::new(QueryErrorCode::Internal, OPERATION, "device ledger transition-observer mutex was poisoned")
582                .with_fact("reason", "transition_observer_mutex_poisoned")
583        })?;
584        let mut next = Vec::with_capacity(registered.len().saturating_add(1));
585        next.extend(registered.iter().cloned());
586        next.push(observer);
587        *registered = Arc::from(next);
588        Ok(())
589    }
590
591    /// Registers an additive post-lock [`LedgerAdmissionObserver`].
592    ///
593    /// It runs only after a grant release, an admission close, or a
594    /// frozen-resident publish has committed and the ledger mutex has been
595    /// released. This preserves the queue-wakeup contract without conflating
596    /// it with atomic state observation.
597    pub(crate) fn register_admission_observer(&self, observer: LedgerAdmissionObserver) -> Result<()> {
598        let mut registered = self.inner.admission_observers.lock().map_err(|_error| {
599            QueryError::new(QueryErrorCode::Internal, OPERATION, "device ledger admission-observer mutex was poisoned")
600                .with_fact("reason", "admission_observer_mutex_poisoned")
601        })?;
602        let mut next = Vec::with_capacity(registered.len().saturating_add(1));
603        next.extend(registered.iter().cloned());
604        next.push(observer);
605        *registered = Arc::from(next);
606        Ok(())
607    }
608
609    /// One atomic ledger snapshot for placement, reports, and metrics.
610    pub(crate) fn snapshot(&self) -> Result<LedgerSnapshot> {
611        let state = self.lock_state("snapshot")?;
612        Ok(self.snapshot_locked(&state))
613    }
614
615    fn snapshot_locked(&self, state: &DeviceLedgerState) -> LedgerSnapshot {
616        let profile = &self.inner.profile;
617        let active_attempts = state.grants.values().filter(|grant| grant.frozen_resident_bytes.is_none()).count();
618        // Admission reserves the complete root plus bounded auxiliary envelope
619        // for every active attempt. Availability is derived from that immutable
620        // envelope under the ledger lock, and mutable executor or stream-pool
621        // counters are not read separately.
622        let committed_auxiliary_slots = active_attempts.saturating_mul(profile.source_work_per_attempt);
623        LedgerSnapshot {
624            device_ordinal: profile.device_ordinal,
625            sequence: state.sequence,
626            managed_capacity_bytes: profile.managed_capacity_bytes,
627            cache_cap_bytes: profile.cache_cap_bytes,
628            query_share_bytes: profile.query_share_bytes(),
629            default_query_limit_bytes: profile.default_query_limit_bytes,
630            max_active_attempts: profile.max_active_attempts,
631            active_attempts,
632            live_grants_bytes: state.live_grants_bytes(),
633            frozen_resident_bytes: state.frozen_resident_bytes,
634            resident_owner_bytes: state.resident_owner_bytes(),
635            root_streams_available: profile.root_stream_count.saturating_sub(active_attempts),
636            auxiliary_streams_available: profile.aux_source_work_capacity.saturating_sub(committed_auxiliary_slots),
637            root_worker_slots_available: profile.root_lane_count.saturating_sub(active_attempts),
638            auxiliary_worker_slots_available: profile
639                .aux_source_work_capacity
640                .saturating_sub(committed_auxiliary_slots),
641            worker_depth: active_attempts.saturating_add(committed_auxiliary_slots),
642            // Resident-cache charges share the same atomic ledger snapshot.
643            cache_evictable_resident_bytes: state.evictable_bytes,
644            cache_pinned_in_use_bytes: state.pinned_bytes,
645            health: state.health.clone(),
646        }
647    }
648
649    /// Public diagnostic report for this ledger.
650    pub(crate) fn report(&self) -> Result<DeviceLedgerReport> {
651        let state = self.lock_state("report")?;
652        let snapshot = self.snapshot_locked(&state);
653        let grants = state
654            .grants
655            .iter()
656            .map(|(grant_id, active)| DeviceLedgerGrantRow {
657                grant_id: *grant_id,
658                admission_request_id: active.admission_request_id,
659                grant_bytes: active.grant_bytes,
660                resident_owner_bytes: active.resident_owner_bytes,
661                frozen_resident_bytes: active.frozen_resident_bytes,
662            })
663            .collect();
664        Ok(DeviceLedgerReport {
665            device_ordinal: snapshot.device_ordinal,
666            sequence: snapshot.sequence,
667            managed_capacity_bytes: snapshot.managed_capacity_bytes,
668            default_query_limit_bytes: snapshot.default_query_limit_bytes,
669            max_active_attempts: snapshot.max_active_attempts,
670            active_attempts: snapshot.active_attempts,
671            live_grants_bytes: snapshot.live_grants_bytes,
672            frozen_resident_bytes: snapshot.frozen_resident_bytes,
673            resident_owner_bytes: snapshot.resident_owner_bytes,
674            root_streams_available: snapshot.root_streams_available,
675            auxiliary_streams_available: snapshot.auxiliary_streams_available,
676            root_worker_slots_available: snapshot.root_worker_slots_available,
677            auxiliary_worker_slots_available: snapshot.auxiliary_worker_slots_available,
678            worker_depth: snapshot.worker_depth,
679            available_bytes: snapshot
680                .query_share_bytes
681                .saturating_sub(snapshot.live_grants_bytes)
682                .saturating_sub(snapshot.frozen_resident_bytes),
683            free_bytes: snapshot
684                .query_share_bytes
685                .saturating_sub(snapshot.live_grants_bytes)
686                .saturating_sub(snapshot.frozen_resident_bytes),
687            evictable_bytes: state.evictable_bytes,
688            cache_evictable_resident_bytes: snapshot.cache_evictable_resident_bytes,
689            cache_pinned_in_use_bytes: snapshot.cache_pinned_in_use_bytes,
690            health: snapshot.health.as_str().to_string(),
691            grants,
692        })
693    }
694
695    pub(crate) fn health(&self) -> Result<DeviceAdmissionHealth> {
696        let state = self.lock_state("health")?;
697        Ok(state.health.clone())
698    }
699
700    /// Closes new admission on this device for a fault confined to it;
701    /// in-flight grants still release.
702    pub(crate) fn close_admission(&self, reason: impl Into<String>) -> Result<u64> {
703        self.close_admission_with(AttemptFaultIsolation::DeviceLocal, reason.into())
704    }
705
706    /// Closes new admission because cleanup could not establish whether a
707    /// fault stayed on one device. Only the service-wide closure uses it.
708    pub(crate) fn close_admission_for_unknown_isolation(&self, detail: impl Into<String>) -> Result<u64> {
709        self.close_admission_with(AttemptFaultIsolation::Unknown, detail.into())
710    }
711
712    fn close_admission_with(&self, isolation: AttemptFaultIsolation, reason: String) -> Result<u64> {
713        let mut state = self.lock_state("close_admission")?;
714        let generation = match &state.health {
715            DeviceAdmissionHealth::Open => 1,
716            DeviceAdmissionHealth::Closed { generation, .. } => generation.saturating_add(1),
717        };
718        state.health = DeviceAdmissionHealth::Closed { reason, isolation, generation };
719        let transition = self.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::AdmissionClosed);
720        // Admission observers wake service queues, so they run outside the ledger lock.
721        drop(state);
722        self.notify_admission_observers(&transition);
723        Ok(generation)
724    }
725
726    /// Closes new admission only when this ledger is still open.
727    ///
728    /// Native device health is process-lifetime state. Multiple service
729    /// contexts can observe the same fault concurrently, but only the first
730    /// observation may advance the ledger health generation and define its
731    /// closure reason.
732    pub(crate) fn close_admission_if_open(&self, reason: impl Into<String>) -> Result<bool> {
733        let mut state = self.lock_state("close_admission_if_open")?;
734        if !matches!(state.health, DeviceAdmissionHealth::Open) {
735            return Ok(false);
736        }
737        state.health = DeviceAdmissionHealth::Closed {
738            reason: reason.into(),
739            isolation: AttemptFaultIsolation::DeviceLocal,
740            generation: 1,
741        };
742        let transition = self.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::AdmissionClosed);
743        // Admission observers wake service queues, so they run outside the ledger lock.
744        drop(state);
745        self.notify_admission_observers(&transition);
746        Ok(true)
747    }
748
749    /// Returns the cap that could be admitted right now.
750    #[cfg(test)]
751    pub(crate) fn candidate_query_cap_bytes(&self, requested_cap_bytes: Option<u64>) -> Result<Option<u64>> {
752        let state = self.lock_state("candidate_max_new_grant_bytes")?;
753        let snapshot = self.snapshot_locked(&state);
754        Ok(snapshot.admissible_query_cap_bytes(requested_cap_bytes))
755    }
756
757    /// Attempts one immutable grant admission for `waiter_id`.
758    ///
759    /// The grant is the explicit cap, or the default cap resolved
760    /// against resident ownership in this transaction. It is never clamped to
761    /// transient capacity and planner estimates do not participate.
762    ///
763    /// `Ok(None)` means the device cannot admit now and the caller should
764    /// wait for a `GrantReleased`, `GrantFrozenResident`, or `AdmissionClosed`
765    /// admission notification. For a well-formed request `Err` means a
766    /// poisoned or re-entered ledger or an exhausted grant generation; a zero
767    /// explicit cap is rejected before the lock is taken. Check and commit
768    /// happen under one lock hold, so two concurrent admissions cannot both
769    /// pass on the same free bytes.
770    pub(crate) fn try_admit_for_attempt(
771        &self,
772        waiter_id: AdmissionRequestId,
773        attempt_id: AttemptId,
774        requested_cap_bytes: Option<u64>,
775    ) -> Result<Option<MemoryGrant>> {
776        if requested_cap_bytes == Some(0) {
777            return Err(QueryError::new(QueryErrorCode::InvalidPlan, OPERATION, "query cap must be greater than zero")
778                .with_fact("reason", "zero_query_cap")
779                .with_fact("admission_request_id", waiter_id));
780        }
781        let mut state = self.lock_state("try_admit")?;
782        if !matches!(state.health, DeviceAdmissionHealth::Open) {
783            return Ok(None);
784        }
785        let snapshot = self.snapshot_locked(&state);
786        let Some(query_cap_bytes) = snapshot.admissible_query_cap_bytes(requested_cap_bytes) else {
787            return Ok(None);
788        };
789        self.commit_grant_locked(&mut state, waiter_id, attempt_id, query_cap_bytes).map(Some)
790    }
791
792    #[cfg(test)]
793    pub(crate) fn try_admit(
794        &self,
795        waiter_id: AdmissionRequestId,
796        requested_cap_bytes: Option<u64>,
797    ) -> Result<Option<MemoryGrant>> {
798        self.try_admit_for_attempt(waiter_id, AttemptId::mint(), requested_cap_bytes)
799    }
800
801    fn commit_grant_locked(
802        &self,
803        state: &mut DeviceLedgerState,
804        waiter_id: AdmissionRequestId,
805        attempt_id: AttemptId,
806        grant_bytes: u64,
807    ) -> Result<MemoryGrant> {
808        let generation = state.next_grant_generation;
809        let next_grant_generation = generation.checked_add(1).ok_or_else(|| {
810            QueryError::new(QueryErrorCode::Internal, OPERATION, "device ledger grant generation exhausted")
811                .with_fact("reason", "grant_generation_exhausted")
812                .with_fact("device_ordinal", self.inner.profile.device_ordinal)
813                .with_fact("attempt_id", attempt_id)
814        })?;
815        let identity = GrantIdentity { attempt_id, generation };
816        state.next_grant_generation = next_grant_generation;
817        state.grants.insert(
818            generation,
819            ActiveGrant {
820                admission_request_id: waiter_id,
821                grant_bytes,
822                resident_owner_bytes: 0,
823                pending_frozen_resident_bytes: None,
824                frozen_resident_bytes: None,
825            },
826        );
827        self.bump_sequence_and_notify_locked(state, LedgerTransitionKind::GrantAdmitted);
828        Ok(MemoryGrant {
829            inner: Arc::new(MemoryGrantInner {
830                ledger: Arc::clone(&self.inner),
831                identity,
832                admission_request_id: waiter_id,
833                grant_bytes,
834                resident_owner_bytes: Default::default(),
835                next_resident_owner_id: std::sync::atomic::AtomicU64::new(1),
836                resident_owner_allocations: Mutex::new(BTreeMap::new()),
837                frozen: Default::default(),
838                frozen_domain: Mutex::new(None),
839            }),
840            resource_epoch: None,
841        })
842    }
843
844    /// Releases one grant after its active or resident ownership ends.
845    ///
846    /// Called from `MemoryGrantInner::drop`, which cannot return an error, so
847    /// re-entry from a transition observer panics instead. A grant id that is
848    /// no longer present is ignored; the map entry is removed once.
849    fn release_grant(&self, grant_id: u64) {
850        if self.in_transition_observer() {
851            self.panic_on_transition_observer_reentrancy("release_grant");
852        }
853        // The state is integer accounting; returning early on a poisoned lock would leak the grant for good.
854        let mut state = self.inner.state.lock().unwrap_or_else(PoisonError::into_inner);
855        let Some(active) = state.grants.remove(&grant_id) else {
856            return;
857        };
858        if let Some(resident_bytes) = active.frozen_resident_bytes {
859            state.frozen_resident_bytes = state.frozen_resident_bytes.saturating_sub(resident_bytes);
860        }
861        let transition = self.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::GrantReleased);
862        // Admission observers lock service admission mutexes, so they run outside the ledger lock.
863        drop(state);
864        self.notify_admission_observers(&transition);
865    }
866
867    /// Attempts to charge bytes retained by an evictable cache.
868    ///
869    /// Cache eviction owns only the cache share (`C <= K`): the check never
870    /// consults live query grants or frozen residents, so cache-first and
871    /// query-first admission orders promise identical capacity. The query
872    /// share bound (`R + Σ Gᵢ <= Q`) independently keeps the combined
873    /// commitment within `M`.
874    pub(crate) fn try_reserve_evictable_bytes(&self, bytes: u64) -> Result<bool> {
875        if bytes == 0 {
876            return Ok(true);
877        }
878        let mut state = self.lock_state("try_reserve_evictable_bytes")?;
879        if state.evictable_bytes.saturating_add(state.pinned_bytes).saturating_add(bytes)
880            > self.inner.profile.cache_cap_bytes
881        {
882            return Ok(false);
883        }
884        state.evictable_bytes = state.evictable_bytes.saturating_add(bytes);
885        self.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::EvictableCharged);
886        Ok(true)
887    }
888
889    /// Moves one exact resident-cache charge from evictable to pinned.
890    ///
891    /// The aggregate cache charge is unchanged. This narrow transition is
892    /// deliberately private to the shared residency state machine: callers
893    /// must not maintain independent pinned counters beside the ledger.
894    pub(crate) fn pin_cache_residency_charge(&self, bytes: u64) -> Result<()> {
895        if bytes == 0 {
896            return Ok(());
897        }
898        let mut state = self.lock_state("pin_cache_residency_charge")?;
899        if state.evictable_bytes < bytes {
900            return Err(QueryError::new(
901                QueryErrorCode::Internal,
902                OPERATION,
903                "resident-cache pin is missing its evictable ledger charge",
904            )
905            .with_fact("reason", "cache_evictable_charge_missing")
906            .with_fact("requested_bytes", bytes)
907            .with_fact("evictable_bytes", state.evictable_bytes));
908        }
909        let pinned_bytes = state.pinned_bytes.checked_add(bytes).ok_or_else(|| {
910            QueryError::new(QueryErrorCode::Internal, OPERATION, "resident-cache pinned ledger charge overflowed")
911                .with_fact("reason", "cache_pinned_charge_overflow")
912                .with_fact("requested_bytes", bytes)
913                .with_fact("pinned_bytes", state.pinned_bytes)
914        })?;
915        state.evictable_bytes -= bytes;
916        state.pinned_bytes = pinned_bytes;
917        self.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::CachePinned);
918        Ok(())
919    }
920
921    /// Moves one exact resident-cache charge from pinned back to evictable.
922    pub(crate) fn unpin_cache_residency_charge(&self, bytes: u64) -> Result<()> {
923        if bytes == 0 {
924            return Ok(());
925        }
926        let mut state = self.lock_state("unpin_cache_residency_charge")?;
927        if state.pinned_bytes < bytes {
928            return Err(QueryError::new(
929                QueryErrorCode::Internal,
930                OPERATION,
931                "resident-cache unpin is missing its pinned ledger charge",
932            )
933            .with_fact("reason", "cache_pinned_charge_missing")
934            .with_fact("requested_bytes", bytes)
935            .with_fact("pinned_bytes", state.pinned_bytes));
936        }
937        let evictable_bytes = state.evictable_bytes.checked_add(bytes).ok_or_else(|| {
938            QueryError::new(QueryErrorCode::Internal, OPERATION, "resident-cache evictable ledger charge overflowed")
939                .with_fact("reason", "cache_evictable_charge_overflow")
940                .with_fact("requested_bytes", bytes)
941                .with_fact("evictable_bytes", state.evictable_bytes)
942        })?;
943        state.pinned_bytes -= bytes;
944        state.evictable_bytes = evictable_bytes;
945        self.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::CacheUnpinned);
946        Ok(())
947    }
948
949    /// Releases the complete exact charge of a destroyed resident-cache entry.
950    ///
951    /// An undercharged release is refused. The residency state machine calls it
952    /// only after GPU-safe destruction has completed, so a mismatched byte count
953    /// fails closed and does not release another entry's charge.
954    pub(crate) fn release_cache_residency_charge_after_destroy(&self, bytes: u64) -> Result<()> {
955        self.release_exact_evictable_cache_charge(bytes, "release_cache_residency_charge_after_destroy")
956    }
957
958    /// Returns unused conservative pre-reservation bytes before a cache entry
959    /// has been committed. No physical allocation owns this excess yet.
960    pub(crate) fn release_cache_residency_precharge_excess(&self, bytes: u64) -> Result<()> {
961        self.release_exact_evictable_cache_charge(bytes, "release_cache_residency_precharge_excess")
962    }
963
964    fn release_exact_evictable_cache_charge(&self, bytes: u64, action: &'static str) -> Result<()> {
965        if bytes == 0 {
966            return Ok(());
967        }
968        let mut state = self.lock_state(action)?;
969        if state.evictable_bytes < bytes {
970            return Err(QueryError::new(
971                QueryErrorCode::Internal,
972                OPERATION,
973                "resident-cache exact release is missing its evictable ledger charge",
974            )
975            .with_fact("reason", "cache_exact_charge_missing")
976            .with_fact("action", action)
977            .with_fact("requested_bytes", bytes)
978            .with_fact("evictable_bytes", state.evictable_bytes));
979        }
980        state.evictable_bytes -= bytes;
981        self.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::EvictableReleased);
982        Ok(())
983    }
984}
985
986/// RAII immutable attempt grant. Dropping the final clone returns its budget
987/// to the device ledger; the budget never changes while the grant is live.
988///
989/// Clones include the ones held inside every `ResidentOwnerLease`, so a grant
990/// whose attempt has finished stays in the ledger for as long as any resident
991/// output it produced is still retained.
992#[derive(Clone)]
993pub struct MemoryGrant {
994    inner: Arc<MemoryGrantInner>,
995    /// Query-service epoch of the admitted attempt. Resident owner leases
996    /// derived from this grant carry a service-generation-only epoch instead,
997    /// see `new_resident_owner_lease`.
998    resource_epoch: Option<Arc<QueryServiceResourceEpoch>>,
999}
1000
1001/// Shared grant retention held by GPU-resident outputs.
1002#[derive(Clone)]
1003pub struct ResidentOwnerLease {
1004    inner: Arc<ResidentOwnerLeaseInner>,
1005    // Keep-alive set on the lease a reader holds; the reader's completion is
1006    // recorded when its last clone drops.
1007    _reader: Option<Arc<ResidentReader>>,
1008}
1009
1010struct ResidentOwnerLeaseInner {
1011    /// Keeps the grant, and through it the ledger row, alive until this
1012    /// owner's final release.
1013    grant: MemoryGrant,
1014    /// Charge added at creation and released once, in `Drop`.
1015    bytes: u64,
1016    owner_id: u64,
1017    completion_fences: Mutex<ResidentCompletionFences>,
1018}
1019
1020/// One completion event per stream that produced or read a resident output,
1021/// each recorded after that stream's last use of it. A frozen owner's final
1022/// release waits on every one; the pooled streams themselves may already run
1023/// another query.
1024#[derive(Default)]
1025struct ResidentCompletionFences {
1026    fences: Vec<ResidentCompletionFence>,
1027    // The first reader whose completion could not be recorded. The final
1028    // release cannot prove that reader finished, so it closes admission.
1029    record_failure: Option<String>,
1030}
1031
1032struct ResidentCompletionFence {
1033    stream_identity: usize,
1034    event: CudaEvent,
1035}
1036
1037/// A reader of a resident output on `stream`. Its drop records the stream's
1038/// completion, so the owner's final release waits for the reads it enqueued.
1039struct ResidentReader {
1040    owner: Arc<ResidentOwnerLeaseInner>,
1041    stream: CudaStream,
1042}
1043
1044impl Drop for ResidentReader {
1045    fn drop(&mut self) {
1046        if let Err(error) = self.owner.record_completion(&self.stream) {
1047            let mut fences = self.owner.completion_fences.lock().unwrap_or_else(PoisonError::into_inner);
1048            fences.record_failure.get_or_insert_with(|| format!("resident reader completion failed: {error}"));
1049        }
1050    }
1051}
1052
1053struct MemoryGrantInner {
1054    ledger: Arc<DeviceLedgerInner>,
1055    identity: GrantIdentity,
1056    admission_request_id: AdmissionRequestId,
1057    grant_bytes: u64,
1058    /// Lock-free projection of the grant row's `resident_owner_bytes`, written
1059    /// under the ledger lock together with the row.
1060    resident_owner_bytes: std::sync::atomic::AtomicU64,
1061    next_resident_owner_id: std::sync::atomic::AtomicU64,
1062    /// Backing device pointers per resident owner. The report commit compares
1063    /// them against the allocation registry before it freezes the grant.
1064    resident_owner_allocations: Mutex<BTreeMap<u64, Arc<[usize]>>>,
1065    /// Set once by `prepare_frozen_resident_charge`, never cleared. Readers
1066    /// use it to decide whether a resident owner's release must wait on its
1067    /// completion fences.
1068    frozen: std::sync::atomic::AtomicBool,
1069    /// The attempt's allocation domain, retained from freeze until final
1070    /// release so the drop below can verify that nothing is still allocated in
1071    /// it when the frozen charge returns to the ledger.
1072    frozen_domain: Mutex<Option<Arc<cudf::QueryMemoryDomain>>>,
1073}
1074
1075impl Drop for MemoryGrantInner {
1076    /// Final release of the grant, after the attempt and every resident owner
1077    /// derived from it are gone. A frozen domain that still reports members
1078    /// or bytes means capacity would be returned while allocations exist in
1079    /// it, so admission on the device is closed before the release.
1080    fn drop(&mut self) {
1081        let ledger = DeviceLedger { inner: Arc::clone(&self.ledger) };
1082        // The slot holds at most one domain handle, valid even when a panic
1083        // poisoned its mutex; only the domain's own usage can close the device.
1084        let frozen_cleanup_failure = match self.frozen_domain.get_mut().unwrap_or_else(PoisonError::into_inner) {
1085            Some(domain) => match domain.usage() {
1086                Ok(usage)
1087                    if usage.active_members == 0
1088                        && usage.current_allocations == 0
1089                        && usage.current_bytes == 0
1090                        && usage.current_aligned_bytes == 0 =>
1091                {
1092                    None
1093                },
1094                Ok(usage) => Some(format!(
1095                    "frozen resident domain did not reach zero at final release (domain_id={}, active_members={}, current_allocations={}, current_bytes={}, current_aligned_bytes={})",
1096                    usage.domain_id,
1097                    usage.active_members,
1098                    usage.current_allocations,
1099                    usage.current_bytes,
1100                    usage.current_aligned_bytes
1101                )),
1102                Err(error) => Some(format!("frozen resident domain final usage sample failed: {error}")),
1103            },
1104            None => None,
1105        };
1106        if let Some(reason) = frozen_cleanup_failure
1107            && let Err(error) = ledger.close_admission_if_open(reason)
1108        {
1109            eprintln!("[query-engine] warning: failed to close admission after frozen resident cleanup: {error}");
1110        }
1111        ledger.release_grant(self.identity.generation);
1112    }
1113}
1114
1115impl MemoryGrant {
1116    pub(crate) fn with_resource_epoch(&self, resource_epoch: Arc<QueryServiceResourceEpoch>) -> Self {
1117        Self { inner: Arc::clone(&self.inner), resource_epoch: Some(resource_epoch) }
1118    }
1119
1120    fn check_resource_epoch(&self, operation: &'static str) -> Result<()> {
1121        self.resource_epoch.as_ref().map_or(Ok(()), |epoch| epoch.check_open(operation))
1122    }
1123
1124    /// Unique ledger-local grant id.
1125    pub(crate) fn grant_id(&self) -> u64 {
1126        self.inner.identity.generation
1127    }
1128
1129    /// Immutable physical-attempt identity committed by the selected ledger.
1130    pub fn identity(&self) -> GrantIdentity {
1131        self.inner.identity
1132    }
1133
1134    /// Service-local admission request that owns this grant.
1135    pub fn admission_request_id(&self) -> AdmissionRequestId {
1136        self.inner.admission_request_id
1137    }
1138
1139    pub(crate) fn identity_key(&self) -> usize {
1140        Arc::as_ptr(&self.inner) as usize
1141    }
1142
1143    /// Immutable grant budget in bytes. This value never changes while the
1144    /// grant is live; the grant has no live growth, shrink, borrowing, or
1145    /// revocation.
1146    pub fn budget_bytes(&self) -> u64 {
1147        self.inner.grant_bytes
1148    }
1149
1150    #[cfg(test)]
1151    pub(crate) fn freeze_resident(&self, resident_bytes: u64) -> Result<()> {
1152        self.prepare_frozen_resident_charge(resident_bytes, None)?;
1153        self.publish_frozen_resident()
1154    }
1155
1156    #[cfg(test)]
1157    pub(super) fn prepare_frozen_resident_for_test(&self, resident_bytes: u64) -> Result<()> {
1158        self.prepare_frozen_resident_charge(resident_bytes, None)
1159    }
1160
1161    /// Atomically replaces this active grant with a measured frozen-resident
1162    /// charge and retains its native owner through final release.
1163    ///
1164    /// `resident_bytes` is the allocation registry's own measurement. The
1165    /// resident-owner charge is reconciled to that same measurement so the
1166    /// owner and frozen charges remain one accounting fact.
1167    ///
1168    /// This is the first of three steps run by the attempt's release path:
1169    /// prepare here, `validate_frozen_resident_publish` before the report is
1170    /// sealed, `publish_frozen_resident` after it is visible. Until the
1171    /// publish the ledger still accounts the full active grant.
1172    pub(crate) fn freeze_resident_with_domain(
1173        &self,
1174        resident_bytes: u64,
1175        domain: Arc<cudf::QueryMemoryDomain>,
1176    ) -> Result<()> {
1177        self.reconcile_resident_owner_bytes(resident_bytes)?;
1178        self.prepare_frozen_resident_charge(resident_bytes, Some(domain))
1179    }
1180
1181    /// Rebases resident ownership onto the allocator's measured bytes.
1182    fn reconcile_resident_owner_bytes(&self, measured_bytes: u64) -> Result<()> {
1183        if measured_bytes > self.inner.grant_bytes {
1184            return Err(QueryError::new(
1185                QueryErrorCode::InternalInvariantViolation,
1186                "query_engine.runtime.gpu.memory_grant.reconcile_resident_owner_bytes",
1187                "measured resident bytes exceed the immutable query cap",
1188            )
1189            .with_fact("measured_bytes", measured_bytes)
1190            .with_fact("query_cap_bytes", self.inner.grant_bytes));
1191        }
1192        let ledger = DeviceLedger { inner: Arc::clone(&self.inner.ledger) };
1193        let mut state = ledger.lock_state("reconcile_resident_owner_bytes")?;
1194        let Some(active) = state.grants.get_mut(&self.inner.identity.generation) else {
1195            return Err(QueryError::new(
1196                QueryErrorCode::InternalInvariantViolation,
1197                "query_engine.runtime.gpu.memory_grant.reconcile_resident_owner_bytes",
1198                "query grant is missing from its device ledger",
1199            )
1200            .with_fact("grant_id", self.grant_id()));
1201        };
1202        if active.resident_owner_bytes == measured_bytes {
1203            return Ok(());
1204        }
1205        active.resident_owner_bytes = measured_bytes;
1206        self.inner.resident_owner_bytes.store(measured_bytes, std::sync::atomic::Ordering::Release);
1207        ledger.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::ResidentOwnerCharged);
1208        Ok(())
1209    }
1210
1211    /// Records the pending frozen charge and retains `domain`. Repeating the
1212    /// call with the same bytes and the same domain is a no-op; a different
1213    /// value is an invariant violation, because one grant has one measured
1214    /// resident byte count.
1215    fn prepare_frozen_resident_charge(
1216        &self,
1217        resident_bytes: u64,
1218        domain: Option<Arc<cudf::QueryMemoryDomain>>,
1219    ) -> Result<()> {
1220        if resident_bytes > self.inner.grant_bytes {
1221            return Err(QueryError::new(
1222                QueryErrorCode::InternalInvariantViolation,
1223                "query_engine.runtime.gpu.memory_grant.freeze_resident",
1224                "frozen resident charge must fit the query cap",
1225            )
1226            .with_fact("resident_bytes", resident_bytes)
1227            .with_fact("query_cap_bytes", self.inner.grant_bytes));
1228        }
1229        let ledger = DeviceLedger { inner: Arc::clone(&self.inner.ledger) };
1230        let mut state = ledger.lock_state("freeze_resident")?;
1231        let active = state.grants.get(&self.inner.identity.generation).ok_or_else(|| {
1232            QueryError::new(
1233                QueryErrorCode::InternalInvariantViolation,
1234                "query_engine.runtime.gpu.memory_grant.freeze_resident",
1235                "query grant is missing from its device ledger",
1236            )
1237            .with_fact("grant_id", self.grant_id())
1238        })?;
1239        let existing = active.frozen_resident_bytes.or(active.pending_frozen_resident_bytes);
1240        if let Some(existing_bytes) = existing {
1241            let frozen_domain = self.inner.frozen_domain.lock().map_err(|_error| {
1242                QueryError::new(
1243                    QueryErrorCode::InternalInvariantViolation,
1244                    "query_engine.runtime.gpu.memory_grant.freeze_resident",
1245                    "frozen resident domain owner mutex was poisoned",
1246                )
1247            })?;
1248            let same_domain = match (frozen_domain.as_ref(), domain.as_ref()) {
1249                (Some(existing), Some(domain)) => Arc::ptr_eq(existing, domain),
1250                (None, None) => true,
1251                _ => false,
1252            };
1253            return (existing_bytes == resident_bytes && same_domain).then_some(()).ok_or_else(|| {
1254                QueryError::new(
1255                    QueryErrorCode::InternalInvariantViolation,
1256                    "query_engine.runtime.gpu.memory_grant.freeze_resident",
1257                    "query grant was frozen with a different resident charge",
1258                )
1259                .with_fact("grant_id", self.grant_id())
1260                .with_fact("existing_resident_bytes", existing_bytes)
1261                .with_fact("resident_bytes", resident_bytes)
1262            });
1263        }
1264        state
1265            .grants
1266            .get_mut(&self.inner.identity.generation)
1267            .expect("grant existence was checked under the same ledger lock")
1268            .pending_frozen_resident_bytes = Some(resident_bytes);
1269        if let Some(domain) = domain {
1270            *self.inner.frozen_domain.lock().map_err(|_error| {
1271                QueryError::new(
1272                    QueryErrorCode::InternalInvariantViolation,
1273                    "query_engine.runtime.gpu.memory_grant.freeze_resident",
1274                    "frozen resident domain owner mutex was poisoned",
1275                )
1276            })? = Some(domain);
1277        }
1278        self.inner.frozen.store(true, std::sync::atomic::Ordering::Release);
1279        Ok(())
1280    }
1281
1282    /// Proves the prepared active-to-frozen transition can still be
1283    /// published. Succeeds without effect when it was already published.
1284    ///
1285    /// The publish itself runs after the report is sealed, where a failure can
1286    /// no longer be recorded as a cleanup disposition. Checking the same
1287    /// preconditions before the seal keeps that failure on the reportable side
1288    /// of the boundary.
1289    pub(crate) fn validate_frozen_resident_publish(&self) -> Result<()> {
1290        let ledger = DeviceLedger { inner: Arc::clone(&self.inner.ledger) };
1291        let state = ledger.lock_state("validate_frozen_resident_publish")?;
1292        let active = state.grants.get(&self.inner.identity.generation).ok_or_else(|| {
1293            QueryError::new(
1294                QueryErrorCode::InternalInvariantViolation,
1295                "query_engine.runtime.gpu.memory_grant.validate_frozen_resident_publish",
1296                "prepared frozen resident grant is missing from its device ledger",
1297            )
1298            .with_fact("grant_id", self.grant_id())
1299        })?;
1300        if active.frozen_resident_bytes.is_some() {
1301            return Ok(());
1302        }
1303        let resident_bytes = active.pending_frozen_resident_bytes.ok_or_else(|| {
1304            QueryError::new(
1305                QueryErrorCode::InternalInvariantViolation,
1306                "query_engine.runtime.gpu.memory_grant.validate_frozen_resident_publish",
1307                "frozen resident transition was not prepared",
1308            )
1309            .with_fact("grant_id", self.grant_id())
1310        })?;
1311        state.frozen_resident_bytes.checked_add(resident_bytes).map(|_| ()).ok_or_else(|| {
1312            QueryError::new(
1313                QueryErrorCode::InternalInvariantViolation,
1314                "query_engine.runtime.gpu.memory_grant.validate_frozen_resident_publish",
1315                "frozen resident accounting would overflow",
1316            )
1317            .with_fact("grant_id", self.grant_id())
1318            .with_fact("resident_bytes", resident_bytes)
1319        })
1320    }
1321
1322    /// Closes new admission on this grant's device only, when it is still
1323    /// open. Returns whether this call closed it.
1324    pub(crate) fn close_device_admission(&self, reason: &'static str) -> Result<bool> {
1325        DeviceLedger { inner: Arc::clone(&self.inner.ledger) }.close_admission_if_open(reason)
1326    }
1327
1328    /// Publishes the prepared active-to-frozen transition after the attempt
1329    /// report is visible. Until this commit the ledger continues to account
1330    /// the full active grant, so no released capacity can race report
1331    /// publication. Idempotent once published. This transition frees an
1332    /// active-attempt slot and therefore notifies the admission observers.
1333    pub(crate) fn publish_frozen_resident(&self) -> Result<()> {
1334        let ledger = DeviceLedger { inner: Arc::clone(&self.inner.ledger) };
1335        let mut state = ledger.lock_state("publish_frozen_resident")?;
1336        let active = state.grants.get_mut(&self.inner.identity.generation).ok_or_else(|| {
1337            QueryError::new(
1338                QueryErrorCode::InternalInvariantViolation,
1339                "query_engine.runtime.gpu.memory_grant.publish_frozen_resident",
1340                "prepared frozen resident grant is missing from its device ledger",
1341            )
1342            .with_fact("grant_id", self.grant_id())
1343        })?;
1344        if active.frozen_resident_bytes.is_some() {
1345            return Ok(());
1346        }
1347        let resident_bytes = active.pending_frozen_resident_bytes.take().ok_or_else(|| {
1348            QueryError::new(
1349                QueryErrorCode::InternalInvariantViolation,
1350                "query_engine.runtime.gpu.memory_grant.publish_frozen_resident",
1351                "frozen resident transition was not prepared",
1352            )
1353            .with_fact("grant_id", self.grant_id())
1354        })?;
1355        state.frozen_resident_bytes = state.frozen_resident_bytes.checked_add(resident_bytes).ok_or_else(|| {
1356            QueryError::new(
1357                QueryErrorCode::InternalInvariantViolation,
1358                "query_engine.runtime.gpu.memory_grant.publish_frozen_resident",
1359                "frozen resident accounting overflowed",
1360            )
1361        })?;
1362        state
1363            .grants
1364            .get_mut(&self.inner.identity.generation)
1365            .expect("grant existence was checked under the same ledger lock")
1366            .frozen_resident_bytes = Some(resident_bytes);
1367        let transition = ledger.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::GrantFrozenResident);
1368        // Admission observers wake service queues, so they run outside the ledger lock.
1369        drop(state);
1370        ledger.notify_admission_observers(&transition);
1371        Ok(())
1372    }
1373
1374    pub(crate) fn is_frozen_resident(&self) -> bool {
1375        self.inner.frozen.load(std::sync::atomic::Ordering::Acquire)
1376    }
1377
1378    /// Device ordinal that owns this grant.
1379    pub fn device_ordinal(&self) -> usize {
1380        self.inner.ledger.profile.device_ordinal
1381    }
1382
1383    /// Device-resident bytes currently owned by live values under this grant.
1384    pub(crate) fn resident_owner_bytes(&self) -> u64 {
1385        self.inner.resident_owner_bytes.load(std::sync::atomic::Ordering::Acquire)
1386    }
1387
1388    /// Records newly published device-resident bytes under this grant.
1389    fn add_resident_owner_bytes(&self, bytes: u64) -> Result<()> {
1390        self.check_resource_epoch("query_engine.runtime.gpu.memory_grant.add_resident_owner_bytes")?;
1391        if bytes == 0 {
1392            return Ok(());
1393        }
1394        let ledger = DeviceLedger { inner: Arc::clone(&self.inner.ledger) };
1395        let mut state = ledger.lock_state("add_resident_owner_bytes")?;
1396        let Some(active) = state.grants.get_mut(&self.inner.identity.generation) else {
1397            return Err(QueryError::new(
1398                QueryErrorCode::InternalInvariantViolation,
1399                "query_engine.runtime.gpu.memory_grant.add_resident_owner_bytes",
1400                "resident owner was published after its grant left the device ledger",
1401            ));
1402        };
1403        if active.frozen_resident_bytes.is_some() {
1404            return Err(QueryError::new(
1405                QueryErrorCode::InternalInvariantViolation,
1406                "query_engine.runtime.gpu.memory_grant.add_resident_owner_bytes",
1407                "resident owner was published after the grant was frozen",
1408            ));
1409        }
1410        let next_resident_owner_bytes = active.resident_owner_bytes.checked_add(bytes).ok_or_else(|| {
1411            QueryError::new(
1412                QueryErrorCode::InternalInvariantViolation,
1413                "query_engine.runtime.gpu.memory_grant.add_resident_owner_bytes",
1414                "resident owner byte accounting overflowed",
1415            )
1416        })?;
1417        active.resident_owner_bytes = next_resident_owner_bytes;
1418        self.inner.resident_owner_bytes.store(next_resident_owner_bytes, std::sync::atomic::Ordering::Release);
1419        ledger.bump_sequence_and_notify_locked(&mut state, LedgerTransitionKind::ResidentOwnerCharged);
1420        Ok(())
1421    }
1422
1423    fn release_resident_owner_bytes(&self, bytes: u64) {
1424        if bytes == 0 {
1425            return;
1426        }
1427        let ledger = DeviceLedger { inner: Arc::clone(&self.inner.ledger) };
1428        if ledger.in_transition_observer() {
1429            ledger.panic_on_transition_observer_reentrancy("release_resident_owner_bytes");
1430        }
1431        // Charge mutations update the grant row and its atomic projection under this lock,
1432        // with no panicking operation between writes. Poison still permits this checked
1433        // release; admission continues to reject the poisoned state.
1434        let mut state = ledger.inner.state.lock().unwrap_or_else(PoisonError::into_inner);
1435        self.release_locked(&ledger, &mut state, bytes);
1436    }
1437
1438    /// Applies one exact release while the ledger state lock is held.
1439    fn release_locked(&self, ledger: &DeviceLedger, state: &mut DeviceLedgerState, bytes: u64) {
1440        let Some(active) = state.grants.get_mut(&self.inner.identity.generation) else {
1441            return;
1442        };
1443        let next_resident_owner_bytes = match active.resident_owner_bytes.checked_sub(bytes) {
1444            Some(next) => next,
1445            // Frozen ownership is rebased onto the allocator measurement.
1446            // Aliased owners can therefore release a larger pre-freeze charge.
1447            None if active.frozen_resident_bytes.is_some() || active.pending_frozen_resident_bytes.is_some() => 0,
1448            None => {
1449                eprintln!("[query-engine] warning: resident owner release exceeded its active charge");
1450                return;
1451            },
1452        };
1453        active.resident_owner_bytes = next_resident_owner_bytes;
1454        self.inner.resident_owner_bytes.store(next_resident_owner_bytes, std::sync::atomic::Ordering::Release);
1455        ledger.bump_sequence_and_notify_locked(state, LedgerTransitionKind::ResidentOwnerReleased);
1456    }
1457
1458    /// Records and retains a newly published resident owner.
1459    pub(crate) fn retain_resident_owner(&self, bytes: u64) -> Result<Option<ResidentOwnerLease>> {
1460        if bytes == 0 {
1461            return Ok(None);
1462        }
1463        self.add_resident_owner_bytes(bytes)?;
1464        Ok(Some(self.new_resident_owner_lease(bytes)?))
1465    }
1466
1467    /// Records and retains a resident owner the test publishes itself.
1468    #[cfg(any(test, feature = "test-hooks"))]
1469    #[doc(hidden)]
1470    pub fn retain_resident_owner_for_test(&self, bytes: u64) -> Result<Option<ResidentOwnerLease>> {
1471        self.retain_resident_owner(bytes)
1472    }
1473
1474    fn new_resident_owner_lease(&self, bytes: u64) -> Result<ResidentOwnerLease> {
1475        let owner_id = self.inner.next_resident_owner_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1476        self.inner
1477            .resident_owner_allocations
1478            .lock()
1479            .unwrap_or_else(std::sync::PoisonError::into_inner)
1480            .insert(owner_id, Arc::from([]));
1481        // The derived owner shares only the grant's backend service
1482        // generation, never its query anchor: residents outlive their
1483        // attempt, and retaining the query epoch would stall report
1484        // publication and the frozen-resident handoff. The generation still
1485        // cannot be torn down and reused while any alias retains GPU bytes.
1486        // Derived owners never mint a second ledger or a second generation.
1487        let resource_epoch = self
1488            .resource_epoch
1489            .as_ref()
1490            .map(|epoch| {
1491                epoch.service_generation_only("query_engine.runtime.gpu.memory_grant.new_resident_owner_lease")
1492            })
1493            .transpose()?;
1494        Ok(ResidentOwnerLease {
1495            inner: Arc::new(ResidentOwnerLeaseInner {
1496                grant: MemoryGrant { inner: Arc::clone(&self.inner), resource_epoch },
1497                bytes,
1498                owner_id,
1499                completion_fences: Mutex::new(ResidentCompletionFences::default()),
1500            }),
1501            _reader: None,
1502        })
1503    }
1504
1505    pub(crate) fn published_resident_owner_count(&self) -> usize {
1506        self.inner.resident_owner_allocations.lock().unwrap_or_else(std::sync::PoisonError::into_inner).len()
1507    }
1508
1509    pub(crate) fn published_resident_allocation_pointers(&self) -> std::collections::BTreeSet<usize> {
1510        self.inner
1511            .resident_owner_allocations
1512            .lock()
1513            .unwrap_or_else(std::sync::PoisonError::into_inner)
1514            .values()
1515            .flat_map(|pointers| pointers.iter().copied())
1516            .collect()
1517    }
1518}
1519
1520impl ResidentOwnerLease {
1521    /// Fences this owner's release on the work `stream` has enqueued so far.
1522    /// A later call from the same stream moves its fence to cover the work
1523    /// enqueued since.
1524    pub(crate) fn record_completion(&self, stream: &CudaStream) -> Result<()> {
1525        self.inner.record_completion(stream)
1526    }
1527
1528    /// A lease for a reader on `stream`. When the lease and every clone of it
1529    /// have dropped, the owner's release also waits for the work `stream`
1530    /// had enqueued by then.
1531    pub(crate) fn for_reader(&self, stream: CudaStream) -> Self {
1532        Self {
1533            inner: Arc::clone(&self.inner),
1534            _reader: Some(Arc::new(ResidentReader { owner: Arc::clone(&self.inner), stream })),
1535        }
1536    }
1537
1538    pub(crate) fn set_backing_allocation_pointers(&self, mut pointers: Vec<usize>) {
1539        pointers.retain(|pointer| *pointer != 0);
1540        pointers.sort_unstable();
1541        pointers.dedup();
1542        self.inner
1543            .grant
1544            .inner
1545            .resident_owner_allocations
1546            .lock()
1547            .unwrap_or_else(std::sync::PoisonError::into_inner)
1548            .insert(self.inner.owner_id, Arc::from(pointers));
1549    }
1550
1551    pub(crate) fn is_frozen_resident(&self) -> bool {
1552        self.inner.grant.is_frozen_resident()
1553    }
1554
1555    /// Bytes retained by this handle in the owning memory grant.
1556    pub(crate) fn bytes(&self) -> u64 {
1557        self.inner.bytes
1558    }
1559
1560    /// Whether this retention consumes capacity from `grant`.
1561    pub(crate) fn belongs_to(&self, grant: &MemoryGrant) -> bool {
1562        self.inner.grant.identity_key() == grant.identity_key()
1563    }
1564
1565    /// Whether this owner shares its grant's backend generation lease.
1566    ///
1567    /// A leased owner keeps the device generation alive until its GPU bytes
1568    /// are released. Grants admitted straight from a ledger, without a query
1569    /// service epoch, have no generation to share.
1570    #[cfg(test)]
1571    pub(crate) fn has_resource_epoch(&self) -> bool {
1572        self.inner.grant.resource_epoch.is_some()
1573    }
1574
1575    /// Whether this owner also retains its query anchor.
1576    ///
1577    /// Resident owners must never retain it: the query epoch has to retire
1578    /// for the attempt report and the frozen-resident handoff to publish.
1579    #[cfg(test)]
1580    pub(crate) fn retains_query_anchor(&self) -> bool {
1581        self.inner.grant.resource_epoch.as_ref().is_some_and(|epoch| epoch.has_query_anchor())
1582    }
1583
1584    pub(crate) fn identity_key(&self) -> usize {
1585        Arc::as_ptr(&self.inner) as usize
1586    }
1587
1588    /// Retains a derived output owner under the same grant.
1589    pub(crate) fn retain_derived_owner(&self, bytes: u64) -> Result<Option<Self>> {
1590        self.inner.grant.retain_resident_owner(bytes)
1591    }
1592}
1593
1594impl ResidentOwnerLeaseInner {
1595    fn record_completion(&self, stream: &CudaStream) -> Result<()> {
1596        let operation = "query_engine.runtime.gpu.frozen_resident_lease.completion_fence";
1597        let mut completion_fences = self.completion_fences.lock().map_err(|_error| {
1598            QueryError::new(
1599                QueryErrorCode::InternalInvariantViolation,
1600                operation,
1601                "frozen resident completion fence mutex was poisoned",
1602            )
1603        })?;
1604        // Recorded under the lock, so a stream's newer event never loses to
1605        // an older one recorded concurrently.
1606        let event = stream.record_completion_event().map_err(|error| {
1607            cudf_query_error(operation, "failed to record the resident output's completion event", error)
1608        })?;
1609        let stream_identity = stream.identity();
1610        match completion_fences.fences.iter_mut().find(|fence| fence.stream_identity == stream_identity) {
1611            Some(fence) => fence.event = event,
1612            None => completion_fences.fences.push(ResidentCompletionFence { stream_identity, event }),
1613        }
1614        Ok(())
1615    }
1616
1617    /// Waits for every recorded completion; returns the first failure.
1618    ///
1619    /// An empty fence list is itself a failure: a frozen owner always has at
1620    /// least the producer's fence, recorded when the result handle was bound,
1621    /// so its absence means the release cannot prove the output's work has
1622    /// finished. The wait uses the teardown entry so it still runs on a device
1623    /// already marked unhealthy.
1624    fn wait_for_completions(&mut self) -> Option<String> {
1625        let completion_fences = match self.completion_fences.get_mut() {
1626            Ok(completion_fences) => completion_fences,
1627            Err(_error) => return Some("frozen resident completion fence mutex was poisoned".to_string()),
1628        };
1629        if completion_fences.fences.is_empty() {
1630            return Some("frozen resident owner reached final release without a completion fence".to_string());
1631        }
1632        let mut failure = completion_fences.record_failure.take();
1633        for fence in &completion_fences.fences {
1634            if let Err(error) =
1635                fence.event.synchronize_for_teardown("query_engine.runtime.gpu.frozen_resident_lease.drop")
1636            {
1637                failure.get_or_insert_with(|| format!("frozen resident completion fence failed: {error}"));
1638            }
1639        }
1640        failure
1641    }
1642}
1643
1644impl Drop for ResidentOwnerLeaseInner {
1645    /// Final release of one resident owner. Only a frozen owner waits on its
1646    /// fences: while the grant is still active, the attempt's own stream
1647    /// cleanup synchronizes the streams before the grant can release, and the
1648    /// bytes stay charged to the live grant either way. A fence failure
1649    /// closes admission on the device because the buffers may still be read
1650    /// when their bytes return to the ledger.
1651    fn drop(&mut self) {
1652        let cleanup_failure = if self.grant.is_frozen_resident() { self.wait_for_completions() } else { None };
1653        if let Some(reason) = cleanup_failure {
1654            let ledger = DeviceLedger { inner: Arc::clone(&self.grant.inner.ledger) };
1655            if let Err(error) = ledger.close_admission_if_open(reason) {
1656                eprintln!(
1657                    "[query-engine] warning: failed to close admission after resident completion failure: {error}"
1658                );
1659            }
1660        }
1661        self.grant
1662            .inner
1663            .resident_owner_allocations
1664            .lock()
1665            .unwrap_or_else(std::sync::PoisonError::into_inner)
1666            .remove(&self.owner_id);
1667        self.grant.release_resident_owner_bytes(self.bytes);
1668    }
1669}
1670
1671impl fmt::Debug for MemoryGrant {
1672    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1673        f.debug_struct("MemoryGrant")
1674            .field("identity", &self.inner.identity)
1675            .field("admission_request_id", &self.inner.admission_request_id)
1676            .field("grant_bytes", &self.inner.grant_bytes)
1677            .field("resident_owner_bytes", &self.resident_owner_bytes())
1678            .finish_non_exhaustive()
1679    }
1680}
1681
1682impl fmt::Debug for DeviceLedger {
1683    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1684        f.debug_struct("DeviceLedger")
1685            .field("device_ordinal", &self.inner.profile.device_ordinal)
1686            .field("managed_capacity_bytes", &self.inner.profile.managed_capacity_bytes)
1687            .finish_non_exhaustive()
1688    }
1689}
1690
1691#[cfg(test)]
1692mod tests;