Skip to main content

eredu_core/
session_authority.rs

1//! Exact session admission and backend-independent mutable-submission ownership.
2
3use std::sync::{
4    atomic::{AtomicU64, Ordering},
5    Arc,
6};
7
8use crate::SessionCapabilities;
9
10/// Exact capabilities admitted before materialization, not a subset requirement.
11#[derive(Debug, Clone, Copy, Eq, PartialEq)]
12pub struct SessionAdmission {
13    capabilities: SessionCapabilities,
14}
15
16impl SessionAdmission {
17    /// Retains the complete pre-materialization report.
18    pub const fn new(capabilities: SessionCapabilities) -> Self {
19        Self { capabilities }
20    }
21
22    /// Validates the complete realized report before a session is published.
23    pub fn validate(self, realized: SessionCapabilities) -> Result<(), SessionAdmissionError> {
24        if self.capabilities == realized {
25            Ok(())
26        } else {
27            Err(SessionAdmissionError {
28                admitted: self.capabilities,
29                realized,
30            })
31        }
32    }
33}
34
35/// A realized session differs from the exact admitted capability report.
36#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
37#[error("realized session capabilities {realized:?} do not match pre-materialization admission {admitted:?}")]
38pub struct SessionAdmissionError {
39    admitted: SessionCapabilities,
40    realized: SessionCapabilities,
41}
42
43impl SessionAdmissionError {
44    /// Returns the complete admitted report.
45    pub const fn admitted(self) -> SessionCapabilities {
46        self.admitted
47    }
48
49    /// Returns the complete realized report.
50    pub const fn realized(self) -> SessionCapabilities {
51        self.realized
52    }
53}
54
55/// Unique authority for a mutable session and its unresolved submission.
56///
57/// Beginning work requires exclusive access to this non-cloneable owner. The
58/// active ticket is atomic so a completion may resolve on another thread; this
59/// does not make a backend's native session or resources thread-safe. Each
60/// authority allocates once, and each lease shares that allocation.
61#[derive(Debug)]
62pub struct SessionAuthority {
63    active: Arc<AtomicU64>,
64    next_ticket: u64,
65}
66
67impl Default for SessionAuthority {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73impl SessionAuthority {
74    /// Creates idle authority for one session.
75    pub fn new() -> Self {
76        Self {
77            active: Arc::new(AtomicU64::new(0)),
78            next_ticket: 1,
79        }
80    }
81
82    /// Rejects mutation while a submission completion owns this session.
83    pub fn require_idle(&self) -> Result<(), SessionAuthorityError> {
84        if self.active.load(Ordering::Acquire) == 0 {
85            Ok(())
86        } else {
87            Err(SessionAuthorityError::Busy)
88        }
89    }
90
91    /// Begins one submission before native allocation or mutable execution.
92    ///
93    /// An aborted submission releases automatically when its lease is dropped.
94    /// Successful submission moves the lease into its native completion; no
95    /// second independently releasable copy can be created.
96    pub fn begin_submission(&mut self) -> Result<SubmissionLease, SessionAuthorityError> {
97        self.require_idle()?;
98        let ticket = self.next_ticket;
99        self.next_ticket = ticket
100            .checked_add(1)
101            .ok_or(SessionAuthorityError::TicketExhausted)?;
102        self.active.store(ticket, Ordering::Release);
103        Ok(SubmissionLease {
104            owner: Arc::clone(&self.active),
105            ticket,
106        })
107    }
108}
109
110/// Submission exclusion or exhaustion, with no backend error dependency.
111#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
112#[non_exhaustive]
113pub enum SessionAuthorityError {
114    /// Another unresolved completion still owns mutable session state.
115    #[error("model session already owns an unresolved submission completion")]
116    Busy,
117    /// The non-repeating ticket sequence cannot allocate another submission.
118    #[error("model session submission ticket space exhausted")]
119    TicketExhausted,
120}
121
122/// Move-only ownership of one unresolved session submission.
123///
124/// The backend must retain this value until exact native completion, terminal
125/// failure, or safe cancellation/teardown. A pending observation must not
126/// resolve it. Native resources must be drained or safely retained before
127/// dropping it: this portable type neither waits nor cancels native work.
128#[derive(Debug)]
129#[must_use = "the submission lease must be retained until native work is safely resolved"]
130pub struct SubmissionLease {
131    owner: Arc<AtomicU64>,
132    ticket: u64,
133}
134
135impl SubmissionLease {
136    /// Releases only this ticket after the backend establishes safe resolution.
137    ///
138    /// Returns whether this call released it. Repeated resolution and later
139    /// destruction cannot clear any newer submission, even across threads.
140    pub fn resolve(&self) -> bool {
141        self.owner
142            .compare_exchange(self.ticket, 0, Ordering::AcqRel, Ordering::Acquire)
143            .is_ok()
144    }
145}
146
147impl Drop for SubmissionLease {
148    fn drop(&mut self) {
149        self.resolve();
150    }
151}
152
153#[cfg(test)]
154mod tests;