feat(stitched): Implement test harness using tests from codeberg

This commit is contained in:
Ginger 2025-12-23 23:40:56 -05:00
parent a1ad9f0144
commit b242bad6d2
No known key found for this signature in database
33 changed files with 993 additions and 32 deletions

28
Cargo.lock generated
View file

@ -1215,6 +1215,7 @@ dependencies = [
"log",
"loole",
"lru-cache",
"peg",
"rand 0.8.5",
"recaptcha-verify",
"regex",
@ -3658,6 +3659,33 @@ dependencies = [
"syn",
]
[[package]]
name = "peg"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477"
dependencies = [
"peg-macros",
"peg-runtime",
]
[[package]]
name = "peg-macros"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71"
dependencies = [
"peg-runtime",
"proc-macro2",
"quote",
]
[[package]]
name = "peg-runtime"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca"
[[package]]
name = "percent-encoding"
version = "2.3.2"

View file

@ -125,3 +125,6 @@ sd-notify.optional = true
[lints]
workspace = true
[dev-dependencies]
peg = "0.8.5"

View file

@ -4,11 +4,11 @@ use std::{
};
use itertools::Itertools;
use ruma::{EventId, OwnedEventId};
use super::{Batch, Gap, OrderKey, StitchedItem, StitcherBackend};
/// Updates to a gap in the stitched order.
#[derive(Debug)]
pub(super) struct GapUpdate<'id, K: OrderKey> {
/// The opaque key of the gap to update.
pub key: K,
@ -21,6 +21,7 @@ pub(super) struct GapUpdate<'id, K: OrderKey> {
}
/// Updates to the stitched order.
#[derive(Debug)]
pub(super) struct OrderUpdates<'id, K: OrderKey> {
/// Updates to individual gaps. The items inserted by these updates _should
/// not_ be synchronized to clients.
@ -35,14 +36,12 @@ pub(super) struct Stitcher<'backend, B: StitcherBackend> {
}
impl<B: StitcherBackend> Stitcher<'_, B> {
pub(super) fn new<'backend>(backend: &'backend B) -> Stitcher<'backend, B> {
Stitcher { backend }
}
pub(super) fn new(backend: &B) -> Stitcher<'_, B> { Stitcher { backend } }
pub(super) fn stitch<'id>(&self, batch: Batch<'id>) -> OrderUpdates<'id, B::Key> {
let mut gap_updates = Vec::new();
let mut remaining_events: BTreeSet<&EventId> = batch.events().collect();
let mut remaining_events: BTreeSet<_> = batch.events().collect();
// 1: Find existing gaps which include IDs of events in `batch`
let matching_gaps = self.backend.find_matching_gaps(batch.events());
@ -53,7 +52,7 @@ impl<B: StitcherBackend> Stitcher<'_, B> {
let matching_events = remaining_events.iter().filter(|id| gap.contains(**id));
// 3. Create the to-insert list from the predecessor sets of each matching event
let events_to_insert: Vec<&'id EventId> = matching_events
let events_to_insert: Vec<_> = matching_events
.filter_map(|event| batch.predecessors(event))
.flat_map(|predecessors| predecessors.predecessor_set.iter())
.filter(|event| remaining_events.contains(*event))
@ -84,7 +83,7 @@ impl<B: StitcherBackend> Stitcher<'_, B> {
fn sort_events_and_create_gaps<'id>(
&self,
batch: &Batch<'id>,
events_to_insert: impl IntoIterator<Item = &'id EventId>,
events_to_insert: impl IntoIterator<Item = &'id str>,
) -> Vec<StitchedItem<'id>> {
// 5. Sort the to-insert list with DAG;received order
let events_to_insert = events_to_insert
@ -96,8 +95,8 @@ impl<B: StitcherBackend> Stitcher<'_, B> {
events_to_insert.capacity() + events_to_insert.capacity().div_euclid(2),
);
for event in events_to_insert.into_iter() {
let missing_prev_events: HashSet<OwnedEventId> = batch
for event in events_to_insert {
let missing_prev_events: HashSet<String> = batch
.predecessors(event)
.expect("events in to_insert should be in batch")
.prev_events
@ -105,14 +104,14 @@ impl<B: StitcherBackend> Stitcher<'_, B> {
.filter(|prev_event| {
!(batch.contains(prev_event) || self.backend.event_exists(prev_event))
})
.map(|id| OwnedEventId::from(*id))
.map(|id| String::from(*id))
.collect();
if !missing_prev_events.is_empty() {
items.push(StitchedItem::Gap(missing_prev_events));
}
items.push(StitchedItem::Event(event))
items.push(StitchedItem::Event(event));
}
items
@ -124,7 +123,7 @@ impl<B: StitcherBackend> Stitcher<'_, B> {
/// otherwise they are sorted by which comes first in the batch.
fn compare_by_dag_received<'id>(
batch: &Batch<'id>,
) -> impl FnMut(&&'id EventId, &&'id EventId) -> Ordering {
) -> impl FnMut(&&'id str, &&'id str) -> Ordering {
|a, b| {
if batch
.predecessors(a)
@ -145,7 +144,7 @@ impl<B: StitcherBackend> Stitcher<'_, B> {
}
}
panic!("neither {} nor {} in batch", a, b);
panic!("neither {a} nor {b} in batch");
}
}
}

