metrale_core/
scope.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Model teardown: ordered, fallible release of state that owns device memory.
4//!
5//! Owner: metrale-core.
6//! Invariants:
7//! - `Teardown::release_all` calls `release` on every registered resource,
8//!   newest first, even after one fails, and leaves the set empty.
9//!
10//! [`ModelResource`] and [`Teardown`] give an ordered, fallible release path,
11//! which `Drop` cannot: it is neither ordered across independent values nor
12//! able to report a failure.
13
14/// 2026-09-25: State that owns device memory and must be released in a defined order.
15///
16/// `Cx` is whatever releasing needs; for GPU state that is the backend
17/// (`dyn GpuBackend`). Making it a type parameter keeps `metrale-core` free of
18/// a dependency on the backend crate while still handing each resource the
19/// thing that owns its memory.
20pub trait ModelResource<Cx: ?Sized>: Send + Sync {
21    /// 2026-09-25: Human name, used to attribute a failure in `Teardown::release_all`'s error.
22    fn label(&self) -> &'static str;
23
24    /// 2026-09-25: Release everything this owns. Must be idempotent.
25    fn release(&mut self, cx: &Cx) -> anyhow::Result<()>;
26}
27
28/// 2026-09-25: Releases a set of resources in reverse registration order, the
29/// inverse of how they were built, which is the only safe order when later
30/// resources borrow earlier ones.
31///
32/// One failure does not abandon the rest: every resource's release is
33/// attempted, and one error naming every failure is returned afterwards.
34pub struct Teardown<Cx: ?Sized> {
35    resources: Vec<Box<dyn ModelResource<Cx>>>,
36}
37
38impl<Cx: ?Sized> Default for Teardown<Cx> {
39    fn default() -> Self {
40        Self {
41            resources: Vec::new(),
42        }
43    }
44}
45
46impl<Cx: ?Sized> Teardown<Cx> {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// 2026-09-25: Register a resource. Callers register in construction order.
52    pub fn push(&mut self, resource: Box<dyn ModelResource<Cx>>) {
53        self.resources.push(resource);
54    }
55
56    pub fn len(&self) -> usize {
57        self.resources.len()
58    }
59
60    pub fn is_empty(&self) -> bool {
61        self.resources.is_empty()
62    }
63
64    /// 2026-09-25: Release everything, newest first. After attempting them all,
65    /// returns one error that lists every failure with its resource's label.
66    pub fn release_all(&mut self, cx: &Cx) -> anyhow::Result<()> {
67        let mut failures = Vec::new();
68        while let Some(mut resource) = self.resources.pop() {
69            if let Err(e) = resource.release(cx) {
70                // 2026-09-25: Every failure is reported, not just the first: after a
71                // partial teardown the operator needs the whole picture to
72                // decide whether the GPU is still usable.
73                failures.push(format!("{}: {e:#}", resource.label()));
74            }
75        }
76        if failures.is_empty() {
77            return Ok(());
78        }
79        Err(anyhow::anyhow!(
80            "{} resource(s) failed to release: {}",
81            failures.len(),
82            failures.join("; ")
83        ))
84    }
85}
86
87#[cfg(test)]
88#[path = "scope_tests.rs"]
89mod tests;