ozzy_replication/
local.rs1mod 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Snapshot {
22 pub journal: JournalSnapshot,
24 pub accepted: Prefix,
26 pub committed: Prefix,
28 pub applied: Prefix,
30 pub pending_operations: usize,
32 pub pending_body_bytes: usize,
34}
35
36#[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 pub const fn configuration(&self) -> Configuration {
52 self.configuration
53 }
54
55 pub const fn limits(&self) -> PipelineLimits {
57 self.limits
58 }
59
60 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 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 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 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 pub fn complete_write(&mut self, ticket: WriteTicket) -> Result<(), Error> {
164 Ok(self.journal.complete_write(ticket)?)
165 }
166
167 pub fn begin_sync(&self) -> Result<SyncTicket, Error> {
169 Ok(self.journal.begin_sync()?)
170 }
171
172 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 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 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)]
223pub enum Error {
225 #[error("invalid single-broker configuration")]
226 Configuration,
228 #[error("invalid single-broker configuration encoding or checksum")]
229 Encoding,
231 #[error("invalid local partition pipeline limits")]
232 Limits,
234 #[error("local partition capacity exhausted")]
235 Capacity,
237 #[error("local partition history mismatch")]
238 Lineage,
240 #[error("stale local partition validation")]
241 StaleValidation,
243 #[error("local application exceeds its durable prefix")]
244 ApplyBeyondDurable,
246 #[error(transparent)]
247 Journal(#[from] ProgressError),
249}