View file

@ -1,14 +1,15 @@
use std::collections::{BTreeMap, HashSet};
use ruma::{EventId, OwnedEventId};
pub(super) mod algorithm;
#[cfg(test)]
mod test;
/// A gap in the stitched order.
pub(super) type Gap = HashSet<OwnedEventId>;
pub(super) type Gap = HashSet<String>;
#[derive(Debug)]
pub(super) enum StitchedItem<'id> {
Event(&'id EventId),
Event(&'id str),
Gap(Gap),
}
@ -16,40 +17,44 @@ pub(super) enum StitchedItem<'id> {
/// order.
pub(super) trait OrderKey: Eq + Clone {}
impl<T: Eq + Clone> OrderKey for T {}
pub(super) trait StitcherBackend {
type Key: OrderKey;
/// Returns all gaps containing an event listed in `events`.
fn find_matching_gaps<'a>(
&'a self,
events: impl Iterator<Item = &'a EventId>,
events: impl Iterator<Item = &'a str>,
) -> impl Iterator<Item = (Self::Key, Gap)>;
fn event_exists<'a>(&'a self, event: &'a EventId) -> bool;
/// Returns whether an event exists in the stitched order.
fn event_exists<'a>(&'a self, event: &'a str) -> bool;
}
/// An ordered map from an event ID to its `prev_events`.
pub(super) type EventEdges<'id> = BTreeMap<&'id EventId, HashSet<&'id EventId>>;
pub(super) type EventEdges<'id> = BTreeMap<&'id str, HashSet<&'id str>>;
/// Information about the `prev_events` of an event.
/// This struct does not store the ID of the event itself.
struct EventPredecessors<'id> {
/// The `prev_events` of the event.
pub prev_events: HashSet<&'id EventId>,
pub prev_events: HashSet<&'id str>,
/// The predecessor set of the event. This is a superset of
/// [`EventPredecessors::prev_events`]. See [`Batch::find_predecessor_set`]
/// for details.
pub predecessor_set: HashSet<&'id EventId>,
pub predecessor_set: HashSet<&'id str>,
}
pub(super) struct Batch<'id> {
events: BTreeMap<&'id EventId, EventPredecessors<'id>>,
events: BTreeMap<&'id str, EventPredecessors<'id>>,
}
impl<'id> Batch<'id> {
pub(super) fn from_edges<'a>(edges: EventEdges<'a>) -> Batch<'a> {
pub(super) fn from_edges(edges: EventEdges<'_>) -> Batch<'_> {
let mut events = BTreeMap::new();
for (event, prev_events) in edges.iter() {
for (event, prev_events) in &edges {
let predecessor_set = Self::find_predecessor_set(event, &edges);
events.insert(*event, EventPredecessors {
@ -66,10 +71,7 @@ impl<'id> Batch<'id> {
/// rooted at `event` containing _only_ events which are included in
/// `edges`. It is represented as a set and not a proper tree structure for
/// efficiency.
fn find_predecessor_set<'a>(
event: &'a EventId,
edges: &EventEdges<'a>,
) -> HashSet<&'a EventId> {
fn find_predecessor_set<'a>(event: &'a str, edges: &EventEdges<'a>) -> HashSet<&'a str> {
// The predecessor set which we are building.
let mut predecessor_set = HashSet::new();
@ -100,11 +102,11 @@ impl<'id> Batch<'id> {
predecessor_set
}
fn events(&self) -> impl Iterator<Item = &'id EventId> { self.events.keys().copied() }
fn events(&self) -> impl Iterator<Item = &'id str> { self.events.keys().copied() }
fn contains(&self, event: &'id EventId) -> bool { self.events.contains_key(event) }
fn contains(&self, event: &'id str) -> bool { self.events.contains_key(event) }
fn predecessors(&self, event: &EventId) -> Option<&EventPredecessors<'id>> {
fn predecessors(&self, event: &str) -> Option<&EventPredecessors<'id>> {
self.events.get(event)
}
}

View file

@ -0,0 +1,170 @@
use std::sync::atomic::{AtomicU64, Ordering};
use itertools::Itertools;
use super::{algorithm::*, *};
use crate::rooms::timeline::stitcher::algorithm::Stitcher;
mod parser;
#[derive(Default)]
struct TestStitcherBackend<'id> {
items: Vec<(u64, StitchedItem<'id>)>,
counter: AtomicU64,
}
impl<'id> TestStitcherBackend<'id> {
fn next_id(&self) -> u64 { self.counter.fetch_add(1, Ordering::Relaxed) }
fn extend(&mut self, results: OrderUpdates<'id, <Self as StitcherBackend>::Key>) {
for update in results.gap_updates {
let Some(gap_index) = self.items.iter().position(|(key, _)| *key == update.key)
else {
panic!("bad update key {}", update.key);
};
let insertion_index;
if update.gap.is_empty() {
self.items.remove(gap_index);
insertion_index = gap_index;
} else {
self.items[gap_index] = (self.next_id(), StitchedItem::Gap(update.gap));
insertion_index = gap_index + 1;
}
let to_insert: Vec<_> = update
.inserted_items
.into_iter()
.map(|item| (self.next_id(), item))
.collect();
self.items
.splice(insertion_index..insertion_index, to_insert.into_iter())
.for_each(drop);
}
let new_items: Vec<_> = results
.new_items
.into_iter()
.map(|item| (self.next_id(), item))
.collect();
self.items.extend(new_items);
}
fn iter(&self) -> impl Iterator<Item = &StitchedItem<'id>> {
self.items.iter().map(|(_, item)| item)
}
}
impl StitcherBackend for TestStitcherBackend<'_> {
type Key = u64;
fn find_matching_gaps<'a>(
&'a self,
events: impl Iterator<Item = &'a str>,
) -> impl Iterator<Item = (Self::Key, Gap)> {
// nobody cares about test suite performance right
let mut gaps = vec![];
for event in events {
for (key, item) in &self.items {
if let StitchedItem::Gap(gap) = item
&& gap.contains(event)
{
gaps.push((*key, gap.clone()));
}
}
}
gaps.into_iter()
}
fn event_exists<'a>(&'a self, event: &'a str) -> bool {
self.items
.iter()
.any(|item| matches!(item.1, StitchedItem::Event(id) if event == id))
}
}
fn run_testcase(testcase: parser::TestCase<'_>) {
let mut backend = TestStitcherBackend::default();
for phase in testcase {
let stitcher = Stitcher::new(&backend);
let batch = Batch::from_edges(phase.batch);
let updates = stitcher.stitch(batch);
println!("updates to make: {:?}", updates);
for (expected, actual) in phase
.order
.new_items
.iter()
.zip_eq(updates.new_items.iter())
{
assert_eq!(
expected, actual,
"bad new item, expected {:?} but got {:?}",
expected, actual
);
}
backend.extend(updates);
println!("ordering: {:?}", backend.items);
for (expected, actual) in phase.order.iter().zip_eq(backend.iter()) {
assert_eq!(
expected, actual,
"bad item in order, expected {:?} but got {:?}",
expected, actual
);
}
// TODO gap notification
}
}
macro_rules! testcase {
($index:literal : $id:ident) => {
#[test]
fn $id() {
let testcase = parser::parse(include_str!(concat!(
"./testcases/",
$index,
"-",
stringify!($id),
".stitched"
)));
run_testcase(testcase);
}
};
}
testcase!("001": receiving_new_events);
testcase!("002": recovering_after_netsplit);
testcase!("zzz": being_before_a_gap_item_beats_being_after_an_existing_item_multiple);
testcase!("zzz": being_before_a_gap_item_beats_being_after_an_existing_item);
testcase!("zzz": chains_are_reordered_using_prev_events);
testcase!("zzz": empty_then_simple_chain);
testcase!("zzz": empty_then_two_chains_interleaved);
testcase!("zzz": empty_then_two_chains);
testcase!("zzz": filling_in_a_gap_with_a_batch_containing_gaps);
testcase!("zzz": gaps_appear_before_events_referring_to_them_received_order);
testcase!("zzz": gaps_appear_before_events_referring_to_them);
testcase!("zzz": if_prev_events_determine_order_they_override_received);
testcase!("zzz": insert_into_first_of_several_gaps);
testcase!("zzz": insert_into_last_of_several_gaps);
testcase!("zzz": insert_into_middle_of_several_gaps);
testcase!("zzz": linked_events_are_split_across_gaps);
testcase!("zzz": linked_events_in_a_diamond_are_split_across_gaps);
testcase!("zzz": middle_of_batch_matches_gap_and_end_of_batch_matches_end);
testcase!("zzz": middle_of_batch_matches_gap);
testcase!("zzz": multiple_events_referring_to_the_same_missing_event_first_has_more);
testcase!("zzz": multiple_events_referring_to_the_same_missing_event);
testcase!("zzz": multiple_events_referring_to_the_same_missing_event_with_more);
testcase!("zzz": multiple_missing_prev_events_turn_into_a_single_gap);
testcase!("zzz": partially_filling_a_gap_leaves_it_before_new_nodes);
testcase!("zzz": partially_filling_a_gap_with_two_events);
testcase!("zzz": received_order_wins_within_a_subgroup_if_no_prev_event_chain);
testcase!("zzz": subgroups_are_processed_in_first_received_order);

View file

@ -0,0 +1,138 @@
use std::collections::{BTreeMap, HashSet};
use crate::rooms::timeline::stitcher::{StitchedItem, test};
pub(super) type TestEventId<'id> = &'id str;
pub(super) type TestGap<'id> = HashSet<TestEventId<'id>>;
#[derive(Debug)]
pub(super) enum TestStitchedItem<'id> {
Event(TestEventId<'id>),
Gap(TestGap<'id>),
}
impl PartialEq<StitchedItem<'_>> for TestStitchedItem<'_> {
fn eq(&self, other: &StitchedItem<'_>) -> bool {
match (self, other) {
| (TestStitchedItem::Event(lhs), StitchedItem::Event(rhs)) => lhs == rhs,
| (TestStitchedItem::Gap(lhs), StitchedItem::Gap(rhs)) =>
lhs.iter().all(|id| rhs.contains(*id)),
| _ => false,
}
}
}
pub(super) type TestCase<'id> = Vec<Phase<'id>>;
pub(super) struct Phase<'id> {
pub batch: Batch<'id>,
pub order: Order<'id>,
pub updated_gaps: Option<HashSet<TestEventId<'id>>>,
}
pub(super) type Batch<'id> = BTreeMap<TestEventId<'id>, HashSet<TestEventId<'id>>>;
pub(super) struct Order<'id> {
pub inserted_items: Vec<TestStitchedItem<'id>>,
pub new_items: Vec<TestStitchedItem<'id>>,
}
impl<'id> Order<'id> {
pub(super) fn iter(&self) -> impl Iterator<Item = &TestStitchedItem<'id>> {
self.inserted_items.iter().chain(self.new_items.iter())
}
}
peg::parser! {
grammar testcase() for str {
/// Parse whitespace.
rule _ -> () = quiet! { $([' '])* {} }
/// Parse empty lines and comments.
rule newline() -> () = quiet! { (("#" [^'\n']*)? "\n")+ {} }
/// Parse an "event ID" in a test case, which may only consist of ASCII letters and numbers.
rule event_id() -> TestEventId<'input>
= quiet! { id:$([char if char.is_ascii_alphanumeric()]+) { id } }
/ expected!("event id")
/// Parse a gap in the order section.
rule gap() -> TestGap<'input>
= "-" events:event_id() ++ "," { events.into_iter().collect() }
/// Parse either an event id or a gap.
rule stitched_item() -> TestStitchedItem<'input> =
id:event_id() { TestStitchedItem::Event(id) }
/ gap:gap() { TestStitchedItem::Gap(gap) }
/// Parse an event line in the batch section, mapping an event name to zero or one prev events.
/// The prev events are merged together by [`batch()`].
rule batch_event() -> (TestEventId<'input>, Option<TestEventId<'input>>)
= id:event_id() prev:(_ "-->" _ prev:event_id() { prev })? { (id, prev) }
/// Parse the batch section of a phase.
rule batch() -> Batch<'input>
= events:batch_event() ++ newline() {
/*
Repeated event lines need to be merged together. For example,
A --> B
A --> C
represents a _single_ event `A` with two prev events, `B` and `C`.
*/
events.into_iter()
.fold(BTreeMap::new(), |mut batch: Batch<'_>, (id, prev_event)| {
// Find the prev events set of this event in the batch.
// If it doesn't exist, make a new empty one.
let mut prev_events = batch.entry(id).or_default();
// If this event line defines a prev event to add, insert it into the set.
if let Some(prev_event) = prev_event {
prev_events.insert(prev_event);
}
batch
})
}
rule order() -> Order<'input> =
items:(item:stitched_item() new:"*"? { (item, new.is_some()) }) ** newline()
{
let (mut inserted_items, mut new_items) = (vec![], vec![]);
for (item, new) in items {
if new {
new_items.push(item);
} else {
inserted_items.push(item);
}
}
Order {
inserted_items,
new_items,
}
}
rule updated_gaps() -> HashSet<TestEventId<'input>> =
events:event_id() ++ newline() { events.into_iter().collect() }
rule phase() -> Phase<'input> =
"=== when we receive these events ==="
newline() batch:batch()
newline() "=== then we arrange into this order ==="
newline() order:order()
updated_gaps:(
newline() "=== and we notify about these gaps ==="
newline() updated_gaps:updated_gaps() { updated_gaps }
)?
{ Phase { batch, order, updated_gaps } }
pub rule testcase() -> TestCase<'input> = phase() ++ newline()
}
}
pub(super) fn parse<'input>(input: &'input str) -> TestCase<'input> {
testcase::testcase(input.trim_ascii_end()).expect("parse error")
}

