1use std::future::Future;
18use std::sync::Arc;
19use std::task::Poll;
20
21use super::batching::{Batch, Progress, write_data};
25use super::manager::{Domain, Inner, Operation, Pending, Target};
26use super::snapshot::SnapshotAction;
27use super::staging::{FileAction, FileCompleted};
28use super::{RecoveryPosition, StorageManager, StoragePart};
29use crate::{BackendFailure, Error, Result};
30
31pub struct Worker(pub(crate) StorageManager<crate::storage::ErasedCatalog>);
40
41struct Running(Arc<Inner>);
42
43enum Action {
46 Write(Arc<Pending>),
47 Snapshot(SnapshotAction),
48 File(FileAction),
49 Delete(Arc<super::deletion::DeleteWork>),
50 Recovery(Target),
51 Prune(super::StorageCursor),
52}
53
54enum Completed {
55 Write(Progress),
56 Opened(u64, Result<super::BackendSnapshot>),
57 Released(u64),
58 File(FileCompleted),
59 Deleted,
60 Recovery(RecoveryPosition),
61 Pruned(bool),
62}
63
64impl Worker {
69 pub async fn cleanup(&mut self) -> Result<()> {
70 if !self.0.inner.state.lock().unwrap().status.closed {
73 return Err(Error::NotReady { domain: "storage cleanup" });
74 }
75 Ok(())
76 }
77
78 pub async fn run(&mut self) -> Result<()> {
79 let inner = &self.0.inner;
80 let _running = Running(inner.clone());
81 let mut changed = inner.changed.subscribe();
82 let mut next_flush = inner.execution.now() + super::config::FLUSH_INTERVAL;
83 let mut retry_at = None;
84 let mut failures = 0usize;
85 let mut retry_generation = 0;
86 let mut prefer_control = true;
87 let mut prefer_files = true;
88 let mut prefer_pruning = true;
89 let mut prefer_state = true;
90 loop {
91 changed.borrow_and_update();
92 let now = inner.execution.now();
93 let action = {
94 let mut state = inner.state.lock().unwrap();
95 if state.status.closed {
96 return Ok(());
97 }
98 if state.retry != retry_generation {
99 retry_generation = state.retry;
100 failures = 0;
101 retry_at = None;
102 state.status.failure = None;
103 }
104 if retry_at.is_some_and(|deadline| now >= deadline) {
105 retry_at = None;
106 state.status.failure = None;
107 }
108 if now >= next_flush {
109 state.request_flush();
110 next_flush = now + super::config::FLUSH_INTERVAL;
111 }
112 state.expire_files();
113 state.request_deletions();
114 state.collect();
115 if state.queue.bytes() >= inner.settings.target_batch_bytes() {
116 state.request_flush();
117 }
118 let prune_through = if state.snapshot_opening() { 0 } else { state.status.saved.sequence };
120 let mut prune = (inner.has_objects
121 && state.ready
122 && !state.status.closing
123 && state.status.failure.is_none()
124 && prune_through > state.objects_pruned)
125 .then(|| Action::Prune(self.0.position(prune_through)));
126 let data = if prefer_pruning && prune.is_some() {
127 prune.take()
128 } else if !state.ready || state.status.failure.is_some() {
129 None
130 } else if let Some(deletion) = state.deletion_action() {
131 Some(Action::Delete(deletion))
132 } else if let Some(target) = state.recovery_request.filter(|target| target.index <= state.recovery_written.index) {
133 Some(Action::Recovery(target))
134 } else if let Some(boundary) = state.write_boundary() {
135 let mut pending = state.queue.iter().filter(|entry| entry.index <= boundary && state.needs_write(entry));
139 let first = pending.next();
140 let selected =
141 if state.data_request.is_some() && first.is_some_and(|entry| entry.operation.domain() == Domain::Recovery) {
142 pending.find(|entry| entry.operation.domain() == Domain::Data).or(first)
143 } else {
144 first
145 };
146 let ready_state = state
150 .recovery_request
151 .and_then(|target| {
152 state.queue.iter().find(|entry| {
153 entry.index <= target.index && entry.operation.domain() == Domain::Recovery && state.needs_write(entry)
154 })
155 })
156 .filter(|entry| {
157 entry.operation.required_data().is_some_and(|required| state.status.saved.covers(required).unwrap_or(false))
158 });
159 let selected = if prefer_state { ready_state.or(selected) } else { selected };
160 selected.cloned().map(Action::Write)
161 } else {
162 None
163 };
164 let data = data.or(prune);
165 let selected = if prefer_control || data.is_none() {
168 let healthy = state.status.failure.is_none();
169 let file = if prefer_files && healthy { state.file_action().map(Action::File) } else { None };
170 let control = file
171 .or_else(|| state.snapshot_action(healthy).map(Action::Snapshot))
172 .or_else(|| if healthy { state.file_action().map(Action::File) } else { None });
173 if let Some(action) = &control {
174 prefer_files = !matches!(action, Action::File(_));
175 }
176 control.or(data)
177 } else {
178 data
179 };
180 state.pruning_objects = matches!(selected, Some(Action::Prune(_)));
181 if state.pruning_objects {
182 state.objects_prune_owed = false;
183 }
184 selected
185 };
186 if let Some(action) = action {
187 if let Action::Write(entry) = &action {
188 prefer_state = entry.operation.domain() != Domain::Recovery;
189 }
190 prefer_pruning = !matches!(action, Action::Prune(_));
191 prefer_control = !matches!(action, Action::Snapshot(_) | Action::File(_));
192 let result = perform(inner, &action).await;
193 if matches!(action, Action::Prune(_)) {
194 inner.state.lock().unwrap().pruning_objects = false;
195 }
196 if let Ok(Completed::Opened(id, snapshot)) = result {
197 inner.state.lock().unwrap().snapshot_opened(id, snapshot);
198 inner.notify();
199 continue;
200 }
201 match result {
202 Ok(completed) => {
203 failures = 0;
204 let mut state = inner.state.lock().unwrap();
205 state.status.failure = None;
206 match (action, completed) {
207 (Action::Delete(work), Completed::Deleted) => {
208 state.receipts.complete(&work.receipt.mutation, work.receipt.value.clone());
209 state.deletions.remove(&work.receipt.mutation);
210 }
211 (Action::Prune(through), Completed::Pruned(done)) => {
212 if done && !state.objects_prune_owed {
213 state.objects_pruned = through.sequence;
214 }
215 }
216 (Action::File(action), Completed::File(result)) => {
217 state.file_completed(action, result, super::config::FILE_SESSION_TIMEOUT)
218 }
219 (Action::Snapshot(SnapshotAction::Release(..)), Completed::Released(id)) => state.snapshot_removed(id),
220 (Action::Write(_), Completed::Write(progress)) => {
221 if let Some(target) = progress.data {
222 let saved = progress.saved.expect("data write returns durability");
223 let through = if saved.sequence == target.sequence {
224 target.index
225 } else {
226 state
227 .queue
228 .iter()
229 .filter(|entry| {
230 entry.operation.domain() == Domain::Data && entry.operation.sequence() <= saved.sequence
231 })
232 .map(|entry| entry.index)
233 .max()
234 .unwrap_or(state.data_flushed)
235 };
236 state.status.saved = saved;
237 state.data_flushed = through;
238 state.retire_data();
239 if state.data_request.is_some_and(|request| request.index <= through) {
240 state.data_request = None;
241 }
242 }
243 if let Some(target) = progress.recovery {
244 state.recovery_written.index = target.index;
245 state.recovery_written.sequence = state.recovery_written.sequence.max(target.sequence);
246 }
247 }
248 (Action::Recovery(target), Completed::Recovery(saved)) => {
249 if let Some(progress) = &state.journal_saved
250 && let Some(frontier) = state
251 .queue
252 .iter()
253 .filter(|entry| entry.index <= target.index)
254 .filter_map(|entry| entry.journal)
255 .max()
256 {
257 progress.send_replace(frontier);
258 }
259 state.status.recovery_saved = saved;
260 state.recovery_flushed = target.index;
261 state.queue.retire(Domain::Recovery, target.index);
262 state.recovery_request = None;
263 }
264 _ => unreachable!("completion matches its storage action"),
265 }
266 state.collect();
267 }
268 Err(error) => {
269 failures = failures.saturating_add(1);
270 let retries = match &action {
273 Action::Delete(_) => 0,
274 Action::Write(pending)
275 if pending.operation.domain() == Domain::Data && {
276 let state = inner.state.lock().unwrap();
277 state.deleting_data(pending.index)
278 } =>
279 {
280 0
281 }
282 Action::File(file) if file.caller_retries(&inner.state.lock().unwrap()) => 0,
283 _ => super::config::MAX_RETRIES,
284 };
285 retry_at = if transient(&error) && failures <= retries {
286 let multiplier = 1u32.checked_shl((failures - 1).min(31) as u32).unwrap_or(u32::MAX);
287 Some(inner.execution.now() + super::config::RETRY_BASE.saturating_mul(multiplier).min(super::config::RETRY_CAP))
288 } else {
289 None
290 };
291 let source = Arc::new(error);
292 let mut state = inner.state.lock().unwrap();
293 if let Action::File(action) = &action {
294 state.file_failed(action, source.clone(), retry_at.is_some());
295 }
296 match &action {
297 Action::Delete(work) => state.deletion_failed(&work.receipt.mutation, source.clone()),
298 Action::Write(pending) if pending.operation.domain() == Domain::Data => {
299 state.deletion_data_failed(pending.index, source.clone())
300 }
301 _ => {}
302 }
303 state.status.failure = Some(source);
304 }
305 }
306 inner.notify();
307 } else {
308 let deadline = retry_at.map_or(next_flush, |retry| retry.min(next_flush));
309 let deadline = inner.state.lock().unwrap().file_deadline().map_or(deadline, |file| file.min(deadline));
310 let mut sleep = Box::pin(inner.execution.sleep(deadline.saturating_duration_since(inner.execution.now())));
311 let mut wake = Box::pin(changed.changed());
312 let wait = std::future::poll_fn(|cx| {
313 if wake.as_mut().poll(cx).is_ready() || sleep.as_mut().poll(cx).is_ready() { Poll::Ready(()) } else { Poll::Pending }
314 });
315 wait.await;
316 }
317 }
318 }
319}
320
321impl Drop for Running {
322 fn drop(&mut self) {
323 let mut state = self.0.state.lock().unwrap();
324 state.status.running = false;
325 state.pruning_objects = false;
326 if !state.status.closed {
327 state.status.failure = Some(Arc::new(Error::NotReady { domain: "storage worker" }));
328 }
329 self.0.notify();
330 }
331}
332
333impl Operation {
334 pub(super) fn encoded_batch_bytes(&self) -> Result<usize> {
335 let sum = |parts: &[StoragePart<'_>]| -> Result<usize> {
336 parts.iter().flat_map(|part| part.entries()).try_fold(0usize, |bytes, entry| {
337 bytes
338 .checked_add(entry.encoded_bytes()?)
339 .ok_or(Error::InvalidInput { field: "storage admission", reason: "encoded batch size overflow" })
340 })
341 };
342 match self {
343 Self::Schema(value) => sum(&[StoragePart::Schemas(std::slice::from_ref(value))]),
344 Self::ObjectGroup { schemas, objects, .. } => {
345 let mut bytes = 0usize;
346 for (schema, object) in schemas.iter().zip(objects) {
347 let next =
348 sum(&[StoragePart::Schemas(std::slice::from_ref(schema)), StoragePart::Objects(std::slice::from_ref(object))])?;
349 bytes = bytes
350 .checked_add(next)
351 .ok_or(Error::InvalidInput { field: "storage admission", reason: "encoded batch size overflow" })?;
352 }
353 Ok(bytes)
354 }
355 Self::Records(values) => sum(&[StoragePart::Records(values)]),
356 Self::Stream(values) => sum(&[StoragePart::Stream(values)]),
357 Self::File(value) => sum(&[StoragePart::Files(std::slice::from_ref(value))]),
358 Self::Recovery(_) | Self::Snapshot(_) => Ok(0),
359 }
360 }
361
362 pub(super) fn check_entry_bytes(&self, maximum: std::num::NonZeroUsize) -> Result<()> {
363 if self.encoded_batch_bytes()? > maximum.get() {
364 return Err(Error::InvalidInput {
365 field: "storage admission",
366 reason: "atomic batch exceeds max_batch_bytes; split before submission",
367 });
368 }
369 Ok(())
370 }
371
372 pub(super) fn required_data(&self) -> Option<&super::StorageCursor> {
373 match self {
374 Self::Recovery(entries) => entries.iter().max_by_key(|entry| entry.required_data.sequence).map(|entry| &entry.required_data),
375 Self::Snapshot(snapshot) => Some(&snapshot.required_data),
376 _ => None,
377 }
378 }
379
380 pub(super) fn domain(&self) -> Domain {
381 match self {
382 Self::Recovery(_) | Self::Snapshot(_) => Domain::Recovery,
383 _ => Domain::Data,
384 }
385 }
386
387 pub(super) fn sequence(&self) -> u64 {
388 match self {
389 Self::Schema(schema) => schema.position.sequence,
390 Self::File(file) => file.position.sequence,
391 Self::ObjectGroup { objects, .. } => objects.last().unwrap().position.sequence,
392 Self::Records(records) => records.last().unwrap().position.sequence,
393 Self::Stream(rows) => rows.last().unwrap().record.position.sequence,
394 Self::Recovery(entries) => entries.last().unwrap().position.sequence,
395 Self::Snapshot(snapshot) => snapshot.through.sequence,
396 }
397 }
398}
399
400async fn perform(inner: &Arc<Inner>, action: &Action) -> Result<Completed> {
405 match action {
406 Action::Delete(work) => work.perform(inner).await.map(|()| Completed::Deleted),
407 Action::Prune(through) => {
408 inner.bounded(inner.primary.prune_objects(through, super::config::OBJECT_PRUNE_ENTRIES)).await.map(Completed::Pruned)
409 }
410 Action::File(action) => super::staging::perform(inner, action).await.map(Completed::File),
411 Action::Snapshot(SnapshotAction::Open(id, at)) => {
412 let opened = inner.bounded(inner.primary.open_snapshot(at)).await;
413 Ok(Completed::Opened(*id, opened))
414 }
415 Action::Snapshot(SnapshotAction::Release(id, snapshot)) => {
416 inner.bounded(inner.primary.release_snapshot(snapshot)).await?;
417 Ok(Completed::Released(*id))
418 }
419 Action::Write(entry) => {
420 if let Some(batch) = Batch::prepare(inner, entry) {
421 return batch.write(inner).await.map(Completed::Write);
422 }
423 let mut progress = Progress::default();
424 progress.include(entry);
425 let part = match &entry.operation {
426 Operation::File(file) => Some(StoragePart::Files(std::slice::from_ref(file))),
427 Operation::Schema(schema) => Some(StoragePart::Schemas(std::slice::from_ref(schema))),
428 Operation::Records(records) => Some(StoragePart::Records(records)),
429 Operation::ObjectGroup { schemas, objects, .. } => {
430 let mut parts = Vec::with_capacity(schemas.len() * 2);
431 for (schema, object) in schemas.iter().zip(objects) {
432 parts.push(StoragePart::Schemas(std::slice::from_ref(schema)));
433 parts.push(StoragePart::Objects(std::slice::from_ref(object)));
434 }
435 progress.saved = Some(write_data(inner, &parts).await?);
436 return Ok(Completed::Write(progress));
437 }
438 Operation::Stream(rows) => Some(StoragePart::Stream(rows)),
439 Operation::Recovery(entries) => {
440 let saved = inner.state.lock().unwrap().status.saved.clone();
441 for entry in entries {
442 if !saved.covers(&entry.required_data)? {
443 return Err(Error::InvalidInput {
444 field: "state evidence dependency",
445 reason: "journal cannot precede required primary persistence",
446 });
447 }
448 }
449 inner.bounded(inner.recovery.write_entries(entries)).await?;
450 None
451 }
452 Operation::Snapshot(snapshot) => {
453 if !inner.state.lock().unwrap().status.saved.covers(&snapshot.required_data)? {
454 return Err(Error::InvalidInput {
455 field: "state snapshot dependency",
456 reason: "snapshot cannot precede required primary persistence",
457 });
458 }
459 inner.bounded(inner.recovery.write_snapshot(snapshot)).await?;
460 None
461 }
462 };
463 if let Some(part) = part {
464 progress.saved = Some(write_data(inner, &[part]).await?);
465 }
466 Ok(Completed::Write(progress))
467 }
468 Action::Recovery(target) => {
469 let through = RecoveryPosition { history: inner.recovery.history().clone(), sequence: target.sequence };
470 let saved = inner.bounded(inner.recovery.flush(&through)).await?;
471 let written = inner.state.lock().unwrap().recovery_written.sequence;
472 if !saved.covers(&through)? || saved.sequence > written {
473 return Err(Error::InvalidInput {
474 field: "recovery flush receipt",
475 reason: "receipt does not cover the requested written frontier",
476 });
477 }
478 Ok(Completed::Recovery(saved))
479 }
480 }
481}
482
483fn transient(error: &Error) -> bool {
484 match error {
485 Error::Backend { kind: BackendFailure::Transient, .. } => true,
486 Error::Persistence { source } => transient(source),
487 _ => false,
488 }
489}
490
491