1use crate::{
8 ownership::*, Admission, Crdt, EventLog, GCounter, GCounterDelta, OrSet, OrSetDelta, Record,
9 RecordId, WireDecode, WireEncode, WireError, WireSchema,
10};
11use alloc::{format, string::String, vec::Vec};
12use std::{
13 fs::{self, File, OpenOptions, TryLockError},
14 io::{self, Read, Seek, SeekFrom, Write},
15 path::{Path, PathBuf},
16};
17
18#[derive(Debug)]
19pub enum LocalError {
20 Refused,
21 Exhausted,
22 RecoveryRequired,
23 Configuration,
24 History(WireError),
25 InvalidHistory,
26 Io(io::Error),
27}
28impl From<io::Error> for LocalError {
29 fn from(e: io::Error) -> Self {
30 Self::Io(e)
31 }
32}
33
34#[derive(Clone, Copy, Debug)]
36pub struct WriteTicket(u64);
37
38pub struct LocalReplica<C: Crdt> {
41 config: WriterConfig,
42 fence: File,
43 held: bool,
44 generation: u64,
45 state: C,
46 log: EventLog<C::Delta>,
47 last_sequence: u64,
48}
49
50impl<C: Crdt> Drop for LocalReplica<C> {
51 fn drop(&mut self) {
52 let _ = self.fence.unlock();
55 }
56}
57
58impl<C: Crdt> LocalReplica<C>
59where
60 C::Delta: OwnedDelta + Clone + PartialEq,
61{
62 fn fresh(root: &Path, config: WriterConfig, state: C) -> Result<Self, LocalError> {
63 config.validate().map_err(|_| LocalError::Configuration)?;
64 fs::create_dir_all(root)?;
65 let mut fence = OpenOptions::new()
66 .read(true)
67 .write(true)
68 .create(true)
69 .truncate(false)
70 .open(root.join(format!("writer-{}.fence", config.writer)))?;
71 let held = match fence.try_lock() {
72 Ok(()) => true,
73 Err(TryLockError::WouldBlock) => false,
74 Err(TryLockError::Error(e)) => return Err(e.into()),
75 };
76 let mut bytes = Vec::new();
77 fence.read_to_end(&mut bytes)?;
78 let generation = if bytes.is_empty() && held {
79 fence.write_all(&config.writers.to_le_bytes())?;
82 fence.write_all(&config.writer.to_le_bytes())?;
83 fence.write_all(&1u64.to_le_bytes())?;
84 fence.sync_all()?;
85 1
86 } else {
87 if bytes.len() != 24 {
88 return Err(LocalError::RecoveryRequired);
89 }
90 let word = |i| u64::from_le_bytes(bytes[i..i + 8].try_into().unwrap());
91 if word(0) != config.writers || word(8) != config.writer {
92 return Err(LocalError::Configuration);
93 }
94 if held {
95 return Err(LocalError::RecoveryRequired);
96 }
97 word(16)
98 };
99 Ok(Self {
100 config,
101 fence,
102 held,
103 generation,
104 log: EventLog::for_crdt(&state),
105 state,
106 last_sequence: 0,
107 })
108 }
109
110 pub fn state(&self) -> &C {
111 &self.state
112 }
113 pub fn log(&self) -> &EventLog<C::Delta> {
114 &self.log
115 }
116 pub fn ticket(&self) -> WriteTicket {
117 WriteTicket(self.generation)
118 }
119 pub fn allocation_bytes(&self) -> Vec<u8> {
121 [
122 self.config.writers.to_le_bytes(),
123 self.config.writer.to_le_bytes(),
124 self.last_sequence.to_le_bytes(),
125 ]
126 .concat()
127 }
128 fn context(&self, ticket: WriteTicket, local: bool) -> WriteContext {
129 WriteContext {
130 config: self.config,
131 held: self.held,
132 generation: ticket.0,
133 current_generation: self.generation,
134 local,
135 }
136 }
137
138 pub fn renew(&mut self, ticket: WriteTicket) -> Result<WriteTicket, LocalError> {
141 if refuses(
142 self.context(ticket, true),
143 RecordId {
144 replica: self.config.writer,
145 sequence: 1,
146 },
147 OwnedPayload::Remove,
148 ) {
149 return Err(LocalError::Refused);
150 }
151 let next = next_sequence(self.generation).ok_or(LocalError::Exhausted)?;
152 self.held = false;
153 self.fence.seek(SeekFrom::Start(16))?;
154 self.fence.write_all(&next.to_le_bytes())?;
155 self.fence.sync_all()?;
156 self.generation = next;
157 self.held = true;
158 Ok(self.ticket())
159 }
160
161 pub fn receive(
165 &mut self,
166 ticket: WriteTicket,
167 record: Record<C::Delta>,
168 ) -> Result<Admission, LocalError> {
169 self.admit(ticket, record, false)
170 }
171 fn admit(
172 &mut self,
173 ticket: WriteTicket,
174 record: Record<C::Delta>,
175 local: bool,
176 ) -> Result<Admission, LocalError> {
177 self.admit_committed(ticket, record, local, |_, _| Ok(()))
178 }
179 fn admit_committed(
180 &mut self,
181 ticket: WriteTicket,
182 record: Record<C::Delta>,
183 local: bool,
184 commit: impl FnOnce(&EventLog<C::Delta>, u64) -> Result<(), LocalError>,
185 ) -> Result<Admission, LocalError> {
186 if refuses(
187 self.context(ticket, local),
188 record.id,
189 record.delta.owned_payload(),
190 ) {
191 return Err(LocalError::Refused);
192 }
193 let mut candidate = self.log.clone();
194 let outcome = candidate.insert_record(record.clone());
195 if outcome != Admission::Accepted {
196 return Ok(outcome);
197 }
198 let sequence = if record.id.replica == self.config.writer {
199 self.last_sequence.max(record.id.sequence)
200 } else {
201 self.last_sequence
202 };
203 self.held = false;
206 commit(&candidate, sequence)?;
207 self.state.apply_delta(record.delta);
208 self.log = candidate;
209 self.last_sequence = sequence;
210 self.held = true;
211 Ok(outcome)
212 }
213 pub fn append(
216 &mut self,
217 ticket: WriteTicket,
218 delta: C::Delta,
219 ) -> Result<Record<C::Delta>, LocalError> {
220 let sequence = next_sequence(self.last_sequence).ok_or(LocalError::Exhausted)?;
221 let record = Record {
222 id: RecordId {
223 replica: self.config.writer,
224 sequence,
225 },
226 delta,
227 };
228 match self.admit(ticket, record.clone(), true)? {
229 Admission::Accepted => Ok(record),
230 _ => Err(LocalError::Refused),
231 }
232 }
233}
234
235impl LocalReplica<GCounter> {
236 pub fn counter(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
237 config.validate().map_err(|_| LocalError::Configuration)?;
238 let n = usize::try_from(config.writers).map_err(|_| LocalError::Configuration)?;
239 Self::fresh(root, config, GCounter::new(n))
240 }
241 pub fn bump(
242 &mut self,
243 ticket: WriteTicket,
244 tally: u64,
245 ) -> Result<Record<GCounterDelta>, LocalError> {
246 self.append(
247 ticket,
248 GCounterDelta {
249 replica: self.config.writer as usize,
250 tally,
251 },
252 )
253 }
254}
255impl LocalReplica<OrSet<String, u64>> {
256 pub fn utf8_set(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
257 Self::fresh(root, config, OrSet::new())
258 }
259 pub fn add(
260 &mut self,
261 ticket: WriteTicket,
262 element: String,
263 ) -> Result<Record<OrSetDelta<String, u64>>, LocalError> {
264 let sequence = next_sequence(self.last_sequence).ok_or(LocalError::Exhausted)?;
265 let token = allocate_token(self.config.writers, self.config.writer, sequence)
266 .ok_or(LocalError::Exhausted)?;
267 self.append(ticket, OrSetDelta::Add { element, token })
268 }
269 pub fn remove(
270 &mut self,
271 ticket: WriteTicket,
272 element: &String,
273 ) -> Result<Record<OrSetDelta<String, u64>>, LocalError> {
274 self.append(
275 ticket,
276 OrSetDelta::Remove {
277 tokens: self.state.observed_tokens(element).into_iter().collect(),
278 },
279 )
280 }
281}
282
283#[path = "persistence.rs"]
284mod persistence;
285
286#[derive(Debug, PartialEq, Eq)]
291pub struct CommittedTransaction {
292 pub config: WriterConfig,
293 pub last_sequence: u64,
294 pub log_bytes: Vec<u8>,
295}
296impl CommittedTransaction {
297 pub fn read(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
298 config.validate().map_err(|_| LocalError::Configuration)?;
299 let bytes = fs::read(transaction_path(root, config))?;
300 if bytes.len() < 24 {
301 return Err(LocalError::RecoveryRequired);
302 }
303 let word = |i| u64::from_le_bytes(bytes[i..i + 8].try_into().unwrap());
304 if word(0) != config.writers || word(8) != config.writer {
305 return Err(LocalError::Configuration);
306 }
307 Ok(Self {
308 config,
309 last_sequence: word(16),
310 log_bytes: bytes[24..].to_vec(),
311 })
312 }
313}
314fn transaction_path(root: &Path, config: WriterConfig) -> PathBuf {
315 root.join(format!("writer-{}.transaction", config.writer))
316}
317
318pub struct DurableReplica<C: Crdt> {
325 inner: LocalReplica<C>,
326 path: PathBuf,
327}
328impl<C: Crdt> DurableReplica<C>
329where
330 C::Delta: OwnedDelta + Clone + PartialEq + WireEncode + WireSchema,
331{
332 fn fresh(root: &Path, config: WriterConfig, state: C) -> Result<Self, LocalError> {
333 let root = root.canonicalize()?;
334 match fs::metadata(transaction_path(&root, config)) {
336 Ok(_) => return Err(LocalError::RecoveryRequired),
337 Err(e) if e.kind() == io::ErrorKind::NotFound => {}
338 Err(e) => return Err(e.into()),
339 }
340 let inner = LocalReplica::fresh(&root, config, state)?;
341 if !inner.held {
342 return Err(LocalError::Refused);
343 }
344 let path = transaction_path(&root, config);
345 let mut replica = Self { inner, path };
346 replica.inner.held = false;
347 Self::commit(&replica.path, config, &replica.inner.log, 0)?;
348 replica.inner.held = true;
349 Ok(replica)
350 }
351 fn commit(
352 path: &Path,
353 config: WriterConfig,
354 log: &EventLog<C::Delta>,
355 sequence: u64,
356 ) -> Result<(), LocalError> {
357 let mut bytes = [
358 config.writers.to_le_bytes(),
359 config.writer.to_le_bytes(),
360 sequence.to_le_bytes(),
361 ]
362 .concat();
363 bytes.extend(
364 log.to_wire_bytes()
365 .map_err(|e| LocalError::Io(io::Error::other(format!("{e:?}"))))?,
366 );
367 persistence::replace(path, &bytes)?;
368 Ok(())
369 }
370 pub fn state(&self) -> &C {
371 self.inner.state()
372 }
373 pub fn log(&self) -> &EventLog<C::Delta> {
374 self.inner.log()
375 }
376 pub fn allocation_bytes(&self) -> Vec<u8> {
377 self.inner.allocation_bytes()
378 }
379 pub fn ticket(&self) -> WriteTicket {
380 self.inner.ticket()
381 }
382 pub fn renew(&mut self, ticket: WriteTicket) -> Result<WriteTicket, LocalError> {
383 self.inner.renew(ticket)
384 }
385 fn admit(
386 &mut self,
387 ticket: WriteTicket,
388 record: Record<C::Delta>,
389 local: bool,
390 ) -> Result<Admission, LocalError> {
391 let path = &self.path;
392 let config = self.inner.config;
393 let outcome = self
394 .inner
395 .admit_committed(ticket, record, local, |log, sequence| {
396 Self::commit(path, config, log, sequence)
397 })?;
398 #[cfg(test)]
399 persistence::checkpoint(7)?;
400 Ok(outcome)
401 }
402 pub fn receive(
403 &mut self,
404 ticket: WriteTicket,
405 record: Record<C::Delta>,
406 ) -> Result<Admission, LocalError> {
407 self.admit(ticket, record, false)
408 }
409 pub fn append(
410 &mut self,
411 ticket: WriteTicket,
412 delta: C::Delta,
413 ) -> Result<Record<C::Delta>, LocalError> {
414 let sequence = next_sequence(self.inner.last_sequence).ok_or(LocalError::Exhausted)?;
415 let record = Record {
416 id: RecordId {
417 replica: self.inner.config.writer,
418 sequence,
419 },
420 delta,
421 };
422 match self.admit(ticket, record.clone(), true)? {
423 Admission::Accepted => Ok(record),
424 _ => Err(LocalError::Refused),
425 }
426 }
427}
428impl<C: Crdt> DurableReplica<C>
429where
430 C::Delta: OwnedDelta + Clone + PartialEq + WireEncode + WireDecode + WireSchema,
431{
432 fn restart(root: &Path, config: WriterConfig, state: C) -> Result<Self, LocalError> {
433 config.validate().map_err(|_| LocalError::Configuration)?;
434 let root = root.canonicalize()?;
435 let mut fence = OpenOptions::new()
437 .read(true)
438 .write(true)
439 .open(root.join(format!("writer-{}.fence", config.writer)))?;
440 match fence.try_lock() {
441 Ok(()) => {}
442 Err(TryLockError::WouldBlock) => return Err(LocalError::Refused),
443 Err(TryLockError::Error(e)) => return Err(e.into()),
444 }
445 let mut bytes = Vec::new();
446 fence.read_to_end(&mut bytes)?;
447 if bytes.len() != 24 {
448 return Err(LocalError::RecoveryRequired);
449 }
450 let word = |i| u64::from_le_bytes(bytes[i..i + 8].try_into().unwrap());
451 if word(0) != config.writers || word(8) != config.writer {
452 return Err(LocalError::Configuration);
453 }
454 let generation = word(16);
455 if generation == 0 {
456 return Err(LocalError::RecoveryRequired);
457 }
458 let transaction = CommittedTransaction::read(&root, config)?;
460 let log = EventLog::<C::Delta>::from_wire_bytes_for(&transaction.log_bytes, &state)
461 .map_err(LocalError::History)?;
462 let mut inner = LocalReplica {
463 config,
464 fence,
465 held: true,
466 generation,
467 log: EventLog::for_crdt(&state),
468 state,
469 last_sequence: 0,
470 };
471 for record in log.records() {
474 if inner.admit(inner.ticket(), record.clone(), false)? != Admission::Accepted {
475 return Err(LocalError::InvalidHistory);
476 }
477 }
478 if inner.last_sequence != transaction.last_sequence {
479 return Err(LocalError::InvalidHistory);
480 }
481 inner.renew(inner.ticket())?;
483 Ok(Self {
484 inner,
485 path: transaction_path(&root, config),
486 })
487 }
488}
489
490impl DurableReplica<GCounter> {
491 pub fn restart_counter(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
494 config.validate().map_err(|_| LocalError::Configuration)?;
495 let n = usize::try_from(config.writers).map_err(|_| LocalError::Configuration)?;
496 Self::restart(root, config, GCounter::new(n))
497 }
498 pub fn counter(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
499 config.validate().map_err(|_| LocalError::Configuration)?;
500 let n = usize::try_from(config.writers).map_err(|_| LocalError::Configuration)?;
501 Self::fresh(root, config, GCounter::new(n))
502 }
503 pub fn bump(
504 &mut self,
505 ticket: WriteTicket,
506 tally: u64,
507 ) -> Result<Record<GCounterDelta>, LocalError> {
508 self.append(
509 ticket,
510 GCounterDelta {
511 replica: self.inner.config.writer as usize,
512 tally,
513 },
514 )
515 }
516}
517impl DurableReplica<OrSet<String, u64>> {
518 pub fn restart_utf8_set(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
520 Self::restart(root, config, OrSet::new())
521 }
522 pub fn utf8_set(root: &Path, config: WriterConfig) -> Result<Self, LocalError> {
523 Self::fresh(root, config, OrSet::new())
524 }
525 pub fn add(
526 &mut self,
527 ticket: WriteTicket,
528 element: String,
529 ) -> Result<Record<OrSetDelta<String, u64>>, LocalError> {
530 let sequence = next_sequence(self.inner.last_sequence).ok_or(LocalError::Exhausted)?;
531 let token = allocate_token(
532 self.inner.config.writers,
533 self.inner.config.writer,
534 sequence,
535 )
536 .ok_or(LocalError::Exhausted)?;
537 self.append(ticket, OrSetDelta::Add { element, token })
538 }
539 pub fn remove(
540 &mut self,
541 ticket: WriteTicket,
542 element: &String,
543 ) -> Result<Record<OrSetDelta<String, u64>>, LocalError> {
544 self.append(
545 ticket,
546 OrSetDelta::Remove {
547 tokens: self
548 .inner
549 .state
550 .observed_tokens(element)
551 .into_iter()
552 .collect(),
553 },
554 )
555 }
556}
557
558#[cfg(test)]
559mod durable_tests {
560 use super::*;
561 use alloc::string::ToString;
562 use std::{
563 process::Command,
564 sync::atomic::{AtomicU64, Ordering},
565 };
566
567 fn unique<T: PartialEq>(values: &[T]) -> bool {
570 values
571 .iter()
572 .enumerate()
573 .all(|(i, x)| !values[..i].contains(x))
574 }
575 fn allocations<D>(records: &[Record<D>]) -> Vec<RecordId> {
576 records.iter().map(|r| r.id).collect()
577 }
578 fn tokens(records: &[Record<OrSetDelta<String, u64>>]) -> Vec<u64> {
579 records
580 .iter()
581 .filter_map(|r| match r.delta {
582 OrSetDelta::Add { token, .. } => Some(token),
583 OrSetDelta::Remove { .. } => None,
584 })
585 .collect()
586 }
587 fn check_allocations<T: PartialEq + Clone>(name: &str, before: &[T], after: &[T]) {
588 assert!(!before.is_empty() && !after.is_empty());
589 let mut all = before.to_vec();
590 all.extend_from_slice(after);
591 assert!(unique(&all), "{name}: allocation reused");
592 assert!(!before.iter().any(|x| after.contains(x)));
593 all.push(before[0].clone());
594 assert!(!unique(&all), "{name}: planted duplicate escaped collector");
595 std::println!(
596 "{name} before={} after={} overlap=0 planted-duplicate=detected",
597 before.len(),
598 after.len()
599 );
600 }
601 fn joined_exchange<C>(
602 a: &mut DurableReplica<C>,
603 b: &mut DurableReplica<C>,
604 empty: impl Fn() -> C,
605 ) where
606 C: Crdt + PartialEq + std::fmt::Debug,
607 C::Delta: OwnedDelta + Clone + PartialEq + WireEncode + WireDecode + WireSchema,
608 {
609 let ab: Vec<_> = a
610 .log()
611 .since(b.log().version())
612 .iter()
613 .map(|r| r.to_wire_bytes().unwrap())
614 .collect();
615 let ba: Vec<_> = b
616 .log()
617 .since(a.log().version())
618 .iter()
619 .map(|r| r.to_wire_bytes().unwrap())
620 .collect();
621 assert!(!ab.is_empty() && !ba.is_empty());
622 let mut finals = Vec::new();
624 for batches in [[&ab, &ba], [&ba, &ab]] {
625 let mut state = empty();
626 let mut log = EventLog::for_crdt(&state);
627 for batch in batches {
628 for packet in batch {
629 let record = Record::<C::Delta>::from_wire_bytes(packet).unwrap();
630 assert_eq!(
631 log.admit_with(record, |d| state.apply_delta(d.clone())),
632 Admission::Accepted
633 );
634 }
635 }
636 finals.push(state);
637 }
638 assert_eq!(finals[0], finals[1]);
639 for (receiver, packets) in [(b, ab), (a, ba)] {
640 for packet in packets {
641 assert_eq!(
642 receiver
643 .receive(
644 receiver.ticket(),
645 Record::<C::Delta>::from_wire_bytes(&packet).unwrap()
646 )
647 .unwrap(),
648 Admission::Accepted
649 );
650 }
651 assert_eq!(receiver.state(), &finals[0]);
652 }
653 }
654 fn joined_config(writer: u64) -> WriterConfig {
655 WriterConfig { writers: 2, writer }
656 }
657 fn joined_finish(root: &Path, loss: bool) {
658 let counter_root = root.join("counter");
659 let set_root = root.join("set");
660 let before_c = EventLog::<GCounterDelta>::from_wire_bytes_for(
661 &fs::read(root.join("counter-issued")).unwrap(),
662 &GCounter::new(2),
663 )
664 .unwrap();
665 let before_s = EventLog::<OrSetDelta<String, u64>>::from_wire_bytes_for(
666 &fs::read(root.join("set-issued")).unwrap(),
667 &OrSet::new(),
668 )
669 .unwrap();
670 assert_eq!(before_c.records().len(), 2);
671 assert_eq!(before_s.records().len(), 2);
672 let mut a = DurableReplica::restart_counter(&counter_root, joined_config(0)).unwrap();
673 let mut sa = DurableReplica::restart_utf8_set(&set_root, joined_config(0)).unwrap();
674 if loss {
675 assert_eq!(a.state().state(), &[0, 0]);
676 assert!(!sa.state().contains(&"café☕".into()));
677 assert!(!sa.state().contains(&"東京".into()));
678 assert!(a.log().records().is_empty() && sa.log().records().is_empty());
679 std::println!(
680 "joined LOSS: acknowledged counter=9 and UTF-8 edits absent after process restart"
681 );
682 return;
683 }
684 assert_eq!(a.state().state(), &[9, 0]);
685 for word in ["café☕", "東京"] {
686 assert!(sa.state().contains(&word.into()));
687 }
688 let mut b = DurableReplica::restart_counter(&counter_root, joined_config(1)).unwrap();
689 let mut sb = DurableReplica::restart_utf8_set(&set_root, joined_config(1)).unwrap();
690 let after_c = [
691 a.bump(a.ticket(), 12).unwrap(),
692 b.bump(b.ticket(), 7).unwrap(),
693 ];
694 let after_s = [
695 sa.add(sa.ticket(), "naïve".into()).unwrap(),
696 sb.add(sb.ticket(), "γειά".into()).unwrap(),
697 ];
698 check_allocations(
699 "counter IDs",
700 &allocations(before_c.records()),
701 &allocations(&after_c),
702 );
703 check_allocations(
704 "set IDs",
705 &allocations(before_s.records()),
706 &allocations(&after_s),
707 );
708 check_allocations("set tokens", &tokens(before_s.records()), &tokens(&after_s));
709 joined_exchange(&mut a, &mut b, || GCounter::new(2));
710 joined_exchange(&mut sa, &mut sb, OrSet::new);
711 assert_eq!(a.state(), b.state());
712 assert_eq!(a.state().state(), &[12, 7]);
713 assert_eq!(sa.state(), sb.state());
714 for word in ["café☕", "東京", "naïve", "γειά"] {
715 assert!(sa.state().contains(&word.into()));
716 }
717 for r in before_c.records() {
718 assert!(a.log().records().contains(r) && b.log().records().contains(r));
719 }
720 for r in before_s.records() {
721 assert!(sa.log().records().contains(r) && sb.log().records().contains(r));
722 }
723 let state_c = a.state().clone();
724 let state_s = sa.state().clone();
725 drop((a, b, sa, sb));
726 for writer in 0..2 {
727 assert_eq!(
728 DurableReplica::restart_counter(&counter_root, joined_config(writer))
729 .unwrap()
730 .state(),
731 &state_c
732 );
733 assert_eq!(
734 DurableReplica::restart_utf8_set(&set_root, joined_config(writer))
735 .unwrap()
736 .state(),
737 &state_s
738 );
739 }
740 std::println!("joined HAPPY: all acknowledged records survive; both exchange orders converge; second restart survives");
741 }
742
743 fn joined_start(root: &Path, loss: bool) {
744 for name in ["counter", "set"] {
745 fs::create_dir_all(root.join(name)).unwrap();
746 }
747 fs::File::open(root).unwrap().sync_all().unwrap();
749 let _b = DurableReplica::counter(&root.join("counter"), joined_config(1)).unwrap();
751 let _sb = DurableReplica::utf8_set(&root.join("set"), joined_config(1)).unwrap();
752 let mut c = DurableReplica::counter(&root.join("counter"), joined_config(0)).unwrap();
753 let mut s = DurableReplica::utf8_set(&root.join("set"), joined_config(0)).unwrap();
754 if loss {
755 c.inner.bump(c.ticket(), 5).unwrap();
758 c.inner.bump(c.ticket(), 9).unwrap();
759 s.inner.add(s.ticket(), "café☕".into()).unwrap();
760 s.inner.add(s.ticket(), "東京".into()).unwrap();
761 } else {
762 c.bump(c.ticket(), 5).unwrap();
763 c.bump(c.ticket(), 9).unwrap();
764 s.add(s.ticket(), "café☕".into()).unwrap();
765 s.add(s.ticket(), "東京".into()).unwrap();
766 }
767 fs::write(
769 root.join("counter-issued"),
770 c.log().to_wire_bytes().unwrap(),
771 )
772 .unwrap();
773 fs::write(root.join("set-issued"), s.log().to_wire_bytes().unwrap()).unwrap();
774 std::println!("joined ACK counter=5,9 set=café☕,東京 loss={loss}");
775 std::io::stdout().flush().unwrap();
776 std::process::exit(77);
778 }
779 static SERIAL: AtomicU64 = AtomicU64::new(0);
780 fn root() -> PathBuf {
781 let root = std::env::temp_dir().join(format!(
782 "durable-{}-{}",
783 std::process::id(),
784 SERIAL.fetch_add(1, Ordering::Relaxed)
785 ));
786 fs::create_dir_all(&root).unwrap();
787 root
788 }
789 fn config() -> WriterConfig {
790 WriterConfig {
791 writers: 2,
792 writer: 0,
793 }
794 }
795 fn read(root: &Path) -> CommittedTransaction {
796 CommittedTransaction::read(root, config()).unwrap()
797 }
798 fn fault(boundary: u8, crash: bool) {
799 persistence::FAULT.with(|f| f.set((boundary, crash)));
800 }
801 fn exercise(
802 kind: &str,
803 root: &Path,
804 boundary: u8,
805 ) -> (CommittedTransaction, CommittedTransaction) {
806 let old;
807 if kind == "counter" {
808 let mut r = DurableReplica::counter(root, config()).unwrap();
809 r.bump(r.ticket(), 5).unwrap();
810 old = read(root);
811 fault(boundary, true);
812 assert_eq!(r.bump(r.ticket(), 9).unwrap().id.sequence, 2);
813 assert_eq!(r.state().state(), &[9, 0]);
814 } else {
815 let mut r = DurableReplica::utf8_set(root, config()).unwrap();
816 r.add(r.ticket(), "café☕".into()).unwrap();
817 old = read(root);
818 fault(boundary, true);
819 assert_eq!(
820 r.remove(r.ticket(), &"café☕".into()).unwrap().id.sequence,
821 2
822 );
823 assert!(!r.state().contains(&"café☕".into()));
824 assert_eq!(r.state().tombstones().len(), 1);
825 }
826 std::println!("ACK {kind}");
828 std::io::stdout().flush().unwrap();
829 persistence::checkpoint(8).unwrap();
830 (old, read(root))
831 }
832 fn replay(kind: &str, transaction: &CommittedTransaction) {
833 assert_eq!(transaction.config, config());
834 assert_eq!(transaction.last_sequence, 2);
835 if kind == "counter" {
836 let mut state = GCounter::new(2);
837 let log =
838 EventLog::<GCounterDelta>::from_wire_bytes_for(&transaction.log_bytes, &state)
839 .unwrap();
840 assert_eq!(log.records().len(), 2);
841 assert_eq!(log.to_wire_bytes().unwrap(), transaction.log_bytes);
842 for record in log.records() {
843 state.apply_delta(record.delta.clone());
844 }
845 assert_eq!(state.state(), &[9, 0]);
846 } else {
847 let mut state = OrSet::<String, u64>::new();
848 let log = EventLog::<OrSetDelta<String, u64>>::from_wire_bytes_for(
849 &transaction.log_bytes,
850 &state,
851 )
852 .unwrap();
853 assert_eq!(log.records().len(), 2);
854 assert_eq!(log.to_wire_bytes().unwrap(), transaction.log_bytes);
855 for record in log.records() {
856 state.apply_delta(record.delta.clone());
857 }
858 assert!(!state.contains(&"café☕".into()));
859 assert_eq!(
860 state.tombstones().iter().copied().collect::<Vec<_>>(),
861 alloc::vec![2]
862 );
863 }
864 }
865 #[test]
866 fn restart_still_works_and_fresh_allocation() {
867 let root = root();
868 let mut r = DurableReplica::counter(&root, config()).unwrap();
869 let before = r.bump(r.ticket(), 5).unwrap();
870 r.receive(
871 r.ticket(),
872 Record {
873 id: RecordId {
874 replica: 1,
875 sequence: 40,
876 },
877 delta: GCounterDelta {
878 replica: 1,
879 tally: 7,
880 },
881 },
882 )
883 .unwrap();
884 let old = r.ticket();
885 let state = r.state().state().to_vec();
886 let log = r.log().to_wire_bytes().unwrap();
887 let allocation = r.allocation_bytes();
888 drop(r);
889 let mut r = DurableReplica::restart_counter(&root, config()).unwrap();
890 assert_eq!(r.state().state(), state);
891 assert_eq!(r.log().to_wire_bytes().unwrap(), log);
892 assert_eq!(r.allocation_bytes(), allocation);
893 assert!(matches!(r.bump(old, 9), Err(LocalError::Refused)));
894 let after = r.bump(r.ticket(), 9).unwrap();
895 assert_eq!(
896 before.id,
897 RecordId {
898 replica: 0,
899 sequence: 1
900 }
901 );
902 assert_eq!(
903 after.id,
904 RecordId {
905 replica: 0,
906 sequence: 2
907 }
908 );
909 assert_eq!(r.state().state(), &[9, 7]);
910 std::println!(
911 "controls=7,2 counter replay={state:?} writable=true IDs {:?} -> {:?}",
912 before.id,
913 after.id
914 );
915 drop(r);
916 assert_eq!(
917 DurableReplica::restart_counter(&root, config())
918 .unwrap()
919 .state()
920 .state(),
921 &[9, 7]
922 );
923
924 let root = self::root();
925 let mut r = DurableReplica::utf8_set(&root, config()).unwrap();
926 let before = r.add(r.ticket(), "café☕".into()).unwrap();
927 let removed = r.remove(r.ticket(), &"café☕".into()).unwrap();
928 r.receive(
929 r.ticket(),
930 Record {
931 id: RecordId {
932 replica: 1,
933 sequence: 40,
934 },
935 delta: OrSetDelta::Add {
936 element: "東京".into(),
937 token: 81,
938 },
939 },
940 )
941 .unwrap();
942 let state = r.state().clone();
943 let log = r.log().to_wire_bytes().unwrap();
944 let allocation = r.allocation_bytes();
945 let old = r.ticket();
946 drop(r);
947 let mut r = DurableReplica::restart_utf8_set(&root, config()).unwrap();
948 assert_eq!(r.state(), &state);
949 assert_eq!(r.log().to_wire_bytes().unwrap(), log);
950 assert_eq!(r.allocation_bytes(), allocation);
951 assert!(matches!(
952 r.add(old, "stale".into()),
953 Err(LocalError::Refused)
954 ));
955 let after = r.add(r.ticket(), "café☕".into()).unwrap();
956 assert_eq!(
957 before.id,
958 RecordId {
959 replica: 0,
960 sequence: 1
961 }
962 );
963 assert_eq!(
964 removed.id,
965 RecordId {
966 replica: 0,
967 sequence: 2
968 }
969 );
970 assert_eq!(
971 after.id,
972 RecordId {
973 replica: 0,
974 sequence: 3
975 }
976 );
977 assert!(matches!(before.delta, OrSetDelta::Add { token: 2, .. }));
978 assert!(matches!(after.delta, OrSetDelta::Add { token: 6, .. }));
979 assert!(r.state().contains(&"café☕".into()));
980 assert!(r.state().contains(&"東京".into()));
981 assert!(r.state().tombstones().contains(&2));
982 std::println!("controls=7,2 set replay=equal including tombstone 2 and remote token 81; writable=true IDs {:?}, {:?} -> {:?}; tokens 2 -> 6", before.id, removed.id, after.id);
983 drop(r);
984 let r = DurableReplica::restart_utf8_set(&root, config()).unwrap();
985 assert!(r.state().contains(&"café☕".into()));
986 assert!(r.state().tombstones().contains(&2));
987 }
988 fn initialized(kind: &str) -> PathBuf {
989 let root = root();
990 if kind == "counter" {
991 let mut r = DurableReplica::counter(&root, config()).unwrap();
992 r.bump(r.ticket(), 5).unwrap();
993 } else {
994 let mut r = DurableReplica::utf8_set(&root, config()).unwrap();
995 r.add(r.ticket(), "café☕".into()).unwrap();
996 }
997 root
998 }
999 fn restart_and_write(kind: &str, root: &Path, config: WriterConfig) -> Result<(), LocalError> {
1001 if kind == "counter" {
1002 let mut r = DurableReplica::restart_counter(root, config)?;
1003 r.bump(r.ticket(), 9)?;
1004 } else {
1005 let mut r = DurableReplica::restart_utf8_set(root, config)?;
1006 r.add(r.ticket(), "new".into())?;
1007 }
1008 Ok(())
1009 }
1010 #[test]
1011 fn restart_corrupt_history() {
1012 for kind in ["counter", "set"] {
1013 let root = initialized(kind);
1014 let path = transaction_path(&root, config());
1015 let mut bytes = fs::read(&path).unwrap();
1016 *bytes.last_mut().unwrap() ^= 1;
1018 fs::write(&path, &bytes).unwrap();
1019 let result = restart_and_write(kind, &root, config());
1020 std::println!(
1021 "control=3 {kind} result={result:?} writable={}",
1022 result.is_ok()
1023 );
1024 assert!(matches!(
1025 result,
1026 Err(LocalError::History(WireError::IntegrityMismatch))
1027 ));
1028 assert_eq!(fs::read(&path).unwrap(), bytes);
1029 }
1030 }
1031 #[test]
1032 fn restart_mismatched_history() {
1033 for kind in ["counter", "set"] {
1034 let root = initialized(kind);
1035 let path = transaction_path(&root, config());
1036 let original = fs::read(&path).unwrap();
1037 let mut bytes = original.clone();
1038 bytes[..8].copy_from_slice(&3u64.to_le_bytes());
1039 fs::write(&path, &bytes).unwrap();
1040 let result = restart_and_write(kind, &root, config());
1041 std::println!(
1042 "control=4 {kind} result={result:?} writable={}",
1043 result.is_ok()
1044 );
1045 assert!(matches!(result, Err(LocalError::Configuration)));
1046 assert_eq!(fs::read(&path).unwrap(), bytes);
1047 fs::write(&path, original).unwrap();
1048 assert!(matches!(
1049 restart_and_write(
1050 kind,
1051 &root,
1052 WriterConfig {
1053 writers: 3,
1054 writer: 0
1055 }
1056 ),
1057 Err(LocalError::Configuration)
1058 ));
1059 }
1060 let root = initialized("counter");
1061 assert!(matches!(
1062 DurableReplica::restart_utf8_set(&root, config()),
1063 Err(LocalError::History(WireError::DeltaTypeMismatch))
1064 ));
1065 }
1066 #[test]
1067 fn restart_invalid_history() {
1068 for kind in ["counter", "set"] {
1069 let root = initialized(kind);
1070 let path = transaction_path(&root, config());
1071 if kind == "counter" {
1073 let mut log = EventLog::for_crdt(&GCounter::new(2));
1074 assert_eq!(
1075 log.insert_record(Record {
1076 id: RecordId {
1077 replica: 0,
1078 sequence: 1
1079 },
1080 delta: GCounterDelta {
1081 replica: 1,
1082 tally: 5
1083 },
1084 }),
1085 Admission::Accepted
1086 );
1087 DurableReplica::<GCounter>::commit(&path, config(), &log, 1).unwrap();
1088 } else {
1089 let mut log = EventLog::for_crdt(&OrSet::<String, u64>::new());
1090 assert_eq!(
1091 log.insert_record(Record {
1092 id: RecordId {
1093 replica: 0,
1094 sequence: 1
1095 },
1096 delta: OrSetDelta::Add {
1097 element: "foreign".into(),
1098 token: 3
1099 },
1100 }),
1101 Admission::Accepted
1102 );
1103 DurableReplica::<OrSet<String, u64>>::commit(&path, config(), &log, 1).unwrap();
1104 }
1105 let bytes = fs::read(&path).unwrap();
1106 let result = restart_and_write(kind, &root, config());
1107 std::println!(
1108 "control=5 {kind} result={result:?} writable={}",
1109 result.is_ok()
1110 );
1111 assert!(matches!(
1112 result,
1113 Err(LocalError::Refused) | Err(LocalError::History(_))
1114 ));
1115 assert_eq!(fs::read(&path).unwrap(), bytes);
1116 let root = initialized(kind);
1117 let path = transaction_path(&root, config());
1118 let original = fs::read(&path).unwrap();
1119 for last in [0u64, 2, u64::MAX] {
1120 let mut bytes = original.clone();
1121 bytes[16..24].copy_from_slice(&last.to_le_bytes());
1122 fs::write(&path, &bytes).unwrap();
1123 assert!(matches!(
1124 restart_and_write(kind, &root, config()),
1125 Err(LocalError::InvalidHistory)
1126 ));
1127 assert_eq!(fs::read(&path).unwrap(), bytes);
1128 }
1129 }
1130 }
1131 #[test]
1132 fn restart_unavailable_ownership() {
1133 let root = root();
1134 let r = DurableReplica::counter(&root, config()).unwrap();
1135 assert!(matches!(
1136 DurableReplica::restart_counter(&root, config()),
1137 Err(LocalError::Refused)
1138 ));
1139 drop(r);
1140 restart_and_write("counter", &root, config()).unwrap();
1141 let root = self::root();
1142 let r = DurableReplica::utf8_set(&root, config()).unwrap();
1143 assert!(matches!(
1144 DurableReplica::restart_utf8_set(&root, config()),
1145 Err(LocalError::Refused)
1146 ));
1147 drop(r);
1148 restart_and_write("set", &root, config()).unwrap();
1149 std::println!("control=6 both primitives: held fence Refused, no replica; released fence and empty histories: writable");
1150 }
1151
1152 #[test]
1153 fn durable_child() {
1154 if let Ok(root) = std::env::var("SAFEMESH_JOINED_ROOT") {
1155 joined_start(
1156 Path::new(&root),
1157 std::env::var_os("SAFEMESH_JOINED_LOSS").is_some(),
1158 );
1159 }
1160 if let Ok(root) = std::env::var("SAFEMESH_DURABLE_ROOT") {
1161 let kind = std::env::var("SAFEMESH_DURABLE_KIND").unwrap();
1162 if std::env::var_os("SAFEMESH_DURABLE_READ").is_some() {
1163 replay(&kind, &read(Path::new(&root)));
1164 return;
1165 }
1166 let boundary = std::env::var("SAFEMESH_DURABLE_BOUNDARY")
1167 .unwrap()
1168 .parse()
1169 .unwrap();
1170 exercise(&kind, Path::new(&root), boundary);
1171 }
1172 }
1173 fn crash(kind: &str, boundary: u8, root: &Path) -> std::process::Output {
1174 let output = Command::new(std::env::current_exe().unwrap())
1175 .args([
1176 "--exact",
1177 "local::durable_tests::durable_child",
1178 "--nocapture",
1179 ])
1180 .env("SAFEMESH_DURABLE_ROOT", root)
1181 .env("SAFEMESH_DURABLE_KIND", kind)
1182 .env("SAFEMESH_DURABLE_BOUNDARY", boundary.to_string())
1183 .output()
1184 .unwrap();
1185 let status = root.join("child.exit");
1187 fs::write(&status, output.status.code().unwrap().to_string()).unwrap();
1188 assert_eq!(fs::read_to_string(status).unwrap(), "77", "{:?}", output);
1189 output
1190 }
1191 #[test]
1192 fn durable_still_works() {
1193 for loss in [false, true] {
1194 let root = root();
1195 let mut command = Command::new(std::env::current_exe().unwrap());
1196 command
1197 .args([
1198 "--exact",
1199 "local::durable_tests::durable_child",
1200 "--nocapture",
1201 ])
1202 .env("SAFEMESH_JOINED_ROOT", &root);
1203 if loss {
1204 command.env("SAFEMESH_JOINED_LOSS", "1");
1205 }
1206 let output = command.output().unwrap();
1207 fs::write(
1208 root.join("joined.exit"),
1209 output.status.code().unwrap().to_string(),
1210 )
1211 .unwrap();
1212 assert_eq!(fs::read_to_string(root.join("joined.exit")).unwrap(), "77");
1213 assert!(String::from_utf8_lossy(&output.stdout).contains("joined ACK"));
1214 joined_finish(&root, loss);
1215 }
1216
1217 for kind in ["counter", "set"] {
1218 let root = root();
1219 let (_, expected) = exercise(kind, &root, 0);
1220 assert_eq!(read(&root), expected);
1223 replay(kind, &read(&root));
1224 let restarted = Command::new(std::env::current_exe().unwrap())
1225 .args([
1226 "--exact",
1227 "local::durable_tests::durable_child",
1228 "--nocapture",
1229 ])
1230 .env("SAFEMESH_DURABLE_ROOT", &root)
1231 .env("SAFEMESH_DURABLE_KIND", kind)
1232 .env("SAFEMESH_DURABLE_READ", "1")
1233 .output()
1234 .unwrap();
1235 let status = root.join("restart.exit");
1236 fs::write(&status, restarted.status.code().unwrap().to_string()).unwrap();
1237 assert_eq!(fs::read_to_string(status).unwrap(), "0", "{restarted:?}");
1238 std::println!("control=6 {kind} ordinary-write/read/restart=PASS");
1239 }
1240 }
1241 #[test]
1242 fn durable_crash_boundaries() {
1243 for kind in ["counter", "set"] {
1244 let (old, new) = exercise(kind, &root(), 0);
1245 for boundary in 1..=8 {
1246 let root = root();
1247 let output = crash(kind, boundary, &root);
1248 let recovered = read(&root);
1249 assert_eq!(&recovered, if boundary < 5 { &old } else { &new });
1250 assert_eq!(
1251 String::from_utf8_lossy(&output.stdout).contains("ACK"),
1252 boundary == 8
1253 );
1254 std::println!(
1255 "control=2 {kind} boundary={boundary}/8 complete={}",
1256 if boundary < 5 { "old" } else { "new" }
1257 );
1258 }
1259 }
1260 }
1261 fn acknowledged_survives(kind: &str) {
1262 let root = root();
1263 let output = crash(kind, 8, &root);
1264 assert!(String::from_utf8_lossy(&output.stdout).contains("ACK"));
1265 replay(kind, &read(&root));
1266 std::println!("control=3 {kind} acknowledged-crash-recovery=PASS");
1267 }
1268 #[test]
1269 fn durable_acknowledged_survives_counter() {
1270 acknowledged_survives("counter");
1271 }
1272 #[test]
1273 fn durable_acknowledged_survives_set() {
1274 acknowledged_survives("set");
1275 }
1276 fn failure<C: Crdt + std::fmt::Debug>(mut r: DurableReplica<C>, delta: C::Delta, boundary: u8)
1277 where
1278 C::Delta: OwnedDelta + Clone + PartialEq + WireEncode + WireSchema,
1279 {
1280 let state = format!("{:?}", r.state());
1281 let log = r.log().to_wire_bytes().unwrap();
1282 let allocation = r.allocation_bytes();
1283 let ticket = r.ticket();
1284 fault(boundary, false);
1285 assert!(matches!(
1286 r.append(ticket, delta.clone()),
1287 Err(LocalError::Io(_))
1288 ));
1289 fault(0, false);
1290 assert_eq!(format!("{:?}", r.state()), state);
1291 assert_eq!(r.log().to_wire_bytes().unwrap(), log);
1292 assert_eq!(r.allocation_bytes(), allocation);
1293 assert!(matches!(
1294 r.append(ticket, delta.clone()),
1295 Err(LocalError::Refused)
1296 ));
1297 assert!(matches!(
1298 r.receive(
1299 ticket,
1300 Record {
1301 id: RecordId {
1302 replica: 0,
1303 sequence: 1
1304 },
1305 delta
1306 }
1307 ),
1308 Err(LocalError::Refused)
1309 ));
1310 assert!(matches!(r.renew(ticket), Err(LocalError::Refused)));
1311 }
1312 #[test]
1313 fn durable_io_failure_disables_writes() {
1314 for boundary in 2..=6 {
1315 failure(
1316 DurableReplica::counter(&root(), config()).unwrap(),
1317 GCounterDelta {
1318 replica: 0,
1319 tally: 9,
1320 },
1321 boundary,
1322 );
1323 failure(
1324 DurableReplica::utf8_set(&root(), config()).unwrap(),
1325 OrSetDelta::Add {
1326 element: "東京".into(),
1327 token: 2,
1328 },
1329 boundary,
1330 );
1331 std::println!(
1332 "controls=4,5 write/sync/uncertain-boundary={boundary} error+disabled=PASS"
1333 );
1334 }
1335 }
1336 #[test]
1337 fn durable_receive_and_refusal() {
1338 let root = root();
1339 let mut r = DurableReplica::counter(&root, config()).unwrap();
1340 let record = Record {
1341 id: RecordId {
1342 replica: 1,
1343 sequence: 1,
1344 },
1345 delta: GCounterDelta {
1346 replica: 1,
1347 tally: 7,
1348 },
1349 };
1350 assert_eq!(
1351 r.receive(r.ticket(), record.clone()).unwrap(),
1352 Admission::Accepted
1353 );
1354 let transaction = read(&root);
1355 assert_eq!(transaction.last_sequence, 0);
1356 assert_eq!(transaction.log_bytes, r.log().to_wire_bytes().unwrap());
1357 assert_eq!(
1358 r.receive(r.ticket(), record.clone()).unwrap(),
1359 Admission::Duplicate
1360 );
1361 let mut collision = record;
1362 collision.delta.tally = 8;
1363 assert_eq!(
1364 r.receive(r.ticket(), collision).unwrap(),
1365 Admission::Collision
1366 );
1367 assert!(matches!(
1368 r.append(
1369 r.ticket(),
1370 GCounterDelta {
1371 replica: 1,
1372 tally: 10
1373 }
1374 ),
1375 Err(LocalError::Refused)
1376 ));
1377 assert_eq!(read(&root), transaction);
1378 let old = r.ticket();
1379 let ticket = r.renew(old).unwrap();
1380 assert!(matches!(r.bump(old, 9), Err(LocalError::Refused)));
1381 r.bump(ticket, 9).unwrap();
1382 assert_eq!(read(&root).last_sequence, 1);
1383 drop(r);
1384 assert!(matches!(
1385 DurableReplica::counter(&root, config()),
1386 Err(LocalError::RecoveryRequired)
1387 ));
1388 }
1389}