View file

@ -0,0 +1,22 @@
=== when we receive these events ===
A
B --> A
C --> B
=== then we arrange into this order ===
# Given the server has some existing events in this order:
A*
B*
C*
=== when we receive these events ===
# When it receives new ones:
D --> C
E --> D
=== then we arrange into this order ===
# Then it simply appends them at the end of the order:
A
B
C
D*
E*

View file

@ -0,0 +1,46 @@
=== when we receive these events ===
A1
A2 --> A1
A3 --> A2
=== then we arrange into this order ===
# Given the server has some existing events in this order:
A1*
A2*
A3*
=== when we receive these events ===
# And after a netsplit the server receives some unrelated events, which refer to
# some unknown event, because the server didn't receive all of them:
B7 --> B6
B8 --> B7
B9 --> B8
=== then we arrange into this order ===
# Then these events are new, and we add a gap to show something is missing:
A1
A2
A3
-B6*
B7*
B8*
B9*
=== when we receive these events ===
# Then if we backfill and receive more of those events later:
B4 --> B3
B5 --> B4
B6 --> B5
=== then we arrange into this order ===
# They are slotted into the gap, and a new gap is created to represent the
# still-missing events:
A1
A2
A3
-B3
B4
B5
B6
B7
B8
B9
=== and we notify about these gaps ===
B6

