Skip to main content

ozzy_replication/
local.rs

1//! Explicit single-broker authority and local-durable confirmation.
2//!
3//! This driver has no votes, followers, elections, or network events. It shares
4//! canonical validation fences and journal completion tickets with replicated
5//! partitions. A failed replicated group cannot become a local driver.
6
7mod configuration;
8pub use configuration::{CONFIGURATION_BYTES, Configuration};
9#[cfg(test)]
10mod tests;
11
12use crate::driver::ValidationTicket;
13use crate::{
14    JournalGeneration, PipelineLimits, Prefix, PreparedOperation, SyncTicket, WriteTicket,
15};
16use ozzy_journal::progress::{JournalProgress, JournalSnapshot, ProgressError};
17use std::collections::VecDeque;
18
19/// Independent observation of acceptance, persistence, and applied visibility.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Snapshot {
22    /// Exact local written and durable journal progress.
23    pub journal: JournalSnapshot,
24    /// Last admitted canonical operation prefix.
25    pub accepted: Prefix,
26    /// Contiguous locally durable confirmation prefix.
27    pub committed: Prefix,
28    /// Last prefix installed in application state.
29    pub applied: Prefix,
30    /// Accepted operations still awaiting application.
31    pub pending_operations: usize,
32    /// Canonical body bytes retained by those operations.
33    pub pending_body_bytes: usize,
34}
35
36/// Bounded single-broker state. Payloads remain owned by the journal adapter.
37#[derive(Debug)]
38pub struct Driver {
39    configuration: Configuration,
40    journal: JournalProgress,
41    accepted: Prefix,
42    committed: Prefix,
43    applied: Prefix,
44    operations: VecDeque<PreparedOperation>,
45    body_bytes: usize,
46    limits: PipelineLimits,
47}
48
49impl Driver {
50    /// Persisted single-broker identity, independent of current write readiness.
51    pub const fn configuration(&self) -> Configuration {
52        self.configuration
53    }
54
55    /// Count/byte capacity shared by all admitted operations on this partition.
56    pub const fn limits(&self) -> PipelineLimits {
57        self.limits
58    }
59
60    /// Activate only after exact-identity journal recovery has validated and
61    /// synchronized this prefix. A fresh, explicitly formatted store uses genesis.
62    /// Unlike replicated restart, no remote agreement or election exists here.
63    pub fn recover(
64        configuration: Configuration,
65        generation: JournalGeneration,
66        recovered: Prefix,
67        limits: PipelineLimits,
68    ) -> Result<Self, Error> {
69        if generation.0 == 0 || (recovered.op.0 == 0) != (recovered.digest == crate::Digest::ZERO) {
70            return Err(Error::Lineage);
71        }
72        if limits.max_operations == 0
73            || limits.max_operations > u32::MAX as usize
74            || limits.max_body_bytes == 0
75            || limits.max_body_bytes > isize::MAX as usize
76        {
77            return Err(Error::Limits);
78        }
79        Ok(Self {
80            configuration,
81            journal: JournalProgress::recover(generation, recovered.op),
82            accepted: recovered,
83            committed: recovered,
84            applied: recovered,
85            operations: VecDeque::with_capacity(limits.max_operations),
86            body_bytes: 0,
87            limits,
88        })
89    }
90
91    /// Observe local progress without changing authority.
92    pub fn snapshot(&self) -> Snapshot {
93        Snapshot {
94            journal: self.journal.snapshot(),
95            accepted: self.accepted,
96            committed: self.committed,
97            applied: self.applied,
98            pending_operations: self.operations.len(),
99            pending_body_bytes: self.body_bytes,
100        }
101    }
102
103    /// Capture validation authority without accepting or confirming records.
104    pub fn begin_validation(&self) -> Result<ValidationTicket, Error> {
105        let journal = self.journal.snapshot();
106        if journal.faulted {
107            return Err(ProgressError::Faulted.into());
108        }
109        Ok(ValidationTicket::from_local_image(
110            self.configuration.broker(),
111            self.configuration.scope(),
112            journal.generation,
113            self.accepted,
114            self.committed,
115            self.applied,
116        ))
117    }
118
119    /// Admit only a fresh contiguous suffix validated on this exact image.
120    /// Retries resolve in the shared canonical journal before this boundary.
121    pub fn prepare_validated(
122        &mut self,
123        ticket: ValidationTicket,
124        operations: &[PreparedOperation],
125    ) -> Result<WriteTicket, Error> {
126        if !ticket.same_images_as(self.begin_validation()?) {
127            return Err(Error::StaleValidation);
128        }
129        if operations.is_empty() {
130            return Err(ProgressError::EmptyGroup.into());
131        }
132        if operations.len() > self.limits.max_operations - self.operations.len() {
133            return Err(Error::Capacity);
134        }
135        let mut end = self.accepted;
136        let mut bytes = 0usize;
137        let scope = self.configuration.scope();
138        for operation in operations {
139            if operation.group_id != scope.group_id
140                || operation.configuration_epoch != scope.configuration_epoch
141                || operation.original_view != 0
142                || end.op.0.checked_add(1) != Some(operation.prefix.op.0)
143                || operation.previous_digest != end.digest
144            {
145                return Err(Error::Lineage);
146            }
147            bytes = bytes
148                .checked_add(operation.body_bytes)
149                .ok_or(Error::Capacity)?;
150            end = operation.prefix;
151        }
152        if bytes > self.limits.max_body_bytes - self.body_bytes {
153            return Err(Error::Capacity);
154        }
155        let write = self.journal.admit(operations.len() as u64)?;
156        self.operations.extend(operations);
157        self.body_bytes += bytes;
158        self.accepted = end;
159        Ok(write)
160    }
161
162    /// Physical write completion alone never confirms or exposes records.
163    pub fn complete_write(&mut self, ticket: WriteTicket) -> Result<(), Error> {
164        Ok(self.journal.complete_write(ticket)?)
165    }
166
167    /// Capture a generation-fenced durability ticket for the written prefix.
168    pub fn begin_sync(&self) -> Result<SyncTicket, Error> {
169        Ok(self.journal.begin_sync()?)
170    }
171
172    /// Called after both the data barrier and exact durable-prefix evidence
173    /// complete. Later writes cannot extend this captured confirmation boundary.
174    pub fn complete_sync(&mut self, ticket: SyncTicket) -> Result<(), Error> {
175        self.journal.complete_sync(ticket)?;
176        let through = self.journal.snapshot().durable;
177        if through > self.committed.op {
178            self.committed = self
179                .operations
180                .iter()
181                .find(|operation| operation.prefix.op == through)
182                .ok_or(Error::Lineage)?
183                .prefix;
184        }
185        Ok(())
186    }
187
188    /// Release live credit only after the shared journal applies this prefix.
189    /// Application visibility and confirmation replies require this boundary.
190    pub fn apply_through(&mut self, through: Prefix) -> Result<(), Error> {
191        self.begin_validation()?;
192        if through == self.applied {
193            return Ok(());
194        }
195        if through.op <= self.applied.op
196            || through.op > self.committed.op
197            || !self
198                .operations
199                .iter()
200                .any(|operation| operation.prefix == through)
201        {
202            return Err(Error::ApplyBeyondDurable);
203        }
204        while self
205            .operations
206            .front()
207            .is_some_and(|operation| operation.prefix.op <= through.op)
208        {
209            let operation = self.operations.pop_front().expect("checked prefix");
210            self.body_bytes -= operation.body_bytes;
211        }
212        self.applied = through;
213        Ok(())
214    }
215
216    /// Fence uncertain I/O until a new exact-identity recovery and generation.
217    pub fn fail(&mut self, generation: JournalGeneration) -> Result<(), Error> {
218        Ok(self.journal.fail(generation)?)
219    }
220}
221
222#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
223/// Rejected local authority, capacity, lineage, or journal progress.
224pub enum Error {
225    #[error("invalid single-broker configuration")]
226    /// Identity, principal, or configuration epoch is invalid.
227    Configuration,
228    #[error("invalid single-broker configuration encoding or checksum")]
229    /// Persistent configuration bytes or checksum are invalid.
230    Encoding,
231    #[error("invalid local partition pipeline limits")]
232    /// Pipeline operation or body bounds are invalid.
233    Limits,
234    #[error("local partition capacity exhausted")]
235    /// Local pending operation or byte capacity is full.
236    Capacity,
237    #[error("local partition history mismatch")]
238    /// Canonical history does not extend the accepted prefix.
239    Lineage,
240    #[error("stale local partition validation")]
241    /// Validation belongs to obsolete local authority or history.
242    StaleValidation,
243    #[error("local application exceeds its durable prefix")]
244    /// Application would advance beyond synchronized history.
245    ApplyBeyondDurable,
246    #[error(transparent)]
247    /// The journal progress contract rejected the transition.
248    Journal(#[from] ProgressError),
249}