1use super::durability::{CommitTier, DurabilityMode};
7use super::lock::StoreLock;
8use super::{AccessMode, OpenOptions, Store, StoreError};
9use crate::ingest::{DeleteBatch, DocId, IngestBatch, IngestDocument};
10use crate::meta::Predicate;
11use crate::vfs::{StdVfs, SyncKind, Vfs};
12use std::collections::BTreeMap;
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicU64, Ordering};
15use xxhash_rust::xxh3::xxh3_64;
16mod portable;
17mod relocate;
18pub use portable::NamespaceRootId;
19pub use relocate::namespace_relocate;
20#[cfg(any(test, feature = "test-seams"))]
21pub use relocate::namespace_relocate_with_steps;
22
23pub(crate) struct PrivatePreparation {
25 root_id: NamespaceRootId,
26 name: String,
27 directory: PathBuf,
28}
29
30impl PrivatePreparation {
31 fn new(
32 root_id: NamespaceRootId,
33 name: &str,
34 directory: &Path,
35 _coordinator: &StoreLock,
36 ) -> Self {
37 Self {
38 root_id,
39 name: name.to_owned(),
40 directory: directory.to_path_buf(),
41 }
42 }
43}
44
45pub(super) fn validate_private_preparation(
46 vfs: &dyn Vfs,
47 directory: &Path,
48 authority: Option<&PrivatePreparation>,
49) -> Result<(), StoreError> {
50 if portable::authority_with_preparation(vfs, directory, authority)?.is_none() {
51 return Err(invalid(
52 directory,
53 "private preparation requires portable authority",
54 ));
55 }
56 Ok(())
57}
58
59const RECORD: &str = ".ze-namespaces";
60const REFERENCE: &str = ".ze-namespace-root";
61const PREPARED: &str = ".ze-prepared";
62const MAGIC: &[u8] = b"ZENS0001";
63const MAX_RECORD: usize = 1024 * 1024;
64static NEXT: AtomicU64 = AtomicU64::new(0);
65type Routes = BTreeMap<String, String>;
66
67pub struct NamespaceMutation {
69 pub name: String,
71 pub options: OpenOptions,
73 pub upserts: Vec<IngestDocument>,
75 pub deletes: Vec<DocId>,
77 pub delete_where: Option<Predicate>,
79}
80
81pub struct LiveNamespaceMutation<'a> {
83 pub store: &'a Store,
85 pub mutation: NamespaceMutation,
87}
88
89pub fn namespace_batch_live(
91 root: &Path,
92 participants: Vec<LiveNamespaceMutation<'_>>,
93) -> Result<Vec<u64>, StoreError> {
94 execute_live(root, participants, &StdVfs, &mut |_| Ok(()), None)
95}
96
97#[cfg(any(test, feature = "test-seams"))]
99#[doc(hidden)]
100pub fn namespace_batch_live_with_steps(
101 root: &Path,
102 participants: Vec<LiveNamespaceMutation<'_>>,
103 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
104) -> Result<Vec<u64>, StoreError> {
105 execute_live(root, participants, &StdVfs, step, None)
106}
107
108#[cfg(any(test, feature = "test-seams"))]
110#[doc(hidden)]
111pub fn namespace_batch_live_on_vfs(
112 root: &Path,
113 participants: Vec<LiveNamespaceMutation<'_>>,
114 vfs: &dyn Vfs,
115) -> Result<Vec<u64>, StoreError> {
116 execute_live(root, participants, vfs, &mut |_| Ok(()), None)
117}
118
119fn execute_live(
120 root: &Path,
121 mut participants: Vec<LiveNamespaceMutation<'_>>,
122 vfs: &dyn Vfs,
123 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
124 coordinator: Option<StoreLock>,
125) -> Result<Vec<u64>, StoreError> {
126 use crate::ingest::wal_payload::{self, TransactionBinding};
127 use std::sync::Arc;
128 let root = std::fs::canonicalize(root).map_err(|e| io(root, e))?;
129 if !(2..=128).contains(&participants.len()) {
130 return Err(invalid(
131 &root,
132 "namespace batch requires 2..128 participants",
133 ));
134 }
135 let _coordinator = match coordinator {
136 Some(lock) => lock,
137 None => StoreLock::acquire(&root).map_err(StoreError::Lock)?,
138 };
139 reclamation::run(vfs, &root, step)?;
140 let deleting = participants
141 .iter()
142 .any(|p| !p.mutation.deletes.is_empty() || p.mutation.delete_where.is_some());
143 if deleting {
144 reclamation::require_retired_erased(vfs, &root)?;
145 }
146 normalize_accepted(vfs, &root, step)?;
147 let (mut root_id, existing_descriptor) = root_record_vfs(vfs, &root)?;
148 if read_optional_vfs(vfs, &root.join(RECORD))?.is_none() {
149 root_id = Some(NamespaceRootId::generate().map_err(|e| io(&root, e))?);
150 }
151 let existing_routes = match existing_descriptor {
152 RootDescriptor::Legacy(routes) => routes,
153 RootDescriptor::Staged(_) => return Err(invalid(&root, "pending adoption")),
154 };
155 let order = participants
156 .iter()
157 .enumerate()
158 .map(|(index, p)| (p.mutation.name.clone(), index))
159 .collect::<BTreeMap<_, _>>();
160 if order.len() != participants.len() {
161 return Err(invalid(&root, "duplicate participant"));
162 }
163 let mut identities = std::collections::BTreeSet::new();
164 for (name, index) in &order {
165 let p = participants
166 .get(*index)
167 .ok_or_else(|| invalid(&root, "participant index"))?;
168 if !name_valid(name) || p.mutation.options.access_mode != AccessMode::ReadWrite {
169 return Err(invalid(&root, "invalid or read-only participant"));
170 }
171 let logical = root.join(name);
172 if std::fs::symlink_metadata(&logical)
173 .map_err(|e| io(&logical, e))?
174 .file_type()
175 .is_symlink()
176 {
177 return Err(invalid(
178 &logical,
179 "namespace links are not transaction participants",
180 ));
181 }
182 let selected = std::fs::canonicalize(resolve(&logical)?).map_err(|e| io(&logical, e))?;
183 let directory = std::fs::canonicalize(&p.store.directory).map_err(|e| io(&logical, e))?;
184 if selected != directory || !identities.insert(directory) {
185 return Err(invalid(&logical, "foreign or aliased namespace handle"));
186 }
187 }
188 let ordered = order
191 .values()
192 .map(|index| {
193 participants
194 .get(*index)
195 .ok_or_else(|| invalid(&root, "participant index"))
196 })
197 .collect::<Result<Vec<_>, _>>()?;
198 let mut maintenance = Vec::new();
199 let mut states = Vec::new();
200 let mut owners = Vec::new();
201 let mut wals = Vec::new();
202 let mut actives = Vec::new();
203 for p in &ordered {
204 maintenance.push(
205 p.store
206 .maintenance
207 .lock()
208 .map_err(|_| invalid(&root, "maintenance lock"))?,
209 );
210 }
211 for p in &ordered {
212 let state = p
213 .store
214 .state
215 .lock()
216 .map_err(|_| invalid(&root, "state lock"))?;
217 match *state {
218 super::StoreState::Open => {}
219 super::StoreState::Closing => return Err(StoreError::Closing),
220 super::StoreState::Closed => return Err(StoreError::Closed),
221 }
222 states.push(state);
223 }
224 for p in &ordered {
225 let owner = p
226 .store
227 .writer_lock
228 .lock()
229 .map_err(|_| invalid(&root, "writer lock"))?;
230 if owner.is_none() {
231 return Err(StoreError::ReadOnly);
232 }
233 owners.push(owner);
234 }
235 for p in &ordered {
236 wals.push(
237 p.store
238 .wal_writer
239 .lock()
240 .map_err(|_| invalid(&root, "WAL lock"))?,
241 );
242 }
243 for p in &ordered {
244 actives.push(
245 p.store
246 .active
247 .lock()
248 .map_err(|_| invalid(&root, "active lock"))?,
249 );
250 }
251 let mut source_snapshots = Vec::new();
252 for ((p, wal), active) in ordered.iter().zip(&wals).zip(&actives) {
254 wal.as_ref()
257 .ok_or(StoreError::ReadOnly)?
258 .manifest_publication()?
259 .complete();
260 if read_optional_vfs(
261 p.store.vfs.as_ref(),
262 &p.store.directory.join(crate::ingest::PURGE_INTENT_FILE),
263 )?
264 .is_some()
265 {
266 return Err(StoreError::StoreBusy {
267 path: p.store.directory.clone(),
268 });
269 }
270 active.as_ref().ok_or(StoreError::Closed)?;
271 if !p.mutation.deletes.is_empty() || p.mutation.delete_where.is_some() {
272 p.store.require_no_snapshot_views()?;
273 }
274 let snapshot = p
275 .store
276 .snapshot
277 .read()
278 .map_err(|_| invalid(&root, "snapshot lock"))?
279 .as_ref()
280 .cloned()
281 .ok_or(StoreError::Closed)?;
282 super::resolve_open_schema(
283 true,
284 &snapshot,
285 p.mutation.options.schema.as_ref(),
286 AccessMode::ReadOnly,
287 )?;
288 super::validate_epoch_identity(
289 p.store.epoch_identity(),
290 p.mutation
291 .options
292 .epoch
293 .as_ref()
294 .map(crate::epoch::StoreEpoch::identity),
295 true,
296 AccessMode::ReadOnly,
297 )?;
298 let tokenizer = crate::fts::tokenizer::Analyzer::new(
299 p.mutation
300 .options
301 .tokenizer
302 .clone()
303 .unwrap_or_else(crate::fts::tokenizer::TokenizerConfig::text_default),
304 )
305 .map_err(StoreError::Tokenizer)?;
306 if tokenizer.config() != p.store.tokenizer.config() {
307 return Err(invalid(&root, "participant tokenizer mismatch"));
308 }
309 source_snapshots.push(snapshot);
310 }
311 let mut staged = Vec::new();
312 for (((p, wal), active), snapshot) in ordered
313 .iter()
314 .zip(&wals)
315 .zip(&actives)
316 .zip(&source_snapshots)
317 {
318 let writer = wal.as_ref().ok_or(StoreError::ReadOnly)?;
319 let current = active.as_ref().ok_or(StoreError::Closed)?;
320 match p
321 .store
322 .stage_namespace(&p.mutation, current, snapshot, writer.durable_end())
323 {
324 Ok(stage) => staged.push(stage),
325 Err(error) => {
326 for (prior, stage) in ordered.iter().zip(staged) {
327 cleanup_stage(prior.store, stage, None)?;
328 }
329 return Err(invalid(&p.store.directory, &error.to_string()));
330 }
331 }
332 }
333 let transaction = (u128::from(std::process::id()) << 96)
334 | (u128::from(
335 std::time::SystemTime::now()
336 .duration_since(std::time::UNIX_EPOCH)
337 .map_err(|_| invalid(&root, "transaction clock"))?
338 .as_nanos() as u64,
339 ) << 32)
340 | u128::from(NEXT.fetch_add(1, Ordering::Relaxed));
341 let mut descriptor = StagedDescriptor(BTreeMap::new(), existing_routes);
342 let mut lengths = Vec::new();
343 let mut selections = Vec::new();
344 for (p, stage) in ordered.iter().zip(&staged) {
345 let wal_path = p.store.directory.join("wal.ze");
346 lengths.push(p.store.vfs.open(&wal_path).map_err(|e| io(&wal_path, e))?);
347 if stage.records.is_empty() {
348 selections.push(None);
349 continue;
350 }
351 let bytes =
352 crate::manifest::encode_manifest(&stage.manifest).map_err(StoreError::Manifest)?;
353 let first_seq = wals
354 .get(selections.len())
355 .and_then(|wal| wal.as_ref())
356 .ok_or(StoreError::ReadOnly)?
357 .durable_end()
358 .checked_add(1)
359 .ok_or(StoreError::GenerationOverflow)?;
360 let binding = TransactionBinding {
361 transaction,
362 participant: match root_id {
363 Some(id) => portable::participant_id(id, &p.mutation.name)?,
364 None => participant_identity(&root, &p.mutation.name)?,
365 },
366 first_seq,
367 last_seq: first_seq
368 .checked_add(stage.records.len() as u64 - 1)
369 .ok_or(StoreError::GenerationOverflow)?,
370 manifest_digest: xxh3_64(&bytes),
371 final_generation: stage.active.generation,
372 };
373 let selection = StagedSelection {
374 manifest: format!(".ze-manifest-{transaction}"),
375 binding,
376 };
377 descriptor
378 .0
379 .insert(p.mutation.name.clone(), selection.clone());
380 selections.push(Some(selection));
381 }
382 if descriptor.0.is_empty() {
383 let generations = ordered
384 .iter()
385 .zip(&staged)
386 .map(|(p, s)| (p.mutation.name.clone(), s.active.generation))
387 .collect::<BTreeMap<_, _>>();
388 return participants
389 .iter()
390 .map(|p| {
391 generations
392 .get(&p.mutation.name)
393 .copied()
394 .ok_or_else(|| invalid(&root, "missing generation"))
395 })
396 .collect();
397 }
398 let decision = encode_root_record(root_id, &RootDescriptor::Staged(descriptor.clone()))?;
399 let prepared = (|| {
400 if root_id.is_some() {
401 if read_optional_vfs(vfs, &root.join(RECORD))?.is_none() {
402 publish(
403 vfs,
404 &root,
405 &encode_root_record(root_id, &RootDescriptor::Legacy(Routes::new()))?,
406 step,
407 )?;
408 step("bootstrap root published").map_err(|e| io(&root, e))?;
409 } else {
410 sync_dir(vfs, &root, step)?;
413 }
414 for p in &ordered {
415 let logical = root.join(&p.mutation.name);
416 let reference = logical.join(REFERENCE);
417 if read_optional_vfs(vfs, &reference)?.is_none() {
418 publish_reference(
419 vfs,
420 &logical,
421 &encode_participant_reference(root_id, &root, &p.mutation.name, 1)?,
422 step,
423 )?;
424 step("participant reference installed").map_err(|e| io(&logical, e))?;
425 }
426 }
427 }
428 for (((p, stage), selection), wal) in ordered
429 .iter()
430 .zip(&mut staged)
431 .zip(&selections)
432 .zip(&mut wals)
433 {
434 let Some(selection) = selection else {
435 continue;
436 };
437 let bytes =
438 crate::manifest::encode_manifest(&stage.manifest).map_err(StoreError::Manifest)?;
439 durable_write(
440 p.store.vfs.as_ref(),
441 &p.store.directory.join(&selection.manifest),
442 &bytes,
443 step,
444 )?;
445 sync_dir(p.store.vfs.as_ref(), &p.store.directory, step)?;
446 let count = u32::try_from(stage.records.len())
447 .map_err(|_| invalid(&root, "prepared member count"))?;
448 let frames = stage
449 .records
450 .iter()
451 .zip(0_u32..)
452 .map(|((op, payload), index)| {
453 wal_payload::encode_prepared(selection.binding, index, count, *op, payload)
454 .map_err(|e| invalid(&root, &e.to_string()))
455 })
456 .collect::<Result<Vec<_>, _>>()?;
457 let refs = frames
458 .iter()
459 .map(|frame| (wal_payload::PREPARED_MUTATION_V1, frame.as_slice()))
460 .collect::<Vec<_>>();
461 wal.as_mut()
462 .ok_or(StoreError::ReadOnly)?
463 .commit_many(&refs)?;
464 step("prepared append").map_err(|e| io(&p.store.directory, e))?;
465 p.store
466 .vfs
467 .sync(&p.store.directory.join("wal.ze"), SyncKind::Full)
468 .map_err(|e| io(&p.store.directory, e))?;
469 step("prepared WAL sync").map_err(|e| io(&p.store.directory, e))?;
470 let active = Arc::get_mut(&mut stage.active.segment)
471 .ok_or_else(|| invalid(&root, "private active ownership"))?;
472 for (row, seq) in stage.rows.iter().zip(selection.binding.first_seq..) {
473 for row in row {
474 active.set_sequence(*row, crate::wal::LogSeq::new(seq))?;
475 }
476 }
477 }
478 if root_id.is_none() {
479 if read_optional_vfs(vfs, &root.join(RECORD))?.is_none() {
480 publish(
481 vfs,
482 &root,
483 &encode_root_record(root_id, &RootDescriptor::Legacy(Routes::new()))?,
484 step,
485 )?;
486 }
487 for p in &ordered {
488 let logical = root.join(&p.mutation.name);
489 let reference = logical.join(REFERENCE);
490 if read_optional_vfs(vfs, &reference)?.is_none() {
491 durable_write(
492 vfs,
493 &reference,
494 &encode_participant_reference(root_id, &root, &p.mutation.name, 1)?,
495 step,
496 )?;
497 sync_dir(vfs, &logical, step)?;
498 }
499 }
500 }
501 Ok::<_, StoreError>(())
502 })();
503 if let Err(error) = prepared {
504 rollback_stages(&ordered, staged, &selections, &lengths, &mut wals)?;
505 return Err(error);
506 }
507 let mut snapshots = Vec::new();
508 for p in &ordered {
509 match p.store.snapshot.write() {
510 Ok(snapshot) => snapshots.push(snapshot),
511 Err(_) => {
512 rollback_stages(&ordered, staged, &selections, &lengths, &mut wals)?;
513 return Err(invalid(&root, "snapshot lock"));
514 }
515 }
516 }
517 let mut publication_attempted = false;
518 let published = (|| {
519 let temporary = root.join(".ze-namespaces.tmp");
520 durable_write(vfs, &temporary, &decision, step)?;
521 let publications = wals
522 .iter()
523 .map(|wal| {
524 wal.as_ref()
525 .ok_or(StoreError::ReadOnly)?
526 .manifest_publication()
527 .map(|publication| publication.arm())
528 })
529 .collect::<Result<Vec<_>, StoreError>>()?;
530 publication_attempted = true;
531 vfs.rename(&temporary, &root.join(RECORD))
532 .map_err(|e| io(&root, e))?;
533 step("commit rename").map_err(|e| io(&root, e))?;
534 sync_dir(vfs, &root, step)?;
535 Ok(publications)
536 })();
537 let mut publications = match published {
538 Ok(publications) => publications,
539 Err(error) => {
540 if publication_attempted {
541 for wal in &mut wals {
542 **wal = None;
543 }
544 } else {
545 rollback_stages(&ordered, staged, &selections, &lengths, &mut wals)?;
546 }
547 return Err(error);
548 }
549 };
550 let adopted = (|| {
553 for ((active, stage), snapshot) in actives.iter_mut().zip(&staged).zip(&mut snapshots) {
554 **active = Some(crate::ingest::ActiveState {
555 generation: stage.active.generation,
556 segment: Arc::clone(&stage.active.segment),
557 });
558 **snapshot = Some(Arc::clone(&stage.snapshot));
559 }
560 step("live states installed").map_err(|e| io(&root, e))?;
561 for ((p, stage), selection) in ordered.iter().zip(&staged).zip(&selections) {
562 if let Some(selection) = selection {
563 accept(
564 p.store.vfs.as_ref(),
565 &p.store.directory,
566 selection,
567 &stage.manifest,
568 step,
569 )?;
570 }
571 }
572 publish(
573 vfs,
574 &root,
575 &encode_root_record(root_id, &RootDescriptor::Legacy(descriptor.1.clone()))?,
576 step,
577 )
578 })();
579 if let Err(error) = adopted {
580 for wal in &mut wals {
581 **wal = None;
582 }
583 return Err(error);
584 }
585 drop(snapshots);
586 drop(actives);
587 let mut completed = BTreeMap::new();
588 for ((p, stage), wal) in ordered.iter().zip(&staged).zip(&mut wals) {
589 let generation = if read_optional_vfs(
590 p.store.vfs.as_ref(),
591 &p.store.directory.join(crate::ingest::PURGE_INTENT_FILE),
592 )?
593 .is_some()
594 {
595 match p
596 .store
597 .complete_namespace_purge_locked(wal.as_mut().ok_or(StoreError::ReadOnly)?)
598 {
599 Ok(generation) => generation,
600 Err(error) => {
601 for wal in &mut wals {
602 **wal = None;
603 }
604 return Err(StoreError::PurgeRecovery {
605 detail: error.to_string(),
606 });
607 }
608 }
609 } else {
610 stage.active.generation
611 };
612 publications.push(
615 wal.as_ref()
616 .ok_or(StoreError::ReadOnly)?
617 .manifest_publication()?
618 .arm(),
619 );
620 completed.insert(p.mutation.name.clone(), generation);
621 }
622 if deleting {
623 reclamation::run(vfs, &root, step)?;
624 reclamation::require_retired_erased(vfs, &root)?;
625 }
626 for publication in publications {
627 publication.complete();
628 }
629 let by_name = completed;
630 drop(wals);
631 drop(owners);
632 drop(states);
633 drop(maintenance);
634 drop(ordered);
635 participants
636 .iter_mut()
637 .map(|p| {
638 by_name
639 .get(&p.mutation.name)
640 .copied()
641 .ok_or_else(|| invalid(&root, "missing generation"))
642 })
643 .collect()
644}
645
646fn cleanup_stage(
647 store: &Store,
648 stage: crate::ingest::NamespaceStage,
649 selection: Option<&StagedSelection>,
650) -> Result<(), StoreError> {
651 let mut paths = stage
652 .replacements
653 .iter()
654 .map(|id| store.directory.join(id.file_name()))
655 .collect::<Vec<_>>();
656 if let Some(selection) = selection {
657 paths.push(store.directory.join(&selection.manifest));
658 }
659 for path in paths {
660 match store.vfs.delete(&path) {
661 Ok(()) => {}
662 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
663 Err(e) => return Err(io(&path, e)),
664 }
665 }
666 store
667 .vfs
668 .sync(&store.directory, SyncKind::Full)
669 .map_err(|e| io(&store.directory, e))
670}
671
672pub(super) fn io(path: &Path, source: std::io::Error) -> StoreError {
673 StoreError::Io {
674 path: path.to_path_buf(),
675 source,
676 }
677}
678pub(crate) fn invalid(path: &Path, message: &str) -> StoreError {
679 io(
680 path,
681 std::io::Error::new(std::io::ErrorKind::InvalidData, message),
682 )
683}
684pub(super) fn name_valid(name: &str) -> bool {
685 name.as_bytes()
686 .first()
687 .is_some_and(u8::is_ascii_alphanumeric)
688 && name.len() <= 255
689 && name
690 .bytes()
691 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
692}
693pub(super) fn envelope(body: &[u8]) -> Vec<u8> {
694 let mut bytes = MAGIC.to_vec();
695 bytes.extend_from_slice(body);
696 bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
697 bytes
698}
699pub(super) fn body<'a>(path: &Path, bytes: &'a [u8]) -> Result<&'a [u8], StoreError> {
700 let end = bytes
701 .len()
702 .checked_sub(8)
703 .ok_or_else(|| invalid(path, "short namespace record"))?;
704 let prefix = bytes
705 .get(..end)
706 .ok_or_else(|| invalid(path, "namespace record bounds"))?;
707 if bytes.len() > MAX_RECORD
708 || !prefix.starts_with(MAGIC)
709 || bytes.get(end..) != Some(xxh3_64(prefix).to_le_bytes().as_slice())
710 {
711 return Err(invalid(path, "namespace record checksum/version"));
712 }
713 prefix
714 .get(MAGIC.len()..)
715 .ok_or_else(|| invalid(path, "namespace record header"))
716}
717pub(super) fn read_optional(path: &Path) -> Result<Option<Vec<u8>>, StoreError> {
718 match StdVfs.open(path) {
719 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
720 Err(e) => Err(io(path, e)),
721 Ok(n) if n > MAX_RECORD as u64 => Err(invalid(path, "namespace record too large")),
722 Ok(_) => StdVfs.read(path).map(Some).map_err(|e| io(path, e)),
723 }
724}
725fn encode(routes: &Routes) -> Vec<u8> {
726 let mut text = String::new();
729 for (name, destination) in routes {
730 text.push_str(name);
731 text.push('\t');
732 text.push_str(destination);
733 text.push('\n');
734 }
735 envelope(text.as_bytes())
736}
737#[derive(Clone, Debug, Eq, PartialEq)]
738enum RootDescriptor {
739 Legacy(Routes),
740 Staged(StagedDescriptor),
741}
742fn routes(root: &Path) -> Result<Routes, StoreError> {
743 match root_descriptor_vfs(&StdVfs, root)? {
744 RootDescriptor::Legacy(routes) => Ok(routes),
745 RootDescriptor::Staged(_) => Err(invalid(
746 &root.join(RECORD),
747 "staged manifest requires transaction adoption",
748 )),
749 }
750}
751fn root_descriptor(root: &Path) -> Result<RootDescriptor, StoreError> {
752 root_descriptor_vfs(&StdVfs, root)
753}
754fn root_descriptor_vfs(vfs: &dyn Vfs, root: &Path) -> Result<RootDescriptor, StoreError> {
755 root_record_vfs(vfs, root).map(|(_, descriptor)| descriptor)
756}
757fn encode_root_record(
758 id: Option<NamespaceRootId>,
759 descriptor: &RootDescriptor,
760) -> Result<Vec<u8>, StoreError> {
761 match id {
762 Some(id) => portable::encode_root(id, descriptor),
763 None => match descriptor {
764 RootDescriptor::Legacy(routes) => Ok(encode(routes)),
765 RootDescriptor::Staged(staged) => encode_staged(staged),
766 },
767 }
768}
769
770fn encode_participant_reference(
771 id: Option<NamespaceRootId>,
772 root: &Path,
773 name: &str,
774 parent_depth: u8,
775) -> Result<Vec<u8>, StoreError> {
776 match id {
777 Some(root_id) => portable::encode_reference(&portable::Reference {
778 root_id,
779 parent_depth,
780 name: name.to_owned(),
781 }),
782 None => Ok(envelope(
783 root.to_str()
784 .ok_or_else(|| invalid(root, "root UTF-8"))?
785 .as_bytes(),
786 )),
787 }
788}
789
790fn root_record_vfs(
791 vfs: &dyn Vfs,
792 root: &Path,
793) -> Result<(Option<NamespaceRootId>, RootDescriptor), StoreError> {
794 let path = root.join(RECORD);
795 let Some(bytes) = read_optional_vfs(vfs, &path)? else {
796 return Ok((None, RootDescriptor::Legacy(Routes::new())));
797 };
798 if bytes.starts_with(portable::ROOT_MAGIC) {
799 return portable::decode_root(&path, &bytes).map(|(id, descriptor)| (Some(id), descriptor));
800 }
801 decode_descriptor(&path, &bytes).map(|descriptor| (None, descriptor))
802}
803fn decode_descriptor(path: &Path, bytes: &[u8]) -> Result<RootDescriptor, StoreError> {
804 if bytes.starts_with(STAGED_MAGIC) {
805 return decode_staged(path, bytes).map(RootDescriptor::Staged);
806 }
807 decode_routes(path, bytes).map(RootDescriptor::Legacy)
808}
809fn decode_routes(path: &Path, bytes: &[u8]) -> Result<Routes, StoreError> {
810 let text = std::str::from_utf8(body(path, bytes)?)
811 .map_err(|_| invalid(path, "namespace record UTF-8"))?;
812 let mut result = Routes::new();
813 for line in text.split_terminator('\n') {
814 let (name, destination) = line
815 .split_once('\t')
816 .ok_or_else(|| invalid(path, "namespace route"))?;
817 let (transaction, child) = destination
818 .split_once('/')
819 .ok_or_else(|| invalid(path, "namespace destination"))?;
820 if !name_valid(name)
821 || child != name
822 || !transaction.starts_with(".ze-batch-")
823 || !transaction
824 .bytes()
825 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'.')
826 || transaction.contains("..")
827 || result
828 .insert(name.to_owned(), destination.to_owned())
829 .is_some()
830 {
831 return Err(invalid(path, "invalid or duplicate namespace route"));
832 }
833 }
834 if encode(&result) != bytes {
835 return Err(invalid(path, "noncanonical namespace record"));
836 }
837 Ok(result)
838}
839
840pub(super) fn validate_store_path(path: &Path) -> Result<(), StoreError> {
842 match std::fs::metadata(path) {
843 Ok(metadata) if !metadata.is_dir() => {
844 return Err(StoreError::NotDirectory {
845 path: path.to_path_buf(),
846 });
847 }
848 Ok(_) => {}
849 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
850 Err(error) => return Err(io(path, error)),
851 }
852 Ok(())
853}
854
855pub(super) fn resolve(path: &Path) -> Result<PathBuf, StoreError> {
857 validate_store_path(path)?;
858 let Some(parent) = path.parent() else {
859 return Ok(path.to_path_buf());
860 };
861 let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
862 return Ok(path.to_path_buf());
863 };
864 let portable = portable::authority(&StdVfs, path)?;
865 let parent = portable.map_or(parent, |(root, _, _)| root);
866 let reference = if portable.is_none() {
867 read_optional(&path.join(REFERENCE))?
868 } else {
869 None
870 };
871 if let Some(reference) = reference {
872 let expected = body(&path.join(REFERENCE), &reference)?;
873 let canonical = std::fs::canonicalize(parent).map_err(|e| io(parent, e))?;
874 if canonical.to_str().map(str::as_bytes) != Some(expected)
875 || read_optional(&parent.join(RECORD))?.is_none()
876 {
877 return Err(invalid(
878 path,
879 "namespace requires its original transaction root",
880 ));
881 }
882 }
883 let selected = match root_descriptor(parent)? {
884 RootDescriptor::Legacy(mut routes) => routes.remove(name),
885 RootDescriptor::Staged(mut descriptor) => descriptor.1.remove(name),
886 };
887 match selected {
888 None => Ok(path.to_path_buf()),
889 Some(destination) => {
890 let selected = parent.join(&destination);
891 if let Some((_, _, id)) = portable
892 && portable::authority(&StdVfs, &selected)?.map(|(_, _, selected_id)| selected_id)
893 != Some(id)
894 {
895 return Err(invalid(&selected, "foreign portable route participant"));
896 }
897 let transaction = selected
898 .parent()
899 .ok_or_else(|| invalid(path, "missing transaction directory"))?;
900 let intent_path = transaction.join("intent.ze");
901 let intent = read_optional(&intent_path)?
902 .ok_or_else(|| invalid(&intent_path, "missing transaction intent"))?;
903 let intent_body = std::str::from_utf8(body(&intent_path, &intent)?)
904 .map_err(|_| invalid(&intent_path, "intent UTF-8"))?;
905 if !intent_body
906 .lines()
907 .any(|line| line == format!("{name}\t{destination}"))
908 {
909 return Err(invalid(
910 &intent_path,
911 "decision disagrees with preparation intent",
912 ));
913 }
914 let prepared_path = selected.join(PREPARED);
915 let bytes = read_optional(&prepared_path)?
916 .ok_or_else(|| invalid(&prepared_path, "missing namespace preparation"))?;
917 if body(&prepared_path, &bytes)? != destination.as_bytes() {
918 return Err(invalid(
919 &prepared_path,
920 "namespace preparation identity mismatch",
921 ));
922 }
923 for filename in ["manifest.ze", "wal.ze"] {
927 let required = selected.join(filename);
928 let length = StdVfs.open(&required).map_err(|e| io(&required, e))?;
929 if length == 0 {
930 return Err(invalid(&required, "empty committed preparation file"));
931 }
932 }
933 Ok(selected)
934 }
935 }
936}
937
938pub(super) fn durable_write(
939 vfs: &dyn Vfs,
940 path: &Path,
941 bytes: &[u8],
942 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
943) -> Result<(), StoreError> {
944 vfs.write(path, bytes).map_err(|e| io(path, e))?;
945 step("write").map_err(|e| io(path, e))?;
946 vfs.sync(path, SyncKind::Full).map_err(|e| io(path, e))?;
947 step("file sync").map_err(|e| io(path, e))
948}
949fn publish_reference(
951 vfs: &dyn Vfs,
952 directory: &Path,
953 bytes: &[u8],
954 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
955) -> Result<(), StoreError> {
956 let temporary = directory.join(".ze-namespace-root.tmp");
957 let reference = directory.join(REFERENCE);
958 durable_write(vfs, &temporary, bytes, step)?;
959 vfs.rename(&temporary, &reference)
960 .map_err(|e| io(&reference, e))?;
961 step("reference rename").map_err(|e| io(&reference, e))?;
962 sync_dir(vfs, directory, step)
963}
964
965pub(super) fn sync_dir(
966 vfs: &dyn Vfs,
967 path: &Path,
968 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
969) -> Result<(), StoreError> {
970 vfs.sync(path, SyncKind::Full).map_err(|e| io(path, e))?;
971 step("directory sync").map_err(|e| io(path, e))
972}
973
974pub(super) fn sync_portable_publication_for_open(
975 vfs: &dyn Vfs,
976 directory: &Path,
977) -> Result<(), StoreError> {
978 if let Some((root, _, _)) = portable::authority(vfs, directory)? {
979 if let Some(parent) = root.parent() {
982 let parent = if parent.as_os_str().is_empty() {
983 Path::new(".")
984 } else {
985 parent
986 };
987 sync_dir(vfs, parent, &mut |_| Ok(()))?;
988 }
989 sync_dir(vfs, root, &mut |_| Ok(()))?;
990 }
991 Ok(())
992}
993
994fn publish(
995 vfs: &dyn Vfs,
996 root: &Path,
997 bytes: &[u8],
998 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
999) -> Result<(), StoreError> {
1000 let temporary = root.join(".ze-namespaces.tmp");
1001 durable_write(vfs, &temporary, bytes, step)?;
1002 vfs.rename(&temporary, &root.join(RECORD))
1003 .map_err(|e| io(root, e))?;
1004 step("commit rename").map_err(|e| io(root, e))?;
1005 sync_dir(vfs, root, step)
1006}
1007
1008fn prepare_copy(
1012 source: &Store,
1013 destination: &Path,
1014 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1015) -> Result<(), StoreError> {
1016 StdVfs
1017 .create_directory(destination)
1018 .map_err(|e| io(destination, e))?;
1019 step("participant directory").map_err(|e| io(destination, e))?;
1020 for path in source
1021 .vfs
1022 .list(&source.directory)
1023 .map_err(|e| io(&source.directory, e))?
1024 {
1025 let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
1026 return Err(invalid(&path, "store filename is not UTF-8"));
1027 };
1028 let acceptance = name
1029 .strip_prefix(".ze-accepted-")
1030 .is_some_and(|transaction| transaction.parse::<u128>().is_ok());
1031 if name != "manifest.ze" && name != "wal.ze" && !name.ends_with(".zseg") && !acceptance {
1032 continue;
1033 }
1034 let length = source.vfs.open(&path).map_err(|e| io(&path, e))?;
1035 let target = destination.join(name);
1036 let mut file = StdVfs.open_append(&target).map_err(|e| io(&target, e))?;
1037 let mut offset = 0;
1038 while offset < length {
1039 let count = usize::try_from((length - offset).min(1024 * 1024))
1040 .map_err(|_| invalid(&path, "copy size"))?;
1041 let bytes = source
1042 .vfs
1043 .read_range(&path, offset, count)
1044 .map_err(|e| io(&path, e))?;
1045 if bytes.len() != count {
1046 return Err(invalid(&path, "short preparation read"));
1047 }
1048 file.append(&bytes).map_err(|e| io(&target, e))?;
1049 offset += bytes.len() as u64;
1050 step("copy chunk").map_err(|e| io(&target, e))?;
1051 }
1052 file.sync(SyncKind::Full).map_err(|e| io(&target, e))?;
1053 step("copied file sync").map_err(|e| io(&target, e))?;
1054 }
1055 sync_dir(&StdVfs, destination, step)
1056}
1057
1058pub fn namespace_batch(
1067 root: &Path,
1068 mutations: Vec<NamespaceMutation>,
1069) -> Result<Vec<u64>, StoreError> {
1070 execute(root, mutations, Operation::Batch, &mut |_| Ok(()))
1071}
1072
1073#[cfg(any(test, feature = "test-seams"))]
1075#[doc(hidden)]
1076pub fn namespace_batch_with_steps(
1077 root: &Path,
1078 mutations: Vec<NamespaceMutation>,
1079 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1080) -> Result<Vec<u64>, StoreError> {
1081 execute(root, mutations, Operation::Batch, step)
1082}
1083
1084pub fn namespace_declare_cascade(
1087 root: &Path,
1088 participants: Vec<NamespaceMutation>,
1089 rule: super::CascadeRule,
1090) -> Result<(), StoreError> {
1091 execute(
1092 root,
1093 participants,
1094 Operation::Declare(rule),
1095 &mut |_| Ok(()),
1096 )
1097 .map(|_| ())
1098}
1099
1100pub fn namespace_delete_cascade(
1104 root: &Path,
1105 participants: Vec<NamespaceMutation>,
1106) -> Result<Vec<u64>, StoreError> {
1107 execute(root, participants, Operation::Cascade, &mut |_| Ok(()))
1108}
1109
1110#[cfg(any(test, feature = "test-seams"))]
1112#[doc(hidden)]
1113pub fn namespace_delete_cascade_with_steps(
1114 root: &Path,
1115 participants: Vec<NamespaceMutation>,
1116 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1117) -> Result<Vec<u64>, StoreError> {
1118 execute(root, participants, Operation::Cascade, step)
1119}
1120
1121enum Operation {
1122 Batch,
1123 Cascade,
1124 Declare(super::CascadeRule),
1125}
1126
1127fn execute(
1128 root: &Path,
1129 mut mutations: Vec<NamespaceMutation>,
1130 operation: Operation,
1131 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1132) -> Result<Vec<u64>, StoreError> {
1133 let root = std::fs::canonicalize(root).map_err(|e| io(root, e))?;
1134 let minimum = if matches!(operation, Operation::Batch) {
1135 2
1136 } else {
1137 1
1138 };
1139 if mutations.len() < minimum || mutations.len() > 128 {
1140 return Err(invalid(
1141 &root,
1142 "namespace batch requires 2..128 participants",
1143 ));
1144 }
1145 if !matches!(operation, Operation::Batch)
1146 && mutations.iter().any(|m| {
1147 !m.upserts.is_empty()
1148 || m.delete_where.is_some()
1149 || (matches!(operation, Operation::Declare(_)) && !m.deletes.is_empty())
1150 })
1151 {
1152 return Err(invalid(
1153 &root,
1154 "cascade operations reject upserts/filters; declarations reject deletes",
1155 ));
1156 }
1157 let _coordinator = StoreLock::acquire(&root).map_err(StoreError::Lock)?;
1158 reclamation::run(&StdVfs, &root, step)?;
1159 if root_record_vfs(&StdVfs, &root)?.0.is_some() {
1160 normalize_accepted(&StdVfs, &root, step)?;
1161 }
1162 let mut next = routes(&root)?;
1163 let mut ordered = BTreeMap::new();
1164 for (index, mutation) in mutations.iter().enumerate() {
1165 if !name_valid(&mutation.name)
1166 || ordered.insert(mutation.name.clone(), index).is_some()
1167 || mutation.options.access_mode != AccessMode::ReadWrite
1168 {
1169 return Err(invalid(
1170 &root,
1171 "invalid, duplicate, or read-only namespace participant",
1172 ));
1173 }
1174 }
1175 let mut sources = BTreeMap::new();
1178 for (name, index) in &ordered {
1179 let mutation = mutations
1180 .get(*index)
1181 .ok_or_else(|| invalid(&root, "participant index"))?;
1182 let path = root.join(name);
1183 if std::fs::symlink_metadata(&path)
1184 .map_err(|e| io(&path, e))?
1185 .file_type()
1186 .is_symlink()
1187 {
1188 return Err(invalid(
1189 &path,
1190 "namespace links are not transaction participants",
1191 ));
1192 }
1193 let mut options = mutation.options.clone();
1194 options.access_mode = AccessMode::ReadOnly;
1195 drop(Store::open(&path, options)?);
1197 sources.insert(name.clone(), Store::open(&path, mutation.options.clone())?);
1198 }
1199 match operation {
1200 Operation::Declare(rule) => {
1201 super::cascade::declare(&root, &sources, rule, step)?;
1202 return Ok(Vec::new());
1203 }
1204 Operation::Cascade => super::cascade::expand(&root, &sources, &mut mutations)?,
1205 Operation::Batch => {
1206 let live = mutations
1207 .into_iter()
1208 .map(|mutation| {
1209 sources
1210 .get(&mutation.name)
1211 .map(|store| LiveNamespaceMutation { store, mutation })
1212 .ok_or_else(|| invalid(&root, "missing source"))
1213 })
1214 .collect::<Result<Vec<_>, _>>()?;
1215 return execute_live(&root, live, &StdVfs, step, Some(_coordinator));
1216 }
1217 }
1218 let mut root_id = root_record_vfs(&StdVfs, &root)?.0;
1219 if read_optional(&root.join(RECORD))?.is_none() {
1220 root_id = Some(NamespaceRootId::generate().map_err(|e| io(&root, e))?);
1221 }
1222 let transaction = loop {
1223 let id = NEXT.fetch_add(1, Ordering::Relaxed);
1224 let path = root.join(format!(".ze-batch-{}-{id}", std::process::id()));
1225 match StdVfs.create_directory(&path) {
1226 Ok(()) => break path,
1227 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
1228 Err(e) => return Err(io(&path, e)),
1229 }
1230 };
1231 step("transaction directory").map_err(|e| io(&transaction, e))?;
1232 let transaction_name = transaction
1233 .file_name()
1234 .and_then(|s| s.to_str())
1235 .ok_or_else(|| invalid(&transaction, "transaction name"))?;
1236 for name in ordered.keys() {
1237 next.insert(name.clone(), format!("{transaction_name}/{name}"));
1238 }
1239 let intent = encode(&next);
1240 let decision = encode_root_record(root_id, &RootDescriptor::Legacy(next.clone()))?;
1241 if decision.len() > MAX_RECORD {
1242 return Err(invalid(&root, "namespace decision too large"));
1243 }
1244 durable_write(&StdVfs, &transaction.join("intent.ze"), &intent, step)?;
1245 sync_dir(&StdVfs, &transaction, step)?;
1246 sync_dir(&StdVfs, &root, step)?;
1247 if root_id.is_some() {
1248 if read_optional(&root.join(RECORD))?.is_none() {
1249 publish(
1250 &StdVfs,
1251 &root,
1252 &encode_root_record(root_id, &RootDescriptor::Legacy(Routes::new()))?,
1253 step,
1254 )?;
1255 step("bootstrap root published").map_err(|e| io(&root, e))?;
1256 }
1257 for name in ordered.keys() {
1258 let logical = root.join(name);
1259 if read_optional(&logical.join(REFERENCE))?.is_none() {
1260 publish_reference(
1261 &StdVfs,
1262 &logical,
1263 &encode_participant_reference(root_id, &root, name, 1)?,
1264 step,
1265 )?;
1266 step("participant reference installed").map_err(|e| io(&logical, e))?;
1267 }
1268 }
1269 }
1270 let mut generations = Vec::new();
1271 for mutation in mutations {
1272 let source = sources
1273 .get(&mutation.name)
1274 .ok_or_else(|| invalid(&root, "missing participant"))?;
1275 let destination = transaction.join(&mutation.name);
1276 prepare_copy(source, &destination, step)?;
1277 step("snapshot prepared").map_err(|e| io(&destination, e))?;
1278 let mut options = mutation.options;
1279 options.schema = Some(source.schema().clone());
1282 options.durability_mode = DurabilityMode::Durable;
1283 options.commit_tier = CommitTier::Durable;
1284 let store = match root_id {
1285 Some(id) => {
1286 publish_reference(
1287 &StdVfs,
1288 &destination,
1289 &encode_participant_reference(root_id, &root, &mutation.name, 2)?,
1290 step,
1291 )?;
1292 let authority =
1293 PrivatePreparation::new(id, &mutation.name, &destination, &_coordinator);
1294 Store::open_private_preparation(&destination, options, authority)?
1295 }
1296 None => Store::open(&destination, options)?,
1297 };
1298 if !mutation.upserts.is_empty() {
1299 let mut batch = IngestBatch::new(mutation.upserts);
1300 if let Some(epoch) = store.epoch_identity() {
1301 batch = batch.with_epoch(epoch);
1302 }
1303 store
1304 .ingest(batch)
1305 .map_err(|e| invalid(&destination, &e.to_string()))?;
1306 step("upserts prepared").map_err(|e| io(&destination, e))?;
1307 }
1308 if !mutation.deletes.is_empty() {
1309 store
1310 .delete(DeleteBatch::new(mutation.deletes))
1311 .map_err(|e| invalid(&destination, &e.to_string()))?;
1312 step("deletes prepared").map_err(|e| io(&destination, e))?;
1313 }
1314 if let Some(predicate) = mutation.delete_where {
1315 store
1316 .delete_matching(&predicate)
1317 .map_err(|e| invalid(&destination, &e.to_string()))?;
1318 step("predicate prepared").map_err(|e| io(&destination, e))?;
1319 }
1320 let generation = store.seal()?;
1321 step("prepared checkpoint").map_err(|e| io(&destination, e))?;
1322 store.close()?;
1323 generations.push(generation);
1324 let relative = next
1325 .get(&mutation.name)
1326 .ok_or_else(|| invalid(&root, "missing route"))?;
1327 durable_write(
1328 &StdVfs,
1329 &destination.join(PREPARED),
1330 &envelope(relative.as_bytes()),
1331 step,
1332 )?;
1333 sync_dir(&StdVfs, &destination, step)?;
1334 }
1335 sync_dir(&StdVfs, &transaction, step)?;
1336 if root_id.is_none() {
1337 if read_optional(&root.join(RECORD))?.is_none() {
1340 publish(
1341 &StdVfs,
1342 &root,
1343 &encode_root_record(root_id, &RootDescriptor::Legacy(Routes::new()))?,
1344 step,
1345 )?;
1346 }
1347 for name in ordered.keys() {
1348 let directory = root.join(name);
1349 let reference = directory.join(REFERENCE);
1350 if read_optional(&reference)?.is_none() {
1351 let root_text = root
1352 .to_str()
1353 .ok_or_else(|| invalid(&root, "root must be UTF-8"))?;
1354 publish_reference(&StdVfs, &directory, &envelope(root_text.as_bytes()), step)?;
1355 }
1356 }
1357 }
1358 publish(&StdVfs, &root, &decision, step)?;
1359 drop(sources);
1360 reclamation::run(&StdVfs, &root, step)?;
1361 Ok(generations)
1362}
1363
1364#[cfg(test)]
1365#[allow(clippy::expect_used)]
1366mod tests {
1367 use super::*;
1368 use crate::vfs::crash::{CrashVfs, MemoryVfs};
1369
1370 #[test]
1371 fn root_decision_power_cuts_never_publish_a_torn_or_partial_selection() {
1372 let root = Path::new("/root");
1373 let before = encode(&Routes::new());
1374 let after = encode(&Routes::from([
1375 ("a".into(), ".ze-batch-1/a".into()),
1376 ("b".into(), ".ze-batch-1/b".into()),
1377 ]));
1378 let memory = MemoryVfs::new();
1379 memory
1380 .insert(root.join(RECORD), before.clone())
1381 .expect("seed decision");
1382 let crash = CrashVfs::new(memory).expect("recorder");
1383 publish(&crash, root, &after, &mut |_| Ok(())).expect("publish");
1384 let states = crash.crash_states().expect("power cuts");
1385 assert!(!states.was_capped());
1386 for state in states.iter() {
1387 let bytes = state
1388 .vfs()
1389 .read(&root.join(RECORD))
1390 .expect("previous or new root");
1391 assert!(bytes == before || bytes == after, "{:?}", state.kind());
1392 }
1393 eprintln!("ZE-239 root power-cut states: {}", states.len());
1394 }
1395}
1396
1397#[cfg(test)]
1398#[allow(clippy::expect_used)]
1399mod descriptor_tests {
1400 use super::*;
1401 #[test]
1402 fn nonparticipant_route_survives_v2_publish() {
1403 let temporary = tempfile::tempdir().expect("root");
1404 let root = temporary.path();
1405 let destination = ".ze-batch-1/c";
1406 let selected = root.join(destination);
1407 std::fs::create_dir_all(&selected).expect("selected directory");
1408 std::fs::create_dir(root.join("c")).expect("logical directory");
1409 let routes = Routes::from([("c".into(), destination.into())]);
1410 std::fs::write(root.join(".ze-batch-1/intent.ze"), encode(&routes)).expect("intent");
1411 std::fs::write(selected.join(PREPARED), envelope(destination.as_bytes()))
1412 .expect("prepared marker");
1413 for filename in ["manifest.ze", "wal.ze"] {
1414 std::fs::write(selected.join(filename), b"retained").expect("payload");
1415 }
1416 publish(&StdVfs, root, &encode(&routes), &mut |_| Ok(())).expect("legacy publish");
1417 assert_eq!(resolve(&root.join("c")).expect("legacy resolve"), selected);
1418 let descriptor = StagedDescriptor(
1419 BTreeMap::from([(
1420 "a".into(),
1421 StagedSelection {
1422 manifest: ".ze-manifest-1".into(),
1423 binding: crate::ingest::wal_payload::TransactionBinding {
1424 transaction: 1,
1425 participant: 2,
1426 first_seq: 3,
1427 last_seq: 3,
1428 manifest_digest: 4,
1429 final_generation: 5,
1430 },
1431 },
1432 )]),
1433 routes,
1434 );
1435 publish(
1436 &StdVfs,
1437 root,
1438 &encode_staged(&descriptor).expect("v2"),
1439 &mut |_| Ok(()),
1440 )
1441 .expect("v2 publish");
1442 assert_eq!(resolve(&root.join("c")).expect("v2 resolve"), selected);
1443 }
1444
1445 #[test]
1446 #[allow(clippy::indexing_slicing)]
1447 fn staged_descriptor_round_trips_and_old_reader_rejects() {
1448 let binding = crate::ingest::wal_payload::TransactionBinding {
1449 transaction: 1,
1450 participant: 2,
1451 first_seq: 3,
1452 last_seq: 3,
1453 manifest_digest: 4,
1454 final_generation: 5,
1455 };
1456 let descriptor = StagedDescriptor(
1457 BTreeMap::from([(
1458 "a".into(),
1459 StagedSelection {
1460 manifest: ".ze-manifest-1".into(),
1461 binding,
1462 },
1463 )]),
1464 Routes::new(),
1465 );
1466 let bytes = encode_staged(&descriptor).expect("encode");
1467 assert_eq!(
1468 bytes.iter().map(|b| format!("{b:02x}")).collect::<String>(),
1469 "5a454e5330303032010000000100610e002e7a652d6d616e69666573742d3101000000000000000000000000000000020000000000000000000000000000000300000000000000030000000000000004000000000000000500000000000000100000005a454e5330303031a3d24bff963b33d2986d4df50a4f2e73"
1470 );
1471 for end in 0..bytes.len() {
1472 assert!(decode_staged(Path::new("root"), &bytes[..end]).is_err());
1473 }
1474 let mut bad = bytes.clone();
1475 bad[7] = b'3';
1476 assert!(decode_staged(Path::new("root"), &bad).is_err());
1477 let mut bad = bytes.clone();
1478 bad[40] ^= 1;
1479 assert!(decode_staged(Path::new("root"), &bad).is_err());
1480 assert!(body(Path::new("root"), &bytes).is_err());
1481 assert_eq!(
1482 decode_staged(Path::new("root"), &bytes).expect("decode"),
1483 descriptor
1484 );
1485 }
1486}
1487
1488const STAGED_MAGIC: &[u8] = b"ZENS0002";
1491#[derive(Clone, Debug, Eq, PartialEq)]
1492struct StagedSelection {
1493 manifest: String,
1494 binding: crate::ingest::wal_payload::TransactionBinding,
1495}
1496#[derive(Clone, Debug, Eq, PartialEq)]
1497struct StagedDescriptor(BTreeMap<String, StagedSelection>, Routes);
1498
1499fn encode_staged(descriptor: &StagedDescriptor) -> Result<Vec<u8>, StoreError> {
1500 let path = Path::new(RECORD);
1501 let mut bytes = STAGED_MAGIC.to_vec();
1502 let count =
1503 u32::try_from(descriptor.0.len()).map_err(|_| invalid(path, "participant count"))?;
1504 if count == 0 {
1505 return Err(invalid(path, "empty staged decision"));
1506 }
1507 bytes.extend_from_slice(&count.to_le_bytes());
1508 let mut transaction = None;
1509 let mut identities = std::collections::BTreeSet::new();
1510 for (name, selection) in &descriptor.0 {
1511 if !name_valid(name)
1512 || !selection.manifest.starts_with(".ze-manifest-")
1513 || !name_valid(selection.manifest.trim_start_matches('.'))
1514 || selection.manifest.contains("..")
1515 || transaction.is_some_and(|id| id != selection.binding.transaction)
1516 || !identities.insert(selection.binding.participant)
1517 {
1518 return Err(invalid(path, "staged participant identity or manifest"));
1519 }
1520 transaction = Some(selection.binding.transaction);
1521 for text in [name, &selection.manifest] {
1522 let length =
1523 u16::try_from(text.len()).map_err(|_| invalid(path, "selection length"))?;
1524 bytes.extend_from_slice(&length.to_le_bytes());
1525 bytes.extend_from_slice(text.as_bytes());
1526 }
1527 bytes.extend_from_slice(
1528 &selection
1529 .binding
1530 .encode()
1531 .map_err(|_| invalid(path, "transaction binding"))?,
1532 );
1533 }
1534 let routes = encode(&descriptor.1);
1535 let length = u32::try_from(routes.len()).map_err(|_| invalid(path, "route table length"))?;
1536 bytes.extend_from_slice(&length.to_le_bytes());
1537 bytes.extend_from_slice(&routes);
1538 bytes.extend_from_slice(&xxh3_64(&bytes).to_le_bytes());
1539 if bytes.len() > MAX_RECORD {
1540 return Err(invalid(path, "staged decision too large"));
1541 }
1542 Ok(bytes)
1543}
1544fn decode_staged(path: &Path, bytes: &[u8]) -> Result<StagedDescriptor, StoreError> {
1545 let end = bytes
1546 .len()
1547 .checked_sub(8)
1548 .ok_or_else(|| invalid(path, "short staged decision"))?;
1549 let prefix = bytes
1550 .get(..end)
1551 .ok_or_else(|| invalid(path, "staged bounds"))?;
1552 if bytes.len() > MAX_RECORD
1553 || !prefix.starts_with(STAGED_MAGIC)
1554 || bytes.get(end..) != Some(xxh3_64(prefix).to_le_bytes().as_slice())
1555 {
1556 return Err(invalid(path, "staged checksum/version"));
1557 }
1558 let mut remaining = prefix
1559 .get(8..)
1560 .ok_or_else(|| invalid(path, "staged header"))?;
1561 fn take<'a>(path: &Path, bytes: &mut &'a [u8], n: usize) -> Result<&'a [u8], StoreError> {
1562 let result = bytes
1563 .get(..n)
1564 .ok_or_else(|| invalid(path, "truncated staged decision"))?;
1565 *bytes = bytes
1566 .get(n..)
1567 .ok_or_else(|| invalid(path, "staged bounds"))?;
1568 Ok(result)
1569 }
1570 let count = u32::from_le_bytes(
1571 take(path, &mut remaining, 4)?
1572 .try_into()
1573 .map_err(|_| invalid(path, "count"))?,
1574 );
1575 let mut result = BTreeMap::new();
1576 for _ in 0..count {
1577 let mut texts = Vec::new();
1578 for _ in 0..2 {
1579 let n = u16::from_le_bytes(
1580 take(path, &mut remaining, 2)?
1581 .try_into()
1582 .map_err(|_| invalid(path, "length"))?,
1583 );
1584 texts.push(
1585 std::str::from_utf8(take(path, &mut remaining, usize::from(n))?)
1586 .map_err(|_| invalid(path, "selection UTF-8"))?
1587 .to_owned(),
1588 );
1589 }
1590 let mut texts = texts.into_iter();
1591 let name = texts.next().ok_or_else(|| invalid(path, "name"))?;
1592 let manifest = texts.next().ok_or_else(|| invalid(path, "manifest"))?;
1593 let binding =
1594 crate::ingest::wal_payload::TransactionBinding::decode(take(path, &mut remaining, 64)?)
1595 .map_err(|_| invalid(path, "transaction binding"))?;
1596 if result
1597 .insert(name, StagedSelection { manifest, binding })
1598 .is_some()
1599 {
1600 return Err(invalid(path, "duplicate participant"));
1601 }
1602 }
1603 let route_length = u32::from_le_bytes(
1604 take(path, &mut remaining, 4)?
1605 .try_into()
1606 .map_err(|_| invalid(path, "route table length"))?,
1607 );
1608 let route_length =
1609 usize::try_from(route_length).map_err(|_| invalid(path, "route table length"))?;
1610 let routes = decode_routes(path, take(path, &mut remaining, route_length)?)?;
1611 let descriptor = StagedDescriptor(result, routes);
1612 if !remaining.is_empty() || encode_staged(&descriptor)? != bytes {
1613 return Err(invalid(path, "noncanonical staged decision"));
1614 }
1615 Ok(descriptor)
1616}
1617
1618fn read_optional_vfs(vfs: &dyn Vfs, path: &Path) -> Result<Option<Vec<u8>>, StoreError> {
1619 match vfs.open(path) {
1620 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
1621 Err(e) => Err(io(path, e)),
1622 Ok(n) if n > MAX_RECORD as u64 => Err(invalid(path, "namespace record too large")),
1623 Ok(_) => vfs.read(path).map(Some).map_err(|e| io(path, e)),
1624 }
1625}
1626
1627fn location(directory: &Path) -> Result<(&Path, &str), StoreError> {
1628 let name = directory
1629 .file_name()
1630 .and_then(|name| name.to_str())
1631 .ok_or_else(|| invalid(directory, "participant name"))?;
1632 let parent = directory
1633 .parent()
1634 .ok_or_else(|| invalid(directory, "participant root"))?;
1635 let root = if parent
1636 .file_name()
1637 .and_then(|name| name.to_str())
1638 .is_some_and(|name| name.starts_with(".ze-batch-"))
1639 {
1640 parent
1641 .parent()
1642 .ok_or_else(|| invalid(directory, "route root"))?
1643 } else {
1644 parent
1645 };
1646 Ok((root, name))
1647}
1648fn reader_location<'a>(
1649 vfs: &dyn Vfs,
1650 directory: &'a Path,
1651 preparation: Option<&PrivatePreparation>,
1652) -> Result<(&'a Path, &'a str, Option<NamespaceRootId>), StoreError> {
1653 if let Some((root, name, id)) =
1654 portable::authority_with_preparation(vfs, directory, preparation)?
1655 {
1656 return Ok((root, name, Some(id)));
1657 }
1658 location(directory).map(|(root, name)| (root, name, None))
1659}
1660fn reader_participant_identity(
1661 vfs: &dyn Vfs,
1662 directory: &Path,
1663 preparation: Option<&PrivatePreparation>,
1664) -> Result<u128, StoreError> {
1665 let (root, name, id) = reader_location(vfs, directory, preparation)?;
1666 match id {
1667 Some(id) => portable::participant_id(id, name),
1668 None => participant_identity(root, name),
1669 }
1670}
1671fn acceptance_path(
1672 directory: &Path,
1673 binding: crate::ingest::wal_payload::TransactionBinding,
1674) -> PathBuf {
1675 directory.join(format!(".ze-accepted-{}", binding.transaction))
1676}
1677fn accepted(
1678 vfs: &dyn Vfs,
1679 directory: &Path,
1680 binding: crate::ingest::wal_payload::TransactionBinding,
1681) -> Result<bool, StoreError> {
1682 let path = acceptance_path(directory, binding);
1683 let Some(bytes) = read_optional_vfs(vfs, &path)? else {
1684 return Ok(false);
1685 };
1686 let recorded = crate::ingest::wal_payload::TransactionBinding::decode(body(&path, &bytes)?)
1687 .map_err(|e| invalid(&path, &e.to_string()))?;
1688 if recorded != binding {
1689 return Err(invalid(&path, "local acceptance binding mismatch"));
1690 }
1691 Ok(true)
1692}
1693pub(crate) fn owns_purge_obligation(
1696 vfs: &dyn Vfs,
1697 directory: &Path,
1698 token: u64,
1699 preparation: Option<&PrivatePreparation>,
1700) -> Result<bool, StoreError> {
1701 for path in vfs.list(directory).map_err(|e| io(directory, e))? {
1702 let Some(transaction) = path
1703 .file_name()
1704 .and_then(|name| name.to_str())
1705 .and_then(|name| name.strip_prefix(".ze-accepted-"))
1706 .and_then(|name| name.parse::<u128>().ok())
1707 else {
1708 continue;
1709 };
1710 if xxh3_64(&transaction.to_le_bytes()) != token {
1711 continue;
1712 }
1713 let bytes = vfs.read(&path).map_err(|e| io(&path, e))?;
1714 let binding = crate::ingest::wal_payload::TransactionBinding::decode(body(&path, &bytes)?)
1715 .map_err(|e| invalid(&path, &e.to_string()))?;
1716 if binding.transaction != transaction {
1717 return Err(invalid(&path, "purge acceptance transaction mismatch"));
1718 }
1719 return Ok(true);
1720 }
1721 if preparation.is_none()
1722 && let Some(selected) = selection(vfs, directory)?
1723 && xxh3_64(&selected.binding.transaction.to_le_bytes()) == token
1724 {
1725 staged_manifest(vfs, directory, &selected)?;
1726 return Ok(true);
1727 }
1728 Ok(false)
1729}
1730
1731fn selection(vfs: &dyn Vfs, directory: &Path) -> Result<Option<StagedSelection>, StoreError> {
1732 let (root, name, _) = reader_location(vfs, directory, None)?;
1733 let RootDescriptor::Staged(mut descriptor) = root_descriptor_vfs(vfs, root)? else {
1734 return Ok(None);
1735 };
1736 let Some(selected) = descriptor.0.remove(name) else {
1737 return Ok(None);
1738 };
1739 let identity = reader_participant_identity(vfs, directory, None)?;
1740 if selected.binding.participant != identity {
1741 return Err(invalid(directory, "participant identity mismatch"));
1742 }
1743 Ok(Some(selected))
1744}
1745fn selected_manifest(
1746 vfs: &dyn Vfs,
1747 directory: &Path,
1748 selected: &StagedSelection,
1749) -> Result<crate::manifest::Manifest, StoreError> {
1750 let path = directory.join(&selected.manifest);
1751 let bytes = vfs.read(&path).map_err(|e| io(&path, e))?;
1752 if xxh3_64(&bytes) != selected.binding.manifest_digest {
1753 return Err(invalid(&path, "staged manifest digest mismatch"));
1754 }
1755 let manifest = crate::manifest::decode_manifest(&path.display().to_string(), &bytes)
1756 .map_err(StoreError::Manifest)?;
1757 if manifest.generation != selected.binding.final_generation {
1758 return Err(invalid(&path, "staged generation mismatch"));
1759 }
1760 Ok(manifest)
1761}
1762
1763fn staged_manifest(
1764 vfs: &dyn Vfs,
1765 directory: &Path,
1766 selected: &StagedSelection,
1767) -> Result<crate::manifest::Manifest, StoreError> {
1768 let manifest = selected_manifest(vfs, directory, selected)?;
1769 let path = directory.join(&selected.manifest);
1770 let wal = crate::wal::WalReader::open(vfs, &directory.join("wal.ze"))
1771 .map_err(StoreError::Wal)?
1772 .into_clean()
1773 .map_err(StoreError::WalRecovery)?;
1774 let selected_records = wal
1776 .records()
1777 .iter()
1778 .filter(|r| (selected.binding.first_seq..=selected.binding.last_seq).contains(&r.seq.get()))
1779 .collect::<Vec<_>>();
1780 let member_count = selected
1781 .binding
1782 .last_seq
1783 .checked_sub(selected.binding.first_seq)
1784 .and_then(|count| count.checked_add(1))
1785 .ok_or_else(|| invalid(&path, "invalid committed prepared range"))?;
1786 if selected_records.len() as u64 != member_count {
1787 return Err(invalid(&path, "committed prepared range incomplete"));
1788 }
1789 for (record, index) in selected_records.iter().zip(0_u32..) {
1790 let payload = record
1791 .payload()
1792 .map_err(|e| invalid(&path, &e.to_string()))?;
1793 let member = crate::ingest::wal_payload::decode_prepared(payload)
1794 .map_err(|e| invalid(&path, &e.to_string()))?;
1795 if record.op != crate::ingest::wal_payload::PREPARED_MUTATION_V1
1796 || member.binding != selected.binding
1797 || member.index != index
1798 {
1799 return Err(invalid(&path, "committed prepared binding mismatch"));
1800 }
1801 }
1802 Ok(manifest)
1803}
1804
1805pub(super) fn manifest_for_open(
1806 vfs: &dyn Vfs,
1807 directory: &Path,
1808 preparation: Option<&PrivatePreparation>,
1809) -> Result<PathBuf, StoreError> {
1810 let canonical = directory.join("manifest.ze");
1811 if vfs
1812 .open(&canonical)
1813 .is_err_and(|e| e.kind() == std::io::ErrorKind::NotFound)
1814 && vfs
1815 .list(directory)
1816 .map_err(|e| io(directory, e))?
1817 .iter()
1818 .any(|path| {
1819 path.file_name()
1820 .and_then(|name| name.to_str())
1821 .is_some_and(|name| name.starts_with(".ze-accepted-"))
1822 })
1823 {
1824 return Err(invalid(&canonical, "missing locally accepted manifest"));
1825 }
1826 if preparation.is_some() {
1827 portable::authority_with_preparation(vfs, directory, preparation)?;
1828 return Ok(canonical);
1829 }
1830 match selection(vfs, directory)? {
1831 Some(selected) if !accepted(vfs, directory, selected.binding)? => {
1832 staged_manifest(vfs, directory, &selected)?;
1833 Ok(directory.join(selected.manifest))
1834 }
1835 _ => Ok(directory.join("manifest.ze")),
1836 }
1837}
1838fn accept(
1839 vfs: &dyn Vfs,
1840 directory: &Path,
1841 selected: &StagedSelection,
1842 manifest: &crate::manifest::Manifest,
1843 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1844) -> Result<(), StoreError> {
1845 let wal = crate::wal::WalReader::open(vfs, &directory.join("wal.ze"))
1848 .map_err(StoreError::Wal)?
1849 .into_clean()
1850 .map_err(StoreError::WalRecovery)?;
1851 let mut ids = Vec::new();
1852 for record in wal.records().iter().filter(|record| {
1853 (selected.binding.first_seq..=selected.binding.last_seq).contains(&record.seq.get())
1854 }) {
1855 let payload = record
1856 .payload()
1857 .map_err(|e| invalid(directory, &e.to_string()))?;
1858 let member = crate::ingest::wal_payload::decode_prepared(payload)
1859 .map_err(|e| invalid(directory, &e.to_string()))?;
1860 if member.binding != selected.binding {
1861 return Err(invalid(directory, "purge binding mismatch"));
1862 }
1863 if let crate::ingest::wal_payload::MutationPayload::Delete(deleted) = member.mutation {
1864 ids.extend(deleted);
1865 }
1866 }
1867 ids.sort_unstable();
1868 ids.dedup();
1869 crate::ingest::namespace_purge_intent(vfs, directory, selected.binding.transaction, ids)
1870 .map_err(|e| StoreError::PurgeRecovery {
1871 detail: e.to_string(),
1872 })?;
1873 step("purge obligation adopted").map_err(|e| io(directory, e))?;
1874 let bytes = crate::manifest::encode_manifest(manifest).map_err(StoreError::Manifest)?;
1875 let temporary = directory.join(".manifest.ze.tmp");
1876 durable_write(vfs, &temporary, &bytes, step)?;
1877 vfs.rename(&temporary, &directory.join("manifest.ze"))
1878 .map_err(|e| io(directory, e))?;
1879 step("accept manifest rename").map_err(|e| io(directory, e))?;
1880 sync_dir(vfs, directory, step)?;
1881 let binding = selected
1882 .binding
1883 .encode()
1884 .map_err(|e| invalid(directory, &e.to_string()))?;
1885 let committed = acceptance_path(directory, selected.binding);
1886 let temporary = committed.with_extension("tmp");
1887 durable_write(vfs, &temporary, &envelope(&binding), step)?;
1888 vfs.rename(&temporary, &committed)
1889 .map_err(|e| io(&committed, e))?;
1890 step("accept binding rename").map_err(|e| io(&committed, e))?;
1891 sync_dir(vfs, directory, step)
1892}
1893
1894pub(super) fn adopt_for_open(vfs: &dyn Vfs, directory: &Path) -> Result<(), StoreError> {
1895 if let Some(selected) = selection(vfs, directory)? {
1896 let (root, _, _) = reader_location(vfs, directory, None)?;
1897 vfs.sync(root, SyncKind::Full).map_err(|e| io(root, e))?;
1900 if accepted(vfs, directory, selected.binding)? {
1901 vfs.sync(directory, SyncKind::Full)
1902 .map_err(|e| io(directory, e))?;
1903 } else {
1904 let manifest = staged_manifest(vfs, directory, &selected)?;
1905 accept(vfs, directory, &selected, &manifest, &mut |_| Ok(()))?;
1906 }
1907 }
1908 Ok(())
1909}
1910fn normalize_accepted(
1911 vfs: &dyn Vfs,
1912 root: &Path,
1913 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
1914) -> Result<(), StoreError> {
1915 let (root_id, descriptor) = root_record_vfs(vfs, root)?;
1916 let RootDescriptor::Staged(descriptor) = descriptor else {
1917 return Ok(());
1918 };
1919 for (name, selected) in &descriptor.0 {
1920 let directory = root.join(descriptor.1.get(name).map_or(name.as_str(), String::as_str));
1921 if !accepted(vfs, &directory, selected.binding)? {
1922 return Err(StoreError::StoreBusy { path: directory });
1923 }
1924 sync_dir(vfs, &directory, step)?;
1925 }
1926 publish(
1927 vfs,
1928 root,
1929 &encode_root_record(root_id, &RootDescriptor::Legacy(descriptor.1))?,
1930 step,
1931 )
1932}
1933
1934pub(crate) fn transaction_decisions(
1935 vfs: &dyn Vfs,
1936 directory: &Path,
1937 records: &[crate::wal::VisibleRecord],
1938 absorbed: u64,
1939 preparation: Option<&PrivatePreparation>,
1940) -> Result<BTreeMap<u128, crate::ingest::wal_payload::TransactionBinding>, StoreError> {
1941 let mut decisions = BTreeMap::new();
1942 let mut checked = std::collections::BTreeSet::new();
1943 for record in records.iter().filter(|r| {
1944 r.seq.get() > absorbed && r.op == crate::ingest::wal_payload::PREPARED_MUTATION_V1
1945 }) {
1946 let payload = record.payload().map_err(|source| StoreError::WalRecord {
1947 seq: record.seq,
1948 source,
1949 })?;
1950 let member = crate::ingest::wal_payload::decode_prepared(payload).map_err(|source| {
1951 StoreError::WalMutation {
1952 seq: record.seq,
1953 op: record.op,
1954 source,
1955 }
1956 })?;
1957 if !checked.insert(member.binding.transaction) {
1958 continue;
1959 }
1960 let identity = reader_participant_identity(vfs, directory, preparation)?;
1961 if member.binding.participant != identity {
1962 return Err(StoreError::WalMutation {
1963 seq: record.seq,
1964 op: record.op,
1965 source: crate::ingest::wal_payload::PayloadError::TransactionBinding,
1966 });
1967 }
1968 if accepted(vfs, directory, member.binding)? {
1969 decisions.insert(member.binding.transaction, member.binding);
1970 } else if preparation.is_none()
1971 && let Some(selected) = selection(vfs, directory)?
1972 && selected.binding.transaction == member.binding.transaction
1973 {
1974 if selected.binding != member.binding {
1975 return Err(invalid(directory, "root prepared binding mismatch"));
1976 }
1977 staged_manifest(vfs, directory, &selected)?;
1978 decisions.insert(member.binding.transaction, selected.binding);
1979 }
1980 }
1981 Ok(decisions)
1982}
1983
1984fn participant_identity(root: &Path, name: &str) -> Result<u128, StoreError> {
1985 let canonical = std::fs::canonicalize(root).map_err(|e| io(root, e))?;
1986 Ok(xxhash_rust::xxh3::xxh3_128(
1987 format!("{}\t{name}", canonical.display()).as_bytes(),
1988 ))
1989}
1990
1991fn rollback_stages(
1992 ordered: &[&LiveNamespaceMutation<'_>],
1993 staged: Vec<crate::ingest::NamespaceStage>,
1994 selections: &[Option<StagedSelection>],
1995 lengths: &[u64],
1996 wals: &mut [std::sync::MutexGuard<'_, Option<crate::ingest::StoreWal>>],
1997) -> Result<(), StoreError> {
1998 let result = (|| {
1999 for ((((p, stage), selection), length), wal) in ordered
2000 .iter()
2001 .zip(staged)
2002 .zip(selections)
2003 .zip(lengths)
2004 .zip(wals.iter_mut())
2005 {
2006 wal.as_mut()
2007 .ok_or(StoreError::ReadOnly)?
2008 .abort_namespace_suffix(
2009 p.store.vfs.as_ref(),
2010 &p.store.directory.join("wal.ze"),
2011 *length,
2012 p.store.durability_policy,
2013 stage.manifest.log_seq,
2014 &p.store.accounting,
2015 )?;
2016 cleanup_stage(p.store, stage, selection.as_ref())?;
2017 }
2018 Ok(())
2019 })();
2020 if result.is_err() {
2021 for wal in wals {
2022 **wal = None;
2023 }
2024 }
2025 result
2026}
2027
2028#[cfg(any(test, feature = "test-seams"))]
2030#[doc(hidden)]
2031pub fn namespace_reclaim(root: &Path) -> Result<(), StoreError> {
2032 let root = std::fs::canonicalize(root).map_err(|e| io(root, e))?;
2033 let _owner = StoreLock::acquire(&root).map_err(StoreError::Lock)?;
2034 reclamation::run(&StdVfs, &root, &mut |_| Ok(()))
2035}
2036
2037#[cfg(test)]
2038mod portable_tests;
2039mod reclamation;
2040pub(super) use reclamation::{
2041 admission as reader_admission, for_open as reclaim_for_open, lease as reader_lease,
2042 refuse as refuse_retired,
2043};
2044
2045#[cfg(any(test, feature = "test-seams"))]
2046#[doc(hidden)]
2047pub fn namespace_reclaim_on_vfs(
2048 root: &Path,
2049 vfs: &dyn Vfs,
2050 step: &mut dyn FnMut(&str) -> std::io::Result<()>,
2051) -> Result<(), StoreError> {
2052 let _owner = StoreLock::acquire(root).map_err(StoreError::Lock)?;
2053 reclamation::run(vfs, root, step)
2054}