View file

@ -0,0 +1,30 @@
=== when we receive these events ===
D --> C
=== then we arrange into this order ===
# We may see situations that are ambiguous about whether an event is new or
# belongs in a gap, because it is a predecessor of a gap event and also has a
# new event as its predecessor. This a rare case where either outcome could be
# valid. If the initial order is this:
-C*
D*
=== when we receive these events ===
# And then we receive B
B --> A
=== then we arrange into this order ===
# Which is new because it's unrelated to everything else
-C
D
-A*
B*
=== when we receive these events ===
# And later it turns out that C refers back to B
C --> B
=== then we arrange into this order ===
# Then we place C into the early gap even though it is after B, so arguably
# should be the newest
C
D
-A
B
=== and we notify about these gaps ===
C

View file

@ -0,0 +1,28 @@
=== when we receive these events ===
# An ambiguous situation can occur when we have multiple gaps that both might
# accepts an event. This should be relatively rare.
A --> G1
B --> A
C --> G2
=== then we arrange into this order ===
-G1*
A
B
-G2*
C
=== when we receive these events ===
# When we receive F, which is a predecessor of both G1 and G2
F
G1 --> F
G2 --> F
=== then we arrange into this order ===
# Then F appears in the earlier gap, but arguably it should appear later.
F
G1
A
B
G2
C
=== and we notify about these gaps ===
G1
G2

View file

@ -0,0 +1,10 @@
=== when we receive these events ===
# Even though we see C first, it is re-ordered because we must obey prev_events
# so A comes first.
C --> A
A
B --> A
=== then we arrange into this order ===
A*
C*
B*

View file

@ -0,0 +1,8 @@
=== when we receive these events ===
A
B --> A
C --> B
=== then we arrange into this order ===
A*
B*
C*

View file

@ -0,0 +1,18 @@
=== when we receive these events ===
# A chain ABC
A
B --> A
C --> B
# And a separate chain XYZ
X --> W
Y --> X
Z --> Y
=== then we arrange into this order ===
# Should produce them in order with a gap
A*
B*
C*
-W*
X*
Y*
Z*

View file

@ -0,0 +1,18 @@
=== when we receive these events ===
# Same as empty_then_two_chains except for received order
# A chain ABC, and a separate chain XYZ, but interleaved
A
X --> W
B --> A
Y --> X
C --> B
Z --> Y
=== then we arrange into this order ===
# Should produce them in order with a gap
A*
-W*
X*
B*
Y*
C*
Z*

View file

@ -0,0 +1,33 @@
=== when we receive these events ===
# Given 3 gaps exist
B --> A
D --> C
F --> E
=== then we arrange into this order ===
-A*
B*
-C*
D*
-E*
F*
=== when we receive these events ===
# When we fill one with something that also refers to non-existent events
C --> X
C --> Y
G --> C
G --> Z
=== then we arrange into this order ===
# Then we fill in the gap (C) and make new gaps too (X+Y and Z)
-A
B
-X,Y
C
D
-E
F
-Z*
G*
=== and we notify about these gaps ===
# And we notify about the gap that was updated
C

View file

@ -0,0 +1,13 @@
=== when we receive these events ===
# Several events refer to missing events and the events are unrelated
C --> Y
C --> Z
A --> X
B
=== then we arrange into this order ===
# The gaps appear immediately before the events referring to them
-Y,Z*
C*
-X*
A*
B*

View file

@ -0,0 +1,14 @@
=== when we receive these events ===
# Several events refer to missing events and the events are related
C --> Y
C --> Z
C --> B
A --> X
B --> A
=== then we arrange into this order ===
# The gaps appear immediately before the events referring to them
-X*
A*
B*
-Y,Z*
C*

View file

@ -0,0 +1,15 @@
=== when we receive these events ===
# The relationships determine the order here, so they override received order
F --> E
C --> B
D --> C
E --> D
B --> A
A
=== then we arrange into this order ===
A*
B*
C*
D*
E*
F*

View file

@ -0,0 +1,27 @@
=== when we receive these events ===
# Given 3 gaps exist
B --> A
D --> C
F --> E
=== then we arrange into this order ===
-A*
B*
-C*
D*
-E*
F*
=== when we receive these events ===
# When the first of them is filled in
A
=== then we arrange into this order ===
# Then we slot it into the gap, not at the end
A
B
-C
D
-E
F
=== and we notify about these gaps ===
# And we notify about the gap being filled in
A

View file

@ -0,0 +1,27 @@
=== when we receive these events ===
# Given 3 gaps exist
B --> A
D --> C
F --> E
=== then we arrange into this order ===
-A*
B*
-C*
D*
-E*
F*
=== when we receive these events ===
# When the last gap is filled in
E
=== then we arrange into this order ===
# Then we slot it into the gap, not at the end
-A
B
-C
D
E
F
=== and we notify about these gaps ===
# And we notify about the gap being filled in
E

View file

@ -0,0 +1,27 @@
=== when we receive these events ===
# Given 3 gaps exist
B --> A
D --> C
F --> E
=== then we arrange into this order ===
-A*
B*
-C*
D*
-E*
F*
=== when we receive these events ===
# When a middle one is filled in
C
=== then we arrange into this order ===
# Then we slot it into the gap, not at the end
-A
B
C
D
-E
F
=== and we notify about these gaps ===
# And we notify about the gap being filled in
C

View file

@ -0,0 +1,29 @@
=== when we receive these events ===
# Given a couple of gaps
B --> X2
D --> X4
=== then we arrange into this order ===
-X2*
B*
-X4*
D*
=== when we receive these events ===
# And linked events that fill those in and are newer
X1
X2 --> X1
X3 --> X2
X4 --> X3
X5 --> X4
=== then we arrange into this order ===
# Then the gaps are filled and new events appear at the front
X1
X2
B
X3
X4
D
X5*
=== and we notify about these gaps ===
X2
X4

View file

@ -0,0 +1,31 @@
=== when we receive these events ===
# Given a couple of gaps
B --> X2a
D --> X3
=== then we arrange into this order ===
-X2a*
B*
-X3*
D*
=== when we receive these events ===
# When we receive a diamond that touches gaps and some new events
X1
X2a --> X1
X2b --> X1
X3 --> X2a
X3 --> X2b
X4 --> X3
=== then we arrange into this order ===
# Then matching events and direct predecessors fit into the gaps
# and other stuff is new
X1
X2a
B
X2b
X3
D
X4*
=== and we notify about these gaps ===
X2a
X3

View file

@ -0,0 +1,25 @@
=== when we receive these events ===
# Given a gap before all the Bs
B1 --> C2
B2 --> B1
=== then we arrange into this order ===
-C2*
B1*
B2*
=== when we receive these events ===
# When a batch arrives with a not-last event matching the gap
C1
C2 --> C1
C3 --> C2
=== then we arrange into this order ===
# Then we slot the matching events into the gap
# and the later events are new
C1
C2
B1
B2
C3*
=== and we notify about these gaps ===
# And we notify about the gap being filled in
C2

View file

@ -0,0 +1,26 @@
=== when we receive these events ===
# Given a gap before all the Bs
B1 --> C2
B2 --> B1
=== then we arrange into this order ===
-C2*
B1*
B2*
=== when we receive these events ===
# When a batch arrives with a not-last event matching the gap, and the last
# event linked to a recent event
C1
C2 --> C1
C3 --> C2
C3 --> B2
=== then we arrange into this order ===
# Then we slot the entire batch into the gap
C1
C2
B1
B2
C3*
=== and we notify about these gaps ===
# And we notify about the gap being filled in
C2

View file

@ -0,0 +1,26 @@
=== when we receive these events ===
# If multiple events all refer to the same missing event:
A --> X
B --> X
C --> X
=== then we arrange into this order ===
# Then we insert gaps before all of them. This avoids the need to search the
# entire existing order whenever we create a new gap.
-X*
A*
-X*
B*
-X*
C*
=== when we receive these events ===
# The ambiguity is resolved when the missing event arrives:
X
=== then we arrange into this order ===
# We choose the earliest gap, and all the relevant gaps are removed (which does
# mean we need to search the existing order).
X
A
B
C
=== and we notify about these gaps ===
X

View file

@ -0,0 +1,29 @@
=== when we receive these events ===
# Several events refer to the same missing event, but the first refers to
# others too
A --> X
A --> Y
A --> Z
B --> X
C --> X
=== then we arrange into this order ===
# We end up with multiple gaps
-X,Y,Z*
A*
-X*
B*
-X*
C*
=== when we receive these events ===
# When we receive the missing item
X
=== then we arrange into this order ===
# It goes into the earliest slot, and the non-empty gap remains
-Y,Z
X
A
B
C
=== and we notify about these gaps ===
X

View file

@ -0,0 +1,28 @@
=== when we receive these events ===
# Several events refer to the same missing event, but one refers to others too
A --> X
B --> X
B --> Y
B --> Z
C --> X
=== then we arrange into this order ===
# We end up with multiple gaps
-X*
A*
-X,Y,Z*
B*
-X*
C*
=== when we receive these events ===
# When we receive the missing item
X
=== then we arrange into this order ===
# It goes into the earliest slot, and the non-empty gap remains
X
A
-Y,Z
B
C
=== and we notify about these gaps ===
X

View file

@ -0,0 +1,9 @@
=== when we receive these events ===
# A refers to multiple missing things
A --> X
A --> Y
A --> Z
=== then we arrange into this order ===
# But we only make one gap, with multiple IDs in it
-X,Y,Z*
A*

View file

@ -0,0 +1,23 @@
=== when we receive these events ===
A
F --> B
F --> C
F --> D
F --> E
=== then we arrange into this order ===
# Given a gap that lists several nodes:
A*
-B,C,D,E*
F*
=== when we receive these events ===
# When we provide one of the missing events:
C
=== then we arrange into this order ===
# Then it is inserted after the gap, and the gap is shrunk:
A
-B,D,E
C
F
=== and we notify about these gaps ===
# And we notify about the gap that was updated
C

View file

@ -0,0 +1,27 @@
=== when we receive these events ===
# Given an event references multiple missing events
A
F --> B
F --> C
F --> D
F --> E
=== then we arrange into this order ===
A*
-B,C,D,E*
F
=== when we receive these events ===
# When we provide some of the missing events
C
E
=== then we arrange into this order ===
# Then we insert them after the gap and shrink the list of events in the gap
A
-B,D
C
E
F
=== and we notify about these gaps ===
# And we notify about the gap that was updated
C
E

View file

@ -0,0 +1,16 @@
=== when we receive these events ===
# Everything is after A, but there is no prev_event chain between the others, so
# we use received order.
A
F --> A
C --> A
D --> A
E --> A
B --> A
=== then we arrange into this order ===
A*
F*
C*
D*
E*
B*

View file

@ -0,0 +1,16 @@
=== when we receive these events ===
# We preserve the received order where it does not conflict with the prev_events
A
X --> W
Y --> X
Z --> Y
B --> A
C --> B
=== then we arrange into this order ===
A*
-W*
X*
Y*
Z*
B*
C*