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;