Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_ore/src/seq/mod.rs

58.3 KiB, 453 runs

created by r1870400018:17912, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! A convergent repository in which a move is recorded as a move.
2//!
3//! Bytes take their identity from the splice that created them and never lose
4//! it. Position is a separate, derived layer: an ordered set of slots, each
5//! claiming a run of byte identities, ordered against each other by Fugue over
6//! origins that name *content* rather than positions. A move mints new slots at
7//! the destination and claims the moved bytes for them; a per-byte
8//! last-writer-wins register decides which slot owns each byte, so two
9//! concurrent moves of one run cannot duplicate it. Because an insertion's
10//! origin names a byte, and that byte's owning slot is wherever it currently
11//! lives, the insertion follows the move without anything being written to make
12//! it do so.
13//!
14//! # A file is a subtree
15//!
16//! There is one forest for the whole repository, and its root children are the
17//! files' **origin anchors**: one byte per file, born dead, minted by the
18//! [`Op::FileCreate`] whose identity is the file's identity. A file is the
19//! subtree beneath one of them, so a slot's file is read off the tree rather
20//! than off the record, and no operation carries a file at all.
21//!
22//! Two consequences are the point of the arrangement. A move between files needs
23//! no routing, because its destination anchor already names content in the file
24//! it lands in; and an edit made concurrently inside a range that moves between
25//! files follows it, for exactly the reason it follows an in-file move -- its
26//! anchor never mentioned a file either.
27//!
28//! # What it guarantees, and what it does not
29//!
30//! Two replicas that have applied the same operations render the same bytes,
31//! whatever order the operations arrived in. That is the whole of the promise.
32//! It is not a promise that the result is what either author wanted:
33//!
34//! - Two moves of the same run leave one copy, at the destination of whichever
35//! move is higher in op order. The loser's intent is discarded, and flagged.
36//! - Two moves of partly overlapping runs tear at the overlap. Both halves
37//! survive, in two places, which is deterministic and is almost certainly not
38//! what either author meant. It is flagged.
39//! - Two moves whose destinations sit inside each other's sources form a cycle.
40//! Inside one file it is broken by demotion: one move lands where its anchor
41//! content was originally written rather than where it now lives, which is
42//! flagged. **Where the cycle crosses a file boundary it is arbitrated instead**,
43//! as one concurrent group: the member highest in op order completes, and every
44//! other member is confined -- its claims are not written, so its content stays
45//! where it was, and both files are told by [`Flag::Confined`]. Nothing has to be
46//! undone, because nothing was done.
47//! - Two splices that concurrently named overlapping content are arbitrated as
48//! one group, so that the contended region holds whole hunks rather than two
49//! authors' bytes interleaved. The member highest in op order prevails; every
50//! member concurrent with it **yields**, its removals not burying and its
51//! insertion buried whole, and [`Flag::Yielded`] names the group and its
52//! maximum. A member in the winner's causal past keeps its work, which is what
53//! leaves a winner's own earlier hunks alone -- and is also why the region is a
54//! promise of whole hunks and not a promise of one author. [`Flag::Overlap`]
55//! still fires beneath the arbitration, as the raw fact it always was.
56//! - Content moved into a file that has been deleted renders nowhere a reader
57//! looks. Nothing is lost and [`Flag::MovedIntoDeleted`] says so.
58//! - An insertion whose every anchored neighbour was deleted by a concurrent
59//! operation renders as a fragment at the deletion site, its context gone.
60//! Nothing is lost and [`Flag::Stranded`] says so, naming both operations.
61//! - An edit concurrent with the deletion of its own file renders only into the
62//! deleted file, which no reader looks at. Nothing is lost and
63//! [`Flag::SplicedIntoDeleted`] says so, naming the edit and the deletion. A
64//! deletion causally ordered with the edit -- either seeing the other -- is a
65//! decision rather than a race, and raises nothing.
66//!
67//! The posture is to converge always and to say what happened always. Every
68//! [`render::Flag`] is a function of the operation set, so a flag is a fact
69//! about the history rather than a note about this run of the renderer.
70//!
71//! # Transient by construction
72//!
73//! The durable record is the operation log. Everything here -- the atoms, the
74//! claim register, the tombstones, the slots, their order -- is derived, and is
75//! rebuilt from the operation set on every render. A [`Sequence`] is therefore
76//! an accumulator and nothing more: applying an operation is set insertion, and
77//! two sequences holding the same operations are the same sequence whatever
78//! order they were built in.
79//!
80//! # Preconditions
81//!
82//! Rendering requires a **causally complete** operation set, and that is checked
83//! against the operations' own parents rather than inferred from what they happen
84//! to name. Every parent must be present, and so must every atom whose content an
85//! anchor or a range names, the origin anchors of the files included. An anchor
86//! naming an atom that has not arrived cannot be resolved, and rather than guess,
87//! the render fails and says which operation named what. [`crate::log::OpLog`]
88//! supplies a closed set by construction; a caller assembling operations by hand
89//! must arrange it.
90//!
91//! Rendering also lays out the whole repository, because ordering is
92//! repository-wide. Laying out only the closure of one file's origin anchor under
93//! the claim register would suffice and would usually be the file's own
94//! operations; that is design work owed, and until it is done, opening one file
95//! costs the repository.
96//!
97//! # Op order
98//!
99//! Every tie-break in the structure is decided by [`OpOrder`], the pair
100//! `(counter, replica)` ascending. Convergence needs only that the order is
101//! total, which it is for any counters at all; the intuition that a later edit
102//! wins needs the counter to be a Lamport clock, one greater than the greatest
103//! the replica has seen. [`crate::log::OpLog::next_counter`] mints exactly that,
104//! so an author who takes identifiers from the log gets the intuition for
105//! nothing; an author minting its own is responsible for the same rule.
106//!
107//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
108//! Anthropic Claude
109
110pub mod atom;
111pub mod claim;
112pub mod render;
113pub mod slot;
114
115#[cfg(test)]
116mod file_tests;
117#[cfg(test)]
118mod overlap_tests;
119#[cfg(test)]
120mod tests;
121#[cfg(test)]
122mod forget_tests;
123
124use crate::id::{
125 ContentId,
126 ContentRange,
127 OpId,
128 ReplicaId,
129};
130use crate::log::Causality;
131use crate::op::{
132 Header,
133 Mode,
134 Op,
135 Placing,
136 Record,
137 Stub,
138};
139use crate::seq::atom::Atoms;
140use crate::seq::claim::{
141 Claims,
142 Dead,
143};
144use crate::seq::render::{
145 Flag,
146 Rendered,
147 Repo,
148 Run,
149 Stats,
150};
151use crate::seq::slot::Slots;
152
153use oxedyne_fe2o3_core::prelude::*;
154use oxedyne_fe2o3_data::interval::IntervalMap;
155
156use std::collections::{
157 BTreeMap,
158 BTreeSet,
159};
160use std::ops::Range;
161
162
163/// The total order every tie-break in the structure is decided by: the Lamport
164/// counter first, the authoring replica second.
165///
166/// This is deliberately not the order on [`OpId`], which sorts by replica first
167/// and is meant for indexing. Sorting by counter first is what makes "the later
168/// edit wins" mean what it says.
169#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
170pub struct OpOrder {
171 pub counter: u64, // which decides
172 pub replica: u64, // which breaks the tie
173}
174
175impl OpOrder {
176 pub fn of(id: &OpId) -> Self {
177 Self {
178 counter: id.counter,
179 replica: id.replica.inner(),
180 }
181 }
182}
183
184
185/// An operation as the sequence holds it: what it says, and what its author had
186/// seen when they said it.
187#[derive(Clone, Debug, Eq, PartialEq)]
188struct Applied {
189 parents: Vec<OpId>, // the author's frontier when it was written
190 op: Op,
191}
192
193
194/// What the lifecycle operations say about one file.
195#[derive(Clone, Debug, Eq, PartialEq)]
196struct FileInfo {
197 path: Vec<u8>, // after every rename the set holds
198 mode: Mode, // after every mode assertion the set holds
199 live: bool, // whether it still exists
200}
201
202
203/// A repository's worth of operations, and the state they describe.
204///
205/// The state is the operation set and nothing else, so applying an operation is
206/// idempotent, commutative and cheap, and the files themselves are computed by
207/// [`Sequence::render`] when they are wanted. There is one sequence per
208/// repository rather than one per file: a file is a subtree of the forest the
209/// render lays out, and which subtree a slot is in is not knowable until the
210/// forest is laid out.
211#[derive(Clone, Debug, Default, Eq, PartialEq)]
212pub struct Sequence {
213 ops: BTreeMap<OpId, Applied>, // by identity
214 forgotten: BTreeMap<OpId, Placing>, // what every Forget in the set says
215}
216
217impl Sequence {
218
219 pub fn new() -> Self {
220 Self { ops: BTreeMap::new(), forgotten: BTreeMap::new() }
221 }
222
223 /// Builds a repository from an operation set, in any order.
224 pub fn build<I>(ops: I)
225 -> Outcome<Self>
226 where
227 I: IntoIterator<Item = (Header, Op)>,
228 {
229 let mut seq = Self::new();
230 for (head, op) in ops {
231 res!(seq.apply(head, op));
232 }
233 Ok(seq)
234 }
235
236 /// Applies an operation under the header that names it.
237 ///
238 /// Applying the same operation twice does nothing the second time. Applying
239 /// two different operations under one identity is refused: an identity names
240 /// one operation, and a structure that quietly kept the first would converge
241 /// on whichever replica saw which. Two headers differing only in their
242 /// parents are two different operations for the same reason.
243 pub fn apply(&mut self, head: Header, op: Op)
244 -> Outcome<()>
245 {
246 res!(op.validate());
247 let id = head.id();
248 // A forget is applied to the set as much as to itself: every operation
249 // it names is held from now on in the shape the forget says, whether
250 // the original was here first or arrives later, and whether what arrives
251 // is the original or the stub a repack wrote. That is what makes the
252 // set a function of its members and nothing else -- two replicas that
253 // hold the same operations and the same forgets render the same bytes,
254 // and it does not matter which of them still has the forgotten bytes on
255 // its disk.
256 if let Op::Forget { of, .. } = &op {
257 res!(self.forget(of));
258 }
259 let op = match self.forgotten.get(&id) {
260 Some(placing) => Op::Forgotten { placing: placing.clone() },
261 None => op,
262 };
263 let applied = Applied { parents: head.parents().to_vec(), op };
264 match self.ops.get(&id) {
265 Some(seen) if *seen != applied => Err(err!(
266 "The identity {} already names a different {}; an operation \
267 identity names one operation.", id, seen.op.name();
268 Invalid, Input, Conflict)),
269 Some(_) => Ok(()),
270 None => {
271 self.ops.insert(id, applied);
272 Ok(())
273 },
274 }
275 }
276
277 /// Holds every operation the stubs name in the shape they say, from now on
278 /// and whether the original is here yet or not.
279 ///
280 /// This is what applying a [`Op::Forget`] does to the set, split out so that
281 /// it can be done without the forget itself joining the set: a state older
282 /// than the forget, rendered from the ancestry of a mark, holds none of the
283 /// forget's parents and cannot carry the forget, and must still not show
284 /// what was forgotten. The bytes are gone from every state there ever was,
285 /// which is what forgetting means.
286 pub fn forget(&mut self, of: &[Stub])
287 -> Outcome<()>
288 {
289 for stub in of {
290 match self.forgotten.get(&stub.id) {
291 Some(seen) if *seen != stub.placing => return Err(err!(
292 "The operation {} is said to keep one shape by one forget and \
293 another by another; a forgotten operation keeps one shape.", stub.id;
294 Invalid, Input, Conflict)),
295 _ => (),
296 }
297 }
298 for stub in of {
299 self.forgotten.insert(stub.id, stub.placing.clone());
300 if let Some(held) = self.ops.get_mut(&stub.id) {
301 held.op = Op::Forgotten { placing: stub.placing.clone() };
302 }
303 }
304 Ok(())
305 }
306
307 /// Every stub the forgets in this set have said, ascending by identifier.
308 pub fn forgotten(&self) -> Vec<Stub> {
309 self.forgotten.iter()
310 .map(|(id, placing)| Stub { id: *id, placing: placing.clone() })
311 .collect()
312 }
313
314 /// Takes every operation of another repository, and returns how many of them
315 /// were new.
316 ///
317 /// This is what a merge is. The state is the operation set and nothing else,
318 /// so two branches meet by taking the union of their sets, and the render of
319 /// the union is the convergent merge -- which is a fact about the two sets and
320 /// not about which branch absorbed which.
321 ///
322 /// Nothing is asked of the two sets causally. Closure is checked where it
323 /// matters, at [`Sequence::render_with`], against the graph the caller holds.
324 ///
325 /// An identity naming a different operation in each set is refused, for the
326 /// reason [`Sequence::apply`] refuses it, and nothing at all is taken: the two
327 /// sets are not two versions of one history and no part of the merge is worth
328 /// keeping.
329 pub fn absorb(&mut self, other: &Self)
330 -> Outcome<usize>
331 {
332 // The forgets of both sides first, since they decide the shape every
333 // operation is compared in: an original on one side and its stub on the
334 // other are one operation, not two.
335 let mut forgotten = self.forgotten.clone();
336 for (id, placing) in &other.forgotten {
337 match forgotten.get(id) {
338 Some(seen) if seen != placing => return Err(err!(
339 "The operation {} is forgotten in one shape in one repository and \
340 in another shape in the other; a forgotten operation keeps one \
341 shape, so the two are not branches of one history.", id;
342 Invalid, Input, Conflict)),
343 _ => {
344 forgotten.insert(*id, placing.clone());
345 },
346 }
347 }
348 let shaped = |id: &OpId, applied: &Applied| -> Applied {
349 match forgotten.get(id) {
350 Some(p) => Applied {
351 parents: applied.parents.clone(),
352 op: Op::Forgotten { placing: p.clone() },
353 },
354 None => applied.clone(),
355 }
356 };
357 let mut fresh: Vec<(OpId, Applied)> = Vec::new();
358 for (id, applied) in &other.ops {
359 let theirs = shaped(id, applied);
360 match self.ops.get(id) {
361 Some(seen) if shaped(id, seen) != theirs => return Err(err!(
362 "The identity {} names a {} in one repository and a {} in the \
363 other; an operation identity names one operation, so the two are \
364 not branches of one history.", id, seen.op.name(), theirs.op.name();
365 Invalid, Input, Conflict)),
366 Some(_) => (),
367 None => fresh.push((*id, theirs)),
368 }
369 }
370 let n = fresh.len();
371 for (id, applied) in fresh {
372 self.ops.insert(id, applied);
373 }
374 for (id, applied) in self.ops.iter_mut() {
375 if let Some(p) = forgotten.get(id) {
376 applied.op = Op::Forgotten { placing: p.clone() };
377 }
378 }
379 self.forgotten = forgotten;
380 Ok(n)
381 }
382
383 /// Applies a durable record.
384 ///
385 /// Everything the log holds belongs here, because the repository is what the
386 /// structure now models: a file's creation mints its origin anchor, a rename
387 /// changes its path, a deletion retires it, and the two content operations
388 /// place bytes. Only a mark says nothing about any of it, and it is kept
389 /// anyway so that the causal graph the render is judged by is not full of
390 /// holes.
391 pub fn apply_record(&mut self, rec: &Record)
392 -> Outcome<()>
393 {
394 self.apply(rec.head.clone(), rec.op.clone())
395 }
396
397 pub fn len(&self) -> usize {
398 self.ops.len()
399 }
400
401 pub fn is_empty(&self) -> bool {
402 self.ops.is_empty()
403 }
404
405 pub fn contains(&self, id: &OpId) -> bool {
406 self.ops.contains_key(id)
407 }
408
409 pub fn get(&self, id: &OpId)
410 -> Option<&Op>
411 {
412 self.ops.get(id).map(|a| &a.op)
413 }
414
415 pub fn parents_of(&self, id: &OpId)
416 -> Option<&[OpId]>
417 {
418 self.ops.get(id).map(|a| a.parents.as_slice())
419 }
420
421 /// Iterates the operations, in ascending order of identity.
422 pub fn iter(&self)
423 -> impl Iterator<Item = (&OpId, &Op)>
424 {
425 self.ops.iter().map(|(id, a)| (id, &a.op))
426 }
427
428 pub fn causality(&self)
429 -> Causality<'_>
430 {
431 Causality::new(self.ops.iter().map(|(id, a)| (*id, a.parents.as_slice())))
432 }
433
434 /// The operations in op order, which is the order every stage of the render
435 /// reads them in.
436 fn in_op_order(&self) -> Vec<(OpId, &Op)> {
437 let mut ops: Vec<(OpId, &Op)> = self.ops.iter()
438 .map(|(id, a)| (*id, &a.op))
439 .collect();
440 ops.sort_by_key(|(id, _)| OpOrder::of(id));
441 ops
442 }
443
444 /// Renders the repository, judging causality by the operations it holds.
445 ///
446 /// This is the ordinary path, the sequence being the whole history. Where a
447 /// caller holds the graph elsewhere -- a log covering more than this set --
448 /// use [`Sequence::render_with`].
449 pub fn render(&self)
450 -> Outcome<Repo>
451 {
452 self.render_with(&self.causality())
453 }
454
455 /// Renders the repository against a causal graph the caller holds.
456 ///
457 /// Fails if the graph is not causally closed, if it does not hold every
458 /// operation the sequence does, or if the operation set names content or a
459 /// file it does not hold. Every render is checked for conservation before it
460 /// is returned, in a release build as much as a debug one, because the
461 /// structures the check needs are the ones the render is still holding.
462 pub fn render_with(&self, cause: &Causality<'_>)
463 -> Outcome<Repo>
464 {
465 let ops = self.in_op_order();
466 res!(self.check_described(cause));
467 res!(Self::check_parents(cause));
468 let atoms = res!(Atoms::build(&ops));
469 res!(Self::check_complete(&ops, &atoms));
470 let files = res!(Self::files(&ops));
471
472 // Where every byte was written, which is one of the two readings the
473 // cross-file classifier takes and is also what tells the overlap groups
474 // which file they are contending over. It is read off a layout with every
475 // move voided and no arbitration applied, so it costs one layout and no
476 // iteration: that layout is acyclic by construction.
477 let birth = res!(Self::birth_files(&ops, &res!(Dead::build(&ops)), &atoms));
478
479 // The render is one fixed point over two arbitrations. A cross-file cycle
480 // confines moves; an overlap group yields splices; and the two are the same
481 // kind of decision, so they are settled in one loop rather than in two that
482 // would each have to be right about the other's answer. Each pass arbitrates
483 // at most one cycle, and a pass that arbitrates one voids at least one move
484 // and never un-voids any, so it terminates.
485 //
486 // The yields are recomputed on every pass. Today they cannot change, being a
487 // function of the operation set and the causal graph alone, and confining a
488 // move changes neither; recomputing them is what keeps the loop correct if a
489 // later rule ever lets a confinement change what overlaps. The traffic runs
490 // the other way and is real: yielding changes the tombstones, the tombstones
491 // change what a trial layout shows, and a trial layout is what tells the
492 // cycle rule which file a block is in.
493 let mut voided: BTreeSet<OpId> = BTreeSet::new();
494 let mut confined: Vec<(OpId, OpId, OpId)> = Vec::new();
495 let mut won: Vec<OpId> = Vec::new();
496 let (claims, slots, order, walk, dead, yields) = loop {
497 let yields = res!(Self::yields(&ops, &birth, cause));
498 let dead = res!(Dead::build_without(&ops, &yields.buried));
499 let claims = res!(Claims::build_without(&ops, &voided));
500 let slots = res!(Slots::place_without(&ops, &voided));
501 match res!(self.arbitrate(
502 &ops, &slots, &claims, &dead, &atoms, &birth, &voided, cause))
503 {
504 Some(decision) => {
505 for (op, home, denied) in decision.losers {
506 if !voided.insert(op) {
507 return Err(err!(
508 "The move {} was confined twice, so the cross-file cycle \
509 rule is not making progress.", op;
510 Bug));
511 }
512 confined.push((op, home, denied));
513 }
514 if let Some(w) = decision.winner {
515 won.push(w);
516 }
517 },
518 None => {
519 let order = res!(slots.order(&claims));
520 let walk = res!(render::traverse(
521 &slots, &order, &claims, &dead, &atoms, render::Emit::Bytes));
522 break (claims, slots, order, walk, dead, yields);
523 },
524 }
525 };
526
527 // The association a wire field would have asserted, derived instead.
528 let mut index: BTreeMap<OpId, OpId> = BTreeMap::new();
529 for (i, slot) in slots.all().iter().enumerate() {
530 if let Some(f) = walk.owner[i] {
531 index.insert(slot.place, f);
532 }
533 }
534
535 let mut flags: Vec<Flag> = Vec::new();
536 for (op, sub, origin) in &order.demoted {
537 flags.push(Flag::Demoted { op: *op, sub: *sub, origin: *origin });
538 if let Some(f) = Self::crossed_file(&slots, &claims, &walk.owner, *op, *sub) {
539 flags.push(f);
540 }
541 }
542 for (op, sub, origin) in &order.dropped {
543 flags.push(Flag::Dropped { op: *op, sub: *sub, origin: *origin });
544 }
545 for (op, sub) in &walk.orphans {
546 flags.push(Flag::Orphaned { op: *op, sub: *sub });
547 }
548 for (op, home, denied) in &confined {
549 flags.push(Flag::Confined { op: *op, home: *home, denied: *denied });
550 }
551 for op in &won {
552 flags.push(Flag::Won { op: *op });
553 }
554 for (op, y) in &yields.map {
555 flags.push(Flag::Yielded {
556 op: *op,
557 to: y.to,
558 group: y.group.clone(),
559 through: y.through,
560 });
561 }
562 for (id, op) in &ops {
563 if !op.is_move() {
564 continue;
565 }
566 if let Some(f) = index.get(id) {
567 if !files.get(f).map(|i| i.live).unwrap_or(false) {
568 flags.push(Flag::MovedIntoDeleted { op: *id, file: *f });
569 }
570 }
571 }
572 flags.extend(res!(Self::torn(&ops, &claims, &voided, cause)));
573 flags.extend(res!(Self::overlaps(&ops, cause)));
574 flags.extend(res!(Self::stranded(&ops, &files, &yields.buried, cause)));
575 flags.extend(res!(Self::spliced_into_deleted(&ops, &files, &index, cause)));
576 flags.sort();
577 flags.dedup();
578
579 // Each file keeps the flags that concern it, and the repository keeps all
580 // of them; a flag naming an operation that reached no file is the
581 // repository's alone.
582 let mut per_file: BTreeMap<OpId, Vec<Flag>> = BTreeMap::new();
583 for flag in &flags {
584 for f in Self::flag_files(flag, &index) {
585 per_file.entry(f).or_default().push(flag.clone());
586 }
587 }
588
589 // Notes are read back off the provenance the walk produced, which is where
590 // every move the operation set holds has already happened.
591 let (mut per_file_notes, repo_notes) = render::notes(&ops, &walk.files);
592
593 let mut walk_files = walk.files;
594 let mut out: Vec<Rendered> = Vec::new();
595 let mut rendered = 0u64;
596 let mut withheld = 0u64;
597 for (id, info) in &files {
598 let (bytes, runs) = walk_files.remove(id).unwrap_or_default();
599 rendered += bytes.len() as u64;
600 if !info.live {
601 withheld += bytes.len() as u64;
602 }
603 let mut flags = per_file.remove(id).unwrap_or_default();
604 flags.dedup();
605 let notes = per_file_notes.remove(id).unwrap_or_default();
606 out.push(Rendered::new(
607 *id, info.path.clone(), info.mode, info.live, bytes, runs, flags, notes));
608 }
609
610 let stats = Stats {
611 ops: ops.len(),
612 files: files.len(),
613 atoms: atoms.count(),
614 atom_bytes: atoms.total(),
615 slots_placed: slots.placed(),
616 slots_divided: slots.len(),
617 claim_intervals: claims.intervals(),
618 dead_intervals: dead.intervals(),
619 notes: repo_notes.len(),
620 max_depth: walk.max_depth,
621 rendered,
622 withheld,
623 orphaned: walk.orphaned,
624 };
625 let repo = Repo::new(out, flags, repo_notes, index, stats);
626 // Checked here, in every build, because here is where it is nearly free:
627 // the atoms and the tombstones are the ones this render just used, and
628 // [`Sequence::check_conservation`] exists only for a caller holding a
629 // render it did not produce. Rebuilding them to ask the same question
630 // cost 5.51 s of a 20.2 s open, measured on a 44,628 operation history.
631 res!(Self::conserved(&repo, &atoms, &dead));
632 Ok(repo)
633 }
634
635 /// Checks that the render accounts for every byte the operation set created:
636 /// each is either rendered exactly once, somewhere in the repository, or
637 /// dead, or owned by a slot that reached no file at all.
638 ///
639 /// The check is repository-wide because that is where it bites. A byte
640 /// rendered in two files at once is what a claim register scoped to one file
641 /// produces, and no per-file check would see it: each file would agree with
642 /// itself while the same bytes appeared in both. The render runs this under a
643 /// debug build; a caller wanting it in a release build calls it.
644 ///
645 /// The tombstones are rebuilt under the same overlap arbitration the render
646 /// used, since a yielded insertion is dead rather than homeless and a check told
647 /// otherwise would report every yield as a byte gone missing. Causality is
648 /// judged by the sequence's own operations, so a caller who rendered against a
649 /// wider graph should check the render it holds rather than this.
650 ///
651 /// **A render this sequence produced has been checked already**, by
652 /// [`Sequence::render_with`], and calling this on one rebuilds the atoms, the
653 /// tombstones, a whole trial layout and the overlap arbitration to reach the
654 /// same answer -- 5.51 s of a 20.2 s open, measured. What remains for this to
655 /// do is the case its name suggests: a repository assembled some other way,
656 /// or one deliberately damaged by a test.
657 pub fn check_conservation(&self, repo: &Repo)
658 -> Outcome<()>
659 {
660 let ops = self.in_op_order();
661 let cause = self.causality();
662 let atoms = res!(Atoms::build(&ops));
663 let birth = res!(Self::birth_files(&ops, &res!(Dead::build(&ops)), &atoms));
664 let yields = res!(Self::yields(&ops, &birth, &cause));
665 let dead = res!(Dead::build_without(&ops, &yields.buried));
666 Self::conserved(repo, &atoms, &dead)
667 }
668
669 /// Collects what the lifecycle operations say about each file, in op order.
670 fn files(ops: &[(OpId, &Op)])
671 -> Outcome<BTreeMap<OpId, FileInfo>>
672 {
673 let mut files: BTreeMap<OpId, FileInfo> = BTreeMap::new();
674 for (id, op) in ops {
675 match op {
676 Op::FileCreate { path } => {
677 files.insert(*id, FileInfo {
678 path: path.clone(),
679 mode: Mode::default(),
680 live: true,
681 });
682 },
683 // A forgotten file has no path to be laid out under and is dead
684 // from birth. It is still a file, so that a rename, a mode or a
685 // deletion naming it is complete rather than an error.
686 Op::Forgotten { placing: Placing::File } => {
687 files.insert(*id, FileInfo {
688 path: Vec::new(),
689 mode: Mode::default(),
690 live: false,
691 });
692 },
693 Op::FileRename { file, path } => {
694 match files.get_mut(file) {
695 Some(info) => info.path = path.clone(),
696 None => return Err(err!(
697 "The operation {} renames the file {}, which no operation \
698 in the set created; the set is not causally complete.",
699 id, file;
700 Invalid, Input, Missing)),
701 }
702 },
703 Op::FileMode { file, mode } => {
704 match files.get_mut(file) {
705 Some(info) => info.mode = *mode,
706 None => return Err(err!(
707 "The operation {} sets the mode of the file {}, which no \
708 operation in the set created; the set is not causally \
709 complete.", id, file;
710 Invalid, Input, Missing)),
711 }
712 },
713 Op::FileDelete { file } => {
714 match files.get_mut(file) {
715 Some(info) => info.live = false,
716 None => return Err(err!(
717 "The operation {} deletes the file {}, which no operation \
718 in the set created; the set is not causally complete.",
719 id, file;
720 Invalid, Input, Missing)),
721 }
722 },
723 _ => (),
724 }
725 }
726 Ok(files)
727 }
728
729 /// Arbitrates the first cross-file cycle the anchor graph holds, if it holds
730 /// one, and returns what it decided.
731 ///
732 /// Every cycle is asked in turn and the first that decides anything is the
733 /// answer, the caller voiding what it names and rendering again. A cycle inside
734 /// one file decides nothing and falls through to demotion, which is where it
735 /// has always been settled.
736 #[allow(clippy::too_many_arguments)]
737 fn arbitrate(
738 &self,
739 ops: &[(OpId, &Op)],
740 slots: &Slots,
741 claims: &Claims,
742 dead: &Dead,
743 atoms: &Atoms,
744 birth: &BTreeMap<OpId, OpId>,
745 voided: &BTreeSet<OpId>,
746 cause: &Causality<'_>,
747 )
748 -> Outcome<Option<Decision>>
749 {
750 let arbiter = Arbiter { ops, dead, atoms, birth, cause };
751 for cycle in res!(slots.cycles(claims)) {
752 if let Some(decision) = res!(arbiter.judge(&cycle, slots, voided)) {
753 return Ok(Some(decision));
754 }
755 }
756 Ok(None)
757 }
758
759 /// The file every atom was written into.
760 ///
761 /// Read off a layout with every move voided, so that the answer is where the
762 /// content would sit if nothing had ever been moved, which is what "the file
763 /// its origin names" means. That layout is always acyclic -- an origin names
764 /// content its author had already seen, so with no claims in play every edge
765 /// runs strictly downwards in op order -- so it costs one layout and no
766 /// iteration.
767 fn birth_files(ops: &[(OpId, &Op)], dead: &Dead, atoms: &Atoms)
768 -> Outcome<BTreeMap<OpId, OpId>>
769 {
770 let all: BTreeSet<OpId> = ops.iter()
771 .filter(|(_, op)| op.is_move())
772 .map(|(id, _)| *id)
773 .collect();
774 let laid = res!(Self::layout(ops, dead, atoms, &all));
775 let mut birth: BTreeMap<OpId, OpId> = BTreeMap::new();
776 for (i, slot) in laid.slots.all().iter().enumerate() {
777 // The slot the creating splice placed, which is where the content was
778 // written; a file's seed names the file itself.
779 if slot.place != slot.claim.op() {
780 continue;
781 }
782 if let Some(f) = laid.owner.get(i).copied().flatten() {
783 birth.insert(slot.claim.op(), f);
784 }
785 }
786 Ok(birth)
787 }
788
789 /// Lays the repository out with a set of moves voided, keeping enough of the
790 /// result to ask which file any byte sits in.
791 fn layout(
792 ops: &[(OpId, &Op)],
793 dead: &Dead,
794 atoms: &Atoms,
795 voided: &BTreeSet<OpId>,
796 )
797 -> Outcome<Layout>
798 {
799 let claims = res!(Claims::build_without(ops, voided));
800 let slots = res!(Slots::place_without(ops, voided));
801 let order = res!(slots.order(&claims));
802 // Only `owner` is kept below, so the content is not laid out at all.
803 let walk = res!(render::traverse(
804 &slots, &order, &claims, dead, atoms, render::Emit::OwnersOnly));
805 Ok(Layout { slots, claims, owner: walk.owner })
806 }
807
808 /// Raises a cross-file flag where breaking a cycle left a placement holding
809 /// content that was written into one file and renders in another.
810 ///
811 /// The comparison is between where the content was written and where the
812 /// demoted placement put it, not between the two ends of the demoted origin,
813 /// which a cycle drawn tight around one gap would report nothing about.
814 ///
815 /// Every demotion this sees is now inside one file, a cross-file cycle having
816 /// been arbitrated before the order was asked for. What still trips the flag is
817 /// a demoted placement whose content had legitimately changed files earlier,
818 /// and the flag is then telling the truth about the two files.
819 fn crossed_file(
820 slots: &Slots,
821 claims: &Claims,
822 owner: &[Option<OpId>],
823 op: OpId,
824 sub: u64,
825 )
826 -> Option<Flag>
827 {
828 let i = slots.find(&op, sub)?;
829 let slot = slots.get(i).ok()?;
830 // The slot placed by the splice that created this content, which is where
831 // the content was written and is where it would still be but for a move.
832 let born = ContentId::new(slot.claim.op(), slot.claim.from());
833 let home = slots.owner_slot(&born, claims, true).ok()?;
834 let from = (*owner.get(home)?)?;
835 let to = (*owner.get(i)?)?;
836 if from == to {
837 return None;
838 }
839 Some(Flag::CrossedFile { op, sub, from, to })
840 }
841
842 /// The files a flag concerns, so that each file keeps its own.
843 fn flag_files(flag: &Flag, index: &BTreeMap<OpId, OpId>) -> Vec<OpId> {
844 let mut out: Vec<OpId> = Vec::new();
845 match flag {
846 Flag::Overlap { ops, .. } => {
847 for id in ops {
848 if let Some(f) = index.get(id) {
849 out.push(*f);
850 }
851 }
852 },
853 Flag::CrossedFile { from, to, .. } => {
854 out.push(*from);
855 out.push(*to);
856 },
857 Flag::Confined { home, denied, .. } => {
858 out.push(*home);
859 out.push(*denied);
860 },
861 Flag::MovedIntoDeleted { file, .. } => out.push(*file),
862 Flag::Stranded { op, by } => {
863 for id in [op, by] {
864 if let Some(f) = index.get(id) {
865 out.push(*f);
866 }
867 }
868 },
869 Flag::SplicedIntoDeleted { file, .. } => out.push(*file),
870 // The yielder's file and the prevailing operation's, by the Stranded
871 // route, so that both authors are told where they are looking. The rest
872 // of the group is in the flag and is one lookup away; spraying the flag
873 // over every member's file would put a five-party collision into five
874 // files and tell nobody anything they could act on.
875 Flag::Yielded { op, to, .. } => {
876 for id in [op, to] {
877 if let Some(f) = index.get(id) {
878 out.push(*f);
879 }
880 }
881 },
882 other => {
883 if let Some(id) = other.op() {
884 if let Some(f) = index.get(&id) {
885 out.push(*f);
886 }
887 }
888 },
889 }
890 out.sort();
891 out.dedup();
892 out
893 }
894
895 /// Checks that the graph describes every operation the sequence holds.
896 fn check_described(&self, cause: &Causality<'_>)
897 -> Outcome<()>
898 {
899 for id in self.ops.keys() {
900 if !cause.contains(id) {
901 return Err(err!(
902 "The causal graph does not hold the operation {}, which the \
903 sequence does, so it cannot judge what that operation was \
904 written against.", id;
905 Invalid, Input, Missing));
906 }
907 }
908 Ok(())
909 }
910
911 /// Checks that every operation an operation was written against is present.
912 ///
913 /// This is the causal precondition proper, read off the parents rather than
914 /// guessed at from the content. An operation whose parent is absent has been
915 /// delivered ahead of history it depends on, and no amount of anchor
916 /// resolution can make up the difference.
917 fn check_parents(cause: &Causality<'_>)
918 -> Outcome<()>
919 {
920 if let Some((id, missing)) = cause.gap() {
921 return Err(err!(
922 "The operation {} was written against {}, which the operation set \
923 does not hold; the set is not causally complete.", id, missing;
924 Invalid, Input, Missing));
925 }
926 Ok(())
927 }
928
929 /// Checks that every content identifier an operation names exists.
930 ///
931 /// A file's origin anchor is one such identifier, so an operation anchored at
932 /// the start of a file the set does not hold is refused here along with
933 /// everything else it might have named.
934 ///
935 /// The content a note is *about* is named here too, although the note claims
936 /// none of it. A note whose subject has not arrived cannot be resolved, and
937 /// resolving it to nothing would be indistinguishable from a note on content
938 /// that has been deleted, which is a different fact about the repository.
939 fn check_complete(ops: &[(OpId, &Op)], atoms: &Atoms)
940 -> Outcome<()>
941 {
942 for (id, op) in ops {
943 for r in op.regions().iter().chain(op.note_on()) {
944 if r.to() > atoms.run_len(&r.op()) {
945 return Err(err!(
946 "The operation {} names the content {}, which the operation \
947 set does not hold; the set is not causally complete.", id, r;
948 Invalid, Input, Missing));
949 }
950 }
951 let (left, right) = op.origins();
952 for a in [left, right].into_iter().flatten() {
953 if a.content.off >= atoms.run_len(&a.content.op) {
954 return Err(err!(
955 "The operation {} is anchored {}, which the operation set \
956 does not hold; the set is not causally complete.", id, a;
957 Invalid, Input, Missing));
958 }
959 }
960 }
961 Ok(())
962 }
963
964 /// Finds the moves whose source was taken from them by a **concurrent** move.
965 ///
966 /// The claim register alone cannot tell a race from a sequence: a move
967 /// superseded on purpose, by a later move of the same content, looks exactly
968 /// like one that lost a race, because in both cases the register names
969 /// somebody else. The parents say which it was, and only a concurrent claim
970 /// tears. An author who moved a block and then moved it again is owed no flag,
971 /// and a flag they cannot act on is noise that hides the ones they can.
972 ///
973 /// A move confined by the cross-file cycle rule owns nothing either, and would
974 /// look torn to a register that could not tell why. It is told
975 /// [`Flag::Confined`] instead: the two flags name different events, and an
976 /// author is owed the one that happened.
977 fn torn(
978 ops: &[(OpId, &Op)],
979 claims: &Claims,
980 voided: &BTreeSet<OpId>,
981 cause: &Causality<'_>,
982 )
983 -> Outcome<Vec<Flag>>
984 {
985 let mut out: Vec<Flag> = Vec::new();
986 for (id, op) in ops {
987 if !op.is_move() || voided.contains(id) {
988 continue;
989 }
990 let mut lost: Vec<ContentRange> = Vec::new();
991 for r in op.regions() {
992 for (span, holder) in claims.runs(r) {
993 if holder == *id || !cause.concurrent(&holder, id) {
994 continue;
995 }
996 let gone = res!(ContentRange::new(r.op(), span.start, span.end));
997 match lost.last_mut() {
998 Some(last) if last.op() == gone.op() && last.to() == gone.from()
999 => res!(last.set_to(gone.to())),
1000 _ => lost.push(gone),
1001 }
1002 }
1003 }
1004 if !lost.is_empty() {
1005 out.push(Flag::Torn { op: *id, lost });
1006 }
1007 }
1008 Ok(out)
1009 }
1010
1011 /// Finds the pairs of concurrent operations that named the same content.
1012 ///
1013 /// A sweep over the named runs, atom by atom: two operations overlap when one
1014 /// starts before another ends. Origins are not counted, because an origin
1015 /// names a gap rather than a claim on content, and two insertions at one gap
1016 /// are ordered rather than in conflict.
1017 ///
1018 /// A pair that overlaps is then asked of the parents graph whether it was
1019 /// concurrent, and only a concurrent pair is flagged. An author who deleted a
1020 /// run they could already see was not in conflict with whoever wrote it, and
1021 /// saying so would make the flag noise. The concurrency test costs a walk of
1022 /// the graph per overlapping pair, and overlapping pairs are rare, so nothing
1023 /// is paid for the histories that have no conflict in them.
1024 fn overlaps(ops: &[(OpId, &Op)], cause: &Causality<'_>)
1025 -> Outcome<Vec<Flag>>
1026 {
1027 let mut named: Vec<(ContentRange, OpId)> = Vec::new();
1028 for (id, op) in ops {
1029 for r in op.regions() {
1030 if !r.is_empty() {
1031 named.push((*r, *id));
1032 }
1033 }
1034 }
1035 named.sort_by_key(|(r, id)| (r.op(), r.from(), r.to(), *id));
1036 let mut out: Vec<Flag> = Vec::new();
1037 // Runs still open at the current position, oldest first.
1038 let mut open: Vec<(ContentRange, OpId)> = Vec::new();
1039 for (r, id) in named {
1040 open.retain(|(o, _)| o.op() == r.op() && o.to() > r.from());
1041 for (o, other) in &open {
1042 if *other == id {
1043 continue;
1044 }
1045 if !cause.concurrent(other, &id) {
1046 continue;
1047 }
1048 if let Some(region) = o.intersection(&r) {
1049 let mut pair = vec![*other, id];
1050 pair.sort();
1051 out.push(Flag::Overlap { ops: pair, region });
1052 }
1053 }
1054 open.push((r, id));
1055 }
1056 Ok(out)
1057 }
1058
1059 /// Decides which splices yield, from the operation set, the causal graph, and
1060 /// the file each byte was written into.
1061 ///
1062 /// **The rule.** Concurrent splices whose named content intersects form a graph,
1063 /// one edge per pair [`Sequence::overlaps`] flags, and its connected components
1064 /// are the arbitration groups; components contending over the same files by the
1065 /// same set of replicas are taken together. The member highest in op order
1066 /// prevails and every member concurrent with it yields, its removals not burying
1067 /// and its insertion buried whole. A splice anchored wholly inside a buried
1068 /// insertion yields too.
1069 ///
1070 /// **Why the component and not the pair.** Arbitrating the intersection alone
1071 /// says nothing about either author's insertion, which is where the unreadable
1072 /// text comes from; arbitrating the whole file would void the work of somebody
1073 /// who was not contending. The component is the smallest unit that leaves the
1074 /// contended region reading as whole hunks. The same-contenders merge is what
1075 /// stops two components of one file being won by different replicas, which at
1076 /// two parties -- the case a small team hits -- is the whole of the fix.
1077 ///
1078 /// **The causal exemption.** A member in the winner's causal past does not
1079 /// yield. It is not decoration: an author whose capture emitted two hunks
1080 /// authored the second with the first as its parent, so without the exemption a
1081 /// winner would void its own other hunk. The price is that the region is a
1082 /// promise of whole hunks rather than a promise of one author, since a third
1083 /// party who synced with one side of a collision and not the other joins the
1084 /// group as its maximum and leaves two authors' hunks composed.
1085 ///
1086 /// **Which file a component is in** is read off the birth layout, where every
1087 /// byte was written. A component whose members named content born in more than
1088 /// one file has that whole set as its key and merges only with a component whose
1089 /// key matches, which is the per-file restriction stated so that it means
1090 /// something in a repository rather than in a single document.
1091 fn yields(
1092 ops: &[(OpId, &Op)],
1093 birth: &BTreeMap<OpId, OpId>,
1094 cause: &Causality<'_>,
1095 )
1096 -> Outcome<Yields>
1097 {
1098 // The splices, which is what arbitration reaches. A move already has two
1099 // arbitration rules of its own -- tearing, and the cross-file cycle -- and a
1100 // third interacting with the same fixed point is unbounded work for a case
1101 // nobody has yet observed.
1102 let sp: Vec<(OpId, &Op)> = ops.iter()
1103 .filter(|(_, op)| matches!(op, Op::Splice { .. }))
1104 .copied()
1105 .collect();
1106
1107 // The overlap graph, by the sweep `overlaps` uses: sort every named run by
1108 // atom and offset, and a run still open when another begins intersects it.
1109 let mut named: Vec<(ContentRange, usize)> = Vec::new();
1110 for (i, (_, op)) in sp.iter().enumerate() {
1111 for r in op.regions() {
1112 if !r.is_empty() {
1113 named.push((*r, i));
1114 }
1115 }
1116 }
1117 named.sort_by_key(|(r, i)| (r.op(), r.from(), r.to(), sp[*i].0));
1118 let mut up: Vec<usize> = (0..sp.len()).collect();
1119 let mut met: BTreeSet<usize> = BTreeSet::new();
1120 let mut open: Vec<(ContentRange, usize)> = Vec::new();
1121 for (r, i) in named {
1122 open.retain(|(o, _)| o.op() == r.op() && o.to() > r.from());
1123 for (_, j) in &open {
1124 if *j == i || !cause.concurrent(&sp[*j].0, &sp[i].0) {
1125 continue;
1126 }
1127 met.insert(i);
1128 met.insert(*j);
1129 join(&mut up, i, *j);
1130 }
1131 open.push((r, i));
1132 }
1133 let mut out = Yields::default();
1134 if met.is_empty() {
1135 return Ok(out);
1136 }
1137
1138 // The connected components, and then the same-contenders merge over them.
1139 let mut comps: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
1140 for i in met {
1141 let root = find(&mut up, i);
1142 comps.entry(root).or_default().push(i);
1143 }
1144 let mut groups: BTreeMap<(BTreeSet<ReplicaId>, BTreeSet<OpId>), Vec<usize>>
1145 = BTreeMap::new();
1146 for members in comps.values() {
1147 let mut reps: BTreeSet<ReplicaId> = BTreeSet::new();
1148 let mut over: BTreeSet<OpId> = BTreeSet::new();
1149 for i in members {
1150 reps.insert(sp[*i].0.replica);
1151 for r in sp[*i].1.regions() {
1152 if let Some(f) = birth.get(&r.op()) {
1153 over.insert(*f);
1154 }
1155 }
1156 }
1157 groups.entry((reps, over)).or_default().extend(members.iter().copied());
1158 }
1159
1160 // The op-order maximum prevails; every member concurrent with it yields.
1161 for members in groups.values_mut() {
1162 members.sort_by_key(|i| OpOrder::of(&sp[*i].0));
1163 let group: Vec<OpId> = members.iter().map(|i| sp[*i].0).collect();
1164 let winner = match group.last() {
1165 Some(w) => *w,
1166 None => return Err(err!("An overlap group with no members."; Bug)),
1167 };
1168 for id in &group {
1169 if cause.concurrent(&winner, id) {
1170 out.map.insert(*id, Yield {
1171 to: winner,
1172 group: group.clone(),
1173 through: None,
1174 });
1175 }
1176 }
1177 }
1178
1179 // Yielding is transitive. A splice whose insertion is anchored wholly within
1180 // an insertion that is buried is buried too: left where it is, it renders as
1181 // a fragment at a dead site, and no flag fires for it, because no concurrent
1182 // operation deleted anything. The winner cannot be reached this way -- to
1183 // anchor inside a buried insertion is to have seen it, and everything buried
1184 // is concurrent with the winner -- so the closure cannot bury a whole group.
1185 let mut inserted: BTreeSet<OpId> = BTreeSet::new();
1186 for (id, op) in ops {
1187 if let Op::Splice { insert, .. } = op {
1188 if !insert.is_empty() {
1189 inserted.insert(*id);
1190 }
1191 }
1192 }
1193 loop {
1194 let buried: BTreeSet<OpId> = out.map.keys()
1195 .copied()
1196 .filter(|o| inserted.contains(o))
1197 .collect();
1198 let inside = |a: &Option<crate::id::Anchor>| -> Option<OpId> {
1199 let host = a.as_ref()?.content.op;
1200 buried.contains(&host).then_some(host)
1201 };
1202 let mut added = false;
1203 for (id, op) in ops {
1204 if out.map.contains_key(id) || !matches!(op, Op::Splice { .. }) {
1205 continue;
1206 }
1207 let (l, r) = op.origins();
1208 let host = match (inside(&l), inside(&r)) {
1209 (Some(h), Some(_)) => h,
1210 _ => continue,
1211 };
1212 let parent = match out.map.get(&host) {
1213 Some(y) => y.clone(),
1214 None => continue,
1215 };
1216 out.map.insert(*id, Yield {
1217 to: parent.to,
1218 group: parent.group,
1219 through: Some(host),
1220 });
1221 added = true;
1222 }
1223 if !added {
1224 break;
1225 }
1226 }
1227 out.buried = out.map.keys().copied().collect();
1228 Ok(out)
1229 }
1230
1231 /// Finds the splices whose insertion anchored into content a **concurrent**
1232 /// operation deleted, so that the inserted bytes render at a deletion site
1233 /// rather than inside the context their author wrote them into.
1234 ///
1235 /// The context of an insertion is the content its origins name; a file's
1236 /// origin anchor names no content a splice can remove, so it is not context.
1237 /// The flag fires only where every contextual neighbour is dead -- an
1238 /// insertion with a living neighbour renders beside it, where its author put
1239 /// it -- and only a **concurrent** deleter is named. A deletion causally
1240 /// ordered against the splice, either way round, was a decision made in
1241 /// knowledge of the other operation and raises nothing; this is the same
1242 /// distinction [`Sequence::torn`] draws, for the same reason.
1243 ///
1244 /// A splice that yielded an overlap arbitration is left out of both halves of
1245 /// the question, because the arbitration answered it first. Its own insertion is
1246 /// buried, so it renders nowhere at all and cannot render at a deletion site;
1247 /// and its removals do not bury, so they leave nobody's context dead. Reporting
1248 /// either would name an event the render did not produce.
1249 fn stranded(
1250 ops: &[(OpId, &Op)],
1251 files: &BTreeMap<OpId, FileInfo>,
1252 yielded: &BTreeSet<OpId>,
1253 cause: &Causality<'_>,
1254 )
1255 -> Outcome<Vec<Flag>>
1256 {
1257 // Every run a splice removed, with the splice that removed it. Only a
1258 // splice kills bytes: a move relocates them, and an anchor follows.
1259 let mut removed: BTreeMap<OpId, Vec<(Range<u64>, OpId)>> = BTreeMap::new();
1260 for (id, op) in ops {
1261 if yielded.contains(id) {
1262 continue;
1263 }
1264 if let Op::Splice { remove, .. } = op {
1265 for r in remove {
1266 if !r.is_empty() {
1267 removed.entry(r.op()).or_default().push((r.offsets(), *id));
1268 }
1269 }
1270 }
1271 }
1272 let mut out: Vec<Flag> = Vec::new();
1273 for (id, op) in ops {
1274 if yielded.contains(id) {
1275 continue;
1276 }
1277 let (left, right) = match op {
1278 Op::Splice { left, right, insert, .. } if !insert.is_empty()
1279 => (left, right),
1280 _ => continue,
1281 };
1282 // The neighbours the insertion was written beside. An absent origin
1283 // is the edge of the file, and a file's origin anchor is the same
1284 // edge spelled as content; neither is context that can die.
1285 let mut ctx: Vec<ContentId> = Vec::new();
1286 for a in [left, right].into_iter().flatten() {
1287 if !files.contains_key(&a.content.op) {
1288 ctx.push(a.content);
1289 }
1290 }
1291 if ctx.is_empty() {
1292 continue;
1293 }
1294 let mut deleters: Vec<OpId> = Vec::new();
1295 let mut all_dead = true;
1296 for c in &ctx {
1297 let mut dead_here = false;
1298 if let Some(runs) = removed.get(&c.op) {
1299 for (span, by) in runs {
1300 if span.contains(&c.off) {
1301 dead_here = true;
1302 if by != id && cause.concurrent(by, id) {
1303 deleters.push(*by);
1304 }
1305 }
1306 }
1307 }
1308 if !dead_here {
1309 all_dead = false;
1310 break;
1311 }
1312 }
1313 if !all_dead {
1314 continue;
1315 }
1316 deleters.sort();
1317 deleters.dedup();
1318 for by in deleters {
1319 out.push(Flag::Stranded { op: *id, by });
1320 }
1321 }
1322 Ok(out)
1323 }
1324
1325 /// Finds the splices that placed content in a file a **concurrent**
1326 /// [`Op::FileDelete`] retired.
1327 ///
1328 /// Only the race is flagged. Every edit ever made goes dark when its file is
1329 /// deliberately deleted, and a deleter who could see the edit chose to delete
1330 /// it, exactly as an editor who could see the deletion chose to write into a
1331 /// dead file; neither is owed a flag, and the parents say which happened.
1332 /// This is narrower than [`Flag::MovedIntoDeleted`], which fires however move
1333 /// and deletion were ordered, because a move actively relocates content into
1334 /// the dead file rather than being overtaken in place.
1335 fn spliced_into_deleted(
1336 ops: &[(OpId, &Op)],
1337 files: &BTreeMap<OpId, FileInfo>,
1338 index: &BTreeMap<OpId, OpId>,
1339 cause: &Causality<'_>,
1340 )
1341 -> Outcome<Vec<Flag>>
1342 {
1343 // The deletions of each file. More than one is legal, and each is its
1344 // own author's decision, judged on its own parents.
1345 let mut dels: BTreeMap<OpId, Vec<OpId>> = BTreeMap::new();
1346 for (id, op) in ops {
1347 if let Op::FileDelete { file } = op {
1348 dels.entry(*file).or_default().push(*id);
1349 }
1350 }
1351 let mut out: Vec<Flag> = Vec::new();
1352 for (id, op) in ops {
1353 // A splice that removes without inserting places nothing, and loses
1354 // nothing to the deletion either: the file's death does strictly more
1355 // than the removal asked for.
1356 match op {
1357 Op::Splice { insert, .. } if !insert.is_empty() => (),
1358 _ => continue,
1359 }
1360 let f = match index.get(id) {
1361 Some(f) => *f,
1362 None => continue,
1363 };
1364 if files.get(&f).map(|i| i.live).unwrap_or(true) {
1365 continue;
1366 }
1367 for del in dels.get(&f).map(|v| v.as_slice()).unwrap_or(&[]) {
1368 if cause.concurrent(del, id) {
1369 out.push(Flag::SplicedIntoDeleted { op: *id, file: f, del: *del });
1370 }
1371 }
1372 }
1373 Ok(out)
1374 }
1375
1376 /// The conservation check proper, over the whole repository.
1377 fn conserved(repo: &Repo, atoms: &Atoms, dead: &Dead)
1378 -> Outcome<()>
1379 {
1380 let mut seen: BTreeMap<OpId, IntervalMap<()>> = BTreeMap::new();
1381 let mut emitted = 0u64;
1382 for file in repo.files() {
1383 for Run { content, .. } in file.runs() {
1384 if content.is_empty() {
1385 continue;
1386 }
1387 emitted += content.len();
1388 res!(seen.entry(content.op()).or_default().insert(content.offsets(), ()));
1389 }
1390 }
1391 let distinct: u64 = seen.values()
1392 .flat_map(|m| m.iter())
1393 .map(|(iv, _)| iv.end - iv.start)
1394 .sum();
1395 if distinct != emitted {
1396 return Err(err!(
1397 "Conservation failed: {} bytes were rendered across the repository \
1398 but only {} of them are distinct, so a byte was shown in two places.",
1399 emitted, distinct;
1400 Bug, Conflict));
1401 }
1402 let buried = dead.within(atoms);
1403 let orphaned = repo.stats().orphaned;
1404 if distinct + buried + orphaned != atoms.total() {
1405 return Err(err!(
1406 "Conservation failed: {} bytes rendered plus {} dead plus {} orphaned \
1407 against {} created.", distinct, buried, orphaned, atoms.total();
1408 Bug, Mismatch));
1409 }
1410 Ok(())
1411 }
1412}
1413
1414
1415/// What the overlap arbitration decided about one splice.
1416#[derive(Clone, Debug, Eq, PartialEq)]
1417struct Yield {
1418 to: OpId, // the op-order maximum, which prevailed
1419 group: Vec<OpId>, // ascending, so that the last of it is the winner
1420 through: Option<OpId>, // the buried insertion this splice sits inside
1421}
1422
1423
1424/// What the overlap arbitration decided about a whole operation set.
1425#[derive(Clone, Debug, Default)]
1426struct Yields {
1427 map: BTreeMap<OpId, Yield>, // every yielding splice, and what it yielded to
1428 buried: BTreeSet<OpId>, // the same, as the tombstones want it
1429}
1430
1431
1432/// The representative of a disjoint set, with the path halved on the way.
1433fn find(up: &mut [usize], i: usize) -> usize {
1434 let mut r = i;
1435 while up[r] != r {
1436 r = up[r];
1437 }
1438 let mut c = i;
1439 while up[c] != c {
1440 let n = up[c];
1441 up[c] = r;
1442 c = n;
1443 }
1444 r
1445}
1446
1447fn join(up: &mut [usize], a: usize, b: usize) {
1448 let (ra, rb) = (find(up, a), find(up, b));
1449 if ra != rb {
1450 up[ra] = rb;
1451 }
1452}
1453
1454
1455/// One trial layout of the repository, kept only to be asked where things are.
1456struct Layout {
1457 slots: Slots,
1458 claims: Claims, // the register they were laid out against
1459 owner: Vec<Option<OpId>>, // the file each slot ended up in
1460}
1461
1462impl Layout {
1463 /// The file a byte sits in, or `None` where no slot in this layout shows it.
1464 fn file_of(&self, cid: &ContentId) -> Option<OpId> {
1465 let i = match self.slots.owner_slot(cid, &self.claims, false) {
1466 Ok(i) => i,
1467 Err(_) => return None,
1468 };
1469 self.owner.get(i).copied().flatten()
1470 }
1471}
1472
1473
1474/// What arbitrating one cycle decided.
1475struct Decision {
1476 winner: Option<OpId>, // where the arbitration names one
1477 losers: Vec<(OpId, OpId, OpId)>, // each with its home and denied files
1478}
1479
1480
1481/// What deciding a cycle takes: the operation set, the two structures a trial
1482/// layout needs, where every byte was written, and what each author had seen.
1483struct Arbiter<'a> {
1484 ops: &'a [(OpId, &'a Op)], // in op order
1485 dead: &'a Dead, // which a trial layout needs
1486 atoms: &'a Atoms, // likewise
1487 birth: &'a BTreeMap<OpId, OpId>, // the file every atom was written into
1488 cause: &'a Causality<'a>, // what tells a race from a sequence
1489}
1490
1491impl Arbiter<'_> {
1492
1493 /// Decides what to do with one cycle, or nothing.
1494 ///
1495 /// Returns `None` where the rule declines, in which case the caller falls back
1496 /// to demotion: that is what happens to a cycle inside one file, to a cycle
1497 /// with no move in it to void, and to a cycle every one of whose members is
1498 /// informed.
1499 ///
1500 /// **The rule.** A cycle in the anchor graph that crosses a file boundary is
1501 /// arbitrated as one concurrent group: the member highest in op order completes
1502 /// wholly, and every other member is voided back to its source and flagged. The
1503 /// design has already argued for this once, over the overlapping range move --
1504 /// one of your two moves happened and you were told which is easier to explain,
1505 /// and to undo, than half a block at each end of the repository.
1506 fn judge(
1507 &self,
1508 cycle: &[usize],
1509 slots: &Slots,
1510 voided: &BTreeSet<OpId>,
1511 )
1512 -> Outcome<Option<Decision>>
1513 {
1514 // The moves the cycle runs through, in op order. A splice in a cycle is not
1515 // voidable, since voiding an insertion would destroy content.
1516 let mut members: Vec<OpId> = Vec::new();
1517 for k in cycle {
1518 let place = res!(slots.get(*k)).place;
1519 if voided.contains(&place) || members.contains(&place) {
1520 continue;
1521 }
1522 if self.op(&place).map(|o| o.is_move()).unwrap_or(false) {
1523 members.push(place);
1524 }
1525 }
1526 members.sort_by_key(OpOrder::of);
1527 if members.is_empty() {
1528 return Ok(None);
1529 }
1530
1531 // A member crosses a boundary when its content and its destination are in
1532 // different files, and that question is asked twice, of two repositories,
1533 // because neither answer alone is sound.
1534 //
1535 // **Where the bytes would be if this cycle had not happened.** One layout
1536 // with the cycle's members voided and nothing else. Asking which file a
1537 // member's content is in is circular while the cycle is unbroken -- the file
1538 // is read off the tree, and the tree is what the cycle is blocking -- and
1539 // voiding every member removes every edge of the cycle, so it lays out.
1540 //
1541 // **Where the bytes were written.** The birth layout, which is the only
1542 // reading that sees a cycle whose members supersede an earlier move of their
1543 // own: voiding such a cycle resurrects the superseded move, which carries
1544 // the content over the boundary itself, and both readings of the cycle then
1545 // come out inside one file when it is two.
1546 //
1547 // The rule is the union. Both failures are false negatives -- each reading
1548 // misses cycles, neither invents them -- and a false negative is a collapse
1549 // while a false positive is a voided move, which is flagged and reversible.
1550 let mut without: BTreeSet<OpId> = voided.clone();
1551 for m in &members {
1552 without.insert(*m);
1553 }
1554 let site = res!(Sequence::layout(self.ops, self.dead, self.atoms, &without));
1555
1556 let mut home: BTreeMap<OpId, OpId> = BTreeMap::new();
1557 let mut dest: BTreeMap<OpId, OpId> = BTreeMap::new();
1558 let mut cross: Vec<OpId> = Vec::new();
1559 for m in &members {
1560 let op = match self.op(m) {
1561 Some(o) => o,
1562 None => continue,
1563 };
1564 let (left, right) = op.origins();
1565 let anchor = match left.or(right) {
1566 Some(a) => a.content,
1567 None => continue,
1568 };
1569 let born_to = self.birth.get(&anchor.op).copied();
1570 let now_to = site.file_of(&anchor);
1571 let mut h: Option<OpId> = None;
1572 let mut differs = false;
1573 for r in op.regions() {
1574 if r.is_empty() {
1575 continue;
1576 }
1577 let first = ContentId::new(r.op(), r.from());
1578 let born_from = self.birth.get(&r.op()).copied();
1579 let now_from = site.file_of(&first);
1580 if h.is_none() {
1581 h = now_from.or(born_from);
1582 }
1583 if born_from.is_some() && born_from != born_to {
1584 differs = true;
1585 }
1586 if now_from.is_some() && now_to.is_some() && now_from != now_to {
1587 differs = true;
1588 }
1589 }
1590 let h = match h {
1591 Some(f) => f,
1592 None => continue,
1593 };
1594 let d = match now_to.or(born_to) {
1595 Some(f) => f,
1596 None => continue,
1597 };
1598 home.insert(*m, h);
1599 dest.insert(*m, d);
1600 if differs {
1601 cross.push(*m);
1602 }
1603 }
1604 if cross.is_empty() {
1605 return Ok(None);
1606 }
1607
1608 // A member that saw another member is informed rather than racing, and an
1609 // informed move is not voided for a race it did not have. This is the
1610 // distinction the torn flag had to learn: the claim register cannot tell a
1611 // race from a sequence, and the parents can.
1612 //
1613 // The exemption is for the causally *last* informed member only. Exempting
1614 // every member with another in its past is defeated by a chain of moves:
1615 // where a replica has made three in a row, each informed by the one before,
1616 // every member but the first is exempt, and where the first is not the one
1617 // that has to go the rule declines and the collapse happens anyway. A member
1618 // superseded by a later member of the same cycle is owed nothing, its own
1619 // author having moved on.
1620 let informed = |m: &OpId| {
1621 members.iter().any(|n| n != m && self.cause.reaches(m, n))
1622 && !members.iter().any(|n| n != m && self.cause.reaches(n, m))
1623 };
1624
1625 // Where the cycle runs through only one move -- its other members being
1626 // splices, or the move anchoring inside its own source -- there is nothing
1627 // to arbitrate between, and a winner-takes-all rule that keeps its only
1628 // member would break no cycle at all. That move is confined instead, and the
1629 // cycle keeps no winner.
1630 let highest = members.iter().copied().max_by_key(OpOrder::of);
1631 let (winner, chosen): (Option<OpId>, Vec<OpId>) = if members.len() == 1 {
1632 (None, cross.iter().copied().filter(|m| !informed(m)).collect())
1633 } else {
1634 (highest, members.iter()
1635 .copied()
1636 .filter(|m| Some(*m) != highest && !informed(m))
1637 .collect())
1638 };
1639 let mut losers: Vec<(OpId, OpId, OpId)> = Vec::new();
1640 for m in chosen {
1641 let h = match home.get(&m) {
1642 Some(f) => *f,
1643 None => continue,
1644 };
1645 let d = dest.get(&m).copied().unwrap_or(h);
1646 losers.push((m, h, d));
1647 }
1648 if losers.is_empty() {
1649 return Ok(None);
1650 }
1651 Ok(Some(Decision { winner, losers }))
1652 }
1653
1654 fn op(&self, id: &OpId) -> Option<&Op> {
1655 self.ops.iter().find(|(i, _)| i == id).map(|(_, op)| *op)
1656 }
1657}