Skip to content

Commit 04c1dbf

Browse files
committed
Add the session operations for taking back, dropping and restoring interactions and for following a retirer
1 parent d66462f commit 04c1dbf

20 files changed

Lines changed: 1354 additions & 33 deletions

File tree

‎document/format/src/export.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ impl<L: Layout> Gdd<L> {
154154
last_hot_sequence: self.session.last_hot_sequence(),
155155
retired_marks: self.session.retired_marks().clone(),
156156
clock_counter: self.session.clock_counter(),
157+
shared: false,
157158
view_settings: self.view_settings.clone(),
158159
network_view_settings: self.network_view_settings.clone(),
159160
};

‎document/format/src/manifest.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::{DEFAULT_HISTORY_CODEC, DEFAULT_HOT_LOG_CODEC, DEFAULT_REGISTRY_CODEC
99
pub const FORMAT_MAGIC: &str = "gdd";
1010

1111
/// Newest format version this build can open, which older documents migrate to. `cargo run migration snapshot` bumps it when the stored types change.
12-
pub const SUPPORTED_FORMAT_VERSION: u32 = 2;
12+
pub const SUPPORTED_FORMAT_VERSION: u32 = 3;
1313

1414
/// The on-disk codec for each working-copy payload, recorded so reads/writes never have to probe
1515
/// the filesystem to discover it. The manifest itself is excluded: it is always JSON, since it must

‎document/format/src/persist.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ impl<L: Layout> Gdd<L> {
292292
last_hot_sequence: self.session.last_hot_sequence(),
293293
retired_marks: self.session.retired_marks().clone(),
294294
clock_counter: self.session.clock_counter(),
295+
shared: false,
295296
view_settings: self.view_settings.clone(),
296297
network_view_settings: self.network_view_settings.clone(),
297298
}

‎document/format/tests/migrations.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,14 @@ fn opening_steps_the_format_version() {
9191
let (working, layout) = gdd.into_storage();
9292

9393
let migrated = GddV1::open_in(working, layout).await.unwrap();
94-
assert_eq!(migrated.manifest().format_version, 2);
94+
assert_eq!(migrated.manifest().format_version, manifest::SUPPORTED_FORMAT_VERSION);
9595

9696
let (working, layout) = migrated.into_storage();
97-
assert_eq!(GddV1::open_in(working, layout).await.unwrap().manifest().format_version, 2, "the bump persisted");
97+
assert_eq!(
98+
GddV1::open_in(working, layout).await.unwrap().manifest().format_version,
99+
manifest::SUPPORTED_FORMAT_VERSION,
100+
"the bump persisted"
101+
);
98102
});
99103
}
100104

‎document/graph-storage/src/crdt.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::{Attributes, Delta, PeerId, Prior, RegistryDelta, Rev, TimeStamp, Value, attr, compute_rev};
2+
use graphene_resource::ResourceHash;
23

34
impl Delta {
45
pub fn new(parent: Option<Rev>, author: PeerId, timestamp: TimeStamp, kind: RegistryDelta, reverse: Vec<Prior>) -> Self {
@@ -79,6 +80,17 @@ impl Delta {
7980
}
8081
}
8182

83+
impl RegistryDelta {
84+
pub fn resource_hash(&self) -> Option<ResourceHash> {
85+
match self {
86+
RegistryDelta::AddResource { entry, .. } => entry.hash,
87+
RegistryDelta::RemoveResource { snapshot, .. } => snapshot.hash,
88+
RegistryDelta::SetResourceHash { hash, .. } => *hash,
89+
_ => None,
90+
}
91+
}
92+
}
93+
8294
#[cfg(test)]
8395
mod tests {
8496
use super::*;

‎document/graph-storage/src/document.rs‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use std::collections::{HashMap, HashSet};
22
use std::hash::Hash;
33

44
use crate::{
5-
Attributes, CrdtError, Delta, ExportSlot, History, HotOp, HotSequence, Implementation, InputSlot, LamportClock, MAX_EXPORT_SLOTS, MAX_INPUT_SLOTS, Network, NetworkId, Node, NodeId, NodeInput,
6-
PeerId, PeerRegistration, Prior, Registry, RegistryDelta, ResourceEntry, ResourceId, RetiredMarks, Rev, SourceValue, TimeStamp, Tombstone, Touched, UserId,
5+
Attributes, CrdtError, Delta, ExportSlot, History, HotOp, HotOpId, HotSequence, Implementation, InputSlot, LamportClock, MAX_EXPORT_SLOTS, MAX_INPUT_SLOTS, Network, NetworkId, Node, NodeId,
6+
NodeInput, PeerId, PeerRegistration, Prior, Registry, RegistryDelta, ResourceEntry, ResourceId, RetiredMarks, Rev, SourceValue, TimeStamp, Tombstone, Touched, UserId,
77
};
88

99
#[derive(Clone, Debug)]
@@ -16,7 +16,7 @@ pub struct Document {
1616
pub(crate) hot_log: Vec<HotOp>,
1717
/// The stamps of the hot log's ops, so a re-announced op is recognised without a scan.
1818
pub(crate) hot_timestamps: HashSet<TimeStamp>,
19-
/// Which hot ops are retired into history, so a late copy is dropped.
19+
/// Which hot ops are retired or taken back, so a late copy is dropped.
2020
pub(crate) retired: RetiredMarks,
2121
/// The registry as of the last retirement, with no un-retired hot ops applied. Retirement computes
2222
/// each delta's `reverse` against this (so LWW reverses capture the true pre-op value, not the
@@ -112,12 +112,35 @@ impl Document {
112112
Ok(())
113113
}
114114

115+
/// Mark hot ops retired, dropping any the hot log holds. Returns what the dropped ops named.
116+
pub(crate) fn mark_retired(&mut self, retired: impl IntoIterator<Item = HotOpId>) -> Touched {
117+
self.retired.extend(retired);
118+
self.drop_retired_hot_ops()
119+
}
120+
115121
/// Take on a peer's marks as well as this peer's. Returns what the dropped ops named.
116122
pub(crate) fn absorb_retired_marks(&mut self, remote: &RetiredMarks) -> Touched {
117123
self.retired.absorb(remote);
118124
self.drop_retired_hot_ops()
119125
}
120126

127+
/// Take back hot ops for good, re-deriving the working registry without them. Returns what they named and the ops.
128+
pub(crate) fn retract_hot_ops(&mut self, ids: &[HotOpId]) -> (Touched, Vec<HotOp>) {
129+
let wanted: HashSet<HotOpId> = ids.iter().copied().collect();
130+
let taken: Vec<HotOp> = self.hot_log.extract_if(.., |hot_op| wanted.contains(&hot_op.id())).collect();
131+
self.retired.take_back(ids);
132+
133+
let mut touched = Touched::default();
134+
for hot_op in &taken {
135+
touched.record(&hot_op.op);
136+
self.hot_timestamps.remove(&hot_op.timestamp);
137+
}
138+
if !taken.is_empty() {
139+
self.rebuild_working();
140+
}
141+
(touched, taken)
142+
}
143+
121144
/// Bring the stamp index back in line after the hot log was filtered as a whole.
122145
pub(crate) fn resync_hot_timestamps(&mut self) {
123146
self.hot_timestamps = self.hot_log.iter().map(|hot_op| hot_op.timestamp).collect();

‎document/graph-storage/src/history.rs‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,7 @@ impl History {
7474
/// (idempotent re-apply) overwrites the existing entry in place rather than appending, so the
7575
/// order and index are unchanged.
7676
pub fn push(&mut self, delta: Delta) {
77-
match &delta.kind {
78-
RegistryDelta::AddResource { entry, .. } => self.resource_hashes.extend(entry.hash),
79-
RegistryDelta::RemoveResource { snapshot, .. } => self.resource_hashes.extend(snapshot.hash),
80-
RegistryDelta::SetResourceHash { hash: Some(hash), .. } => {
81-
self.resource_hashes.insert(*hash);
82-
}
83-
_ => {}
84-
}
77+
self.resource_hashes.extend(delta.kind.resource_hash());
8578
if let Some(&position) = self.index.get(&delta.id) {
8679
self.deltas[position] = delta;
8780
return;
@@ -212,6 +205,13 @@ impl History {
212205
}
213206
}
214207

208+
/// Record when a retired delta entered history (outside its `Rev`). Returns whether the delta was found.
209+
pub(crate) fn set_retired_at(&mut self, rev: Rev, wall_ms: u64) -> bool {
210+
let Some(&position) = self.index.get(&rev) else { return false };
211+
self.deltas[position].retired_at_ms = wall_ms;
212+
true
213+
}
214+
215215
/// Set a local annotation attribute (e.g. a commit message) on a retired delta in place. Excluded
216216
/// from the delta's `Rev`, so identity and the index are unchanged. Returns whether the delta was found.
217217
pub fn annotate(&mut self, rev: Rev, key: &str, value: Value, timestamp: TimeStamp) -> bool {

‎document/graph-storage/src/history_metadata.rs‎

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,25 @@ pub mod rev_attr {
4343
pub const TAG_PREFIX: &str = "tag:";
4444
}
4545

46-
/// The standing fact for each subject and key. Folding the same facts in any order gives the same record.
47-
#[derive(Clone, Debug, Default, PartialEq)]
46+
/// The standing fact for each subject and key. Folding the same facts in any order gives the same record, so it travels as its facts.
47+
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
48+
#[serde(from = "Vec<MetadataFact>", into = "Vec<MetadataFact>")]
4849
pub struct HistoryMetadata {
4950
facts: BTreeMap<Subject, BTreeMap<String, MetadataFact>>,
5051
}
5152

53+
impl From<Vec<MetadataFact>> for HistoryMetadata {
54+
fn from(facts: Vec<MetadataFact>) -> Self {
55+
Self::fold(facts)
56+
}
57+
}
58+
59+
impl From<HistoryMetadata> for Vec<MetadataFact> {
60+
fn from(record: HistoryMetadata) -> Self {
61+
record.facts().cloned().collect()
62+
}
63+
}
64+
5265
impl HistoryMetadata {
5366
pub fn is_empty(&self) -> bool {
5467
self.facts.is_empty()
@@ -107,13 +120,20 @@ impl HistoryMetadata {
107120
Some(MetadataFact { subject, key, value, stamp })
108121
}
109122

110-
/// Take on a fact unless the standing one has a later stamp, or the same stamp and a value that encodes no lower.
111-
pub fn absorb(&mut self, fact: MetadataFact) {
123+
/// Take on a fact unless the standing one has a later stamp, or the same stamp and a value that encodes no lower. Returns whether it landed.
124+
pub fn absorb(&mut self, fact: MetadataFact) -> bool {
112125
let attributes = self.facts.entry(fact.subject).or_default();
113126
let stands = |mine: &MetadataFact| mine.stamp.cmp(&fact.stamp).then_with(|| mine.value.to_string().cmp(&fact.value.to_string())).is_ge();
114-
if !attributes.get(&fact.key).is_some_and(stands) {
115-
attributes.insert(fact.key.clone(), fact);
127+
if attributes.get(&fact.key).is_some_and(stands) {
128+
return false;
116129
}
130+
attributes.insert(fact.key.clone(), fact);
131+
true
132+
}
133+
134+
/// Take on another copy's record. Returns the facts that landed.
135+
pub fn merge(&mut self, other: &Self) -> Vec<MetadataFact> {
136+
other.facts().filter(|fact| self.absorb((*fact).clone())).cloned().collect()
117137
}
118138
}
119139

@@ -145,6 +165,17 @@ mod tests {
145165
assert_eq!(HistoryMetadata::fold([name("Ada", 10), name("Bea", 10)]), HistoryMetadata::fold([name("Bea", 10), name("Ada", 10)]));
146166
}
147167

168+
#[test]
169+
fn merging_copies_agrees_either_way_and_only_once() {
170+
let a = HistoryMetadata::fold([fact(ADA, user_attr::NAME, "Ada", 10)]);
171+
let b = HistoryMetadata::fold([fact(ADA, user_attr::NAME, "Bea", 20)]);
172+
let (mut ab, mut ba) = (a.clone(), b.clone());
173+
assert_eq!(ab.merge(&b).len(), 1);
174+
assert!(ba.merge(&a).is_empty());
175+
assert_eq!(ab, ba);
176+
assert!(ab.merge(&b).is_empty(), "merging again changes nothing");
177+
}
178+
148179
#[test]
149180
fn a_clock_behind_a_peers_still_gets_its_say() {
150181
let record = HistoryMetadata::fold([fact(ADA, user_attr::NAME, "Ada", 100)]);

‎document/graph-storage/src/registry.rs‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{Attributes, NetworkId, NodeId, PeerId, Registry, ResourceId, ResourceStore, SourceKey, TimeStamp};
1+
use crate::{Attributes, NetworkId, NodeId, PeerId, Registry, ResourceId, ResourceStore, SourceKey, TimeStamp, Tombstone};
22
use std::collections::HashMap;
33

44
impl Registry {
@@ -25,6 +25,17 @@ impl Registry {
2525
}
2626
}
2727

28+
/// Whether both registries removed the same entities at the same times. Registries equal in value but not in their
29+
/// removal marks resolve later ops differently.
30+
pub fn removal_marks_equal(&self, other: &Self) -> bool {
31+
fn same<K: std::hash::Hash + Eq, T>(a: &HashMap<K, Tombstone<T>>, b: &HashMap<K, Tombstone<T>>) -> bool {
32+
a.len() == b.len()
33+
&& a.iter()
34+
.all(|(id, mark)| b.get(id).is_some_and(|other| other.timestamp == mark.timestamp && other.placeholder == mark.placeholder))
35+
}
36+
same(&self.removed_nodes, &other.removed_nodes) && same(&self.removed_networks, &other.removed_networks) && same(&self.removed_resources, &other.removed_resources)
37+
}
38+
2839
/// True if both registries agree on every value-bearing field, ignoring per-slot and
2940
/// per-attribute timestamps. Mirrors `compute_deltas`'s value-only semantics, so unchanged
3041
/// state at a stamped slot doesn't count as drift. `peer_users` is excluded: it isn't diffed by

‎document/graph-storage/src/schema/session.rs‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,14 @@ pub struct HotOpId {
2828
pub sequence: HotSequence,
2929
}
3030

31-
/// Which hot ops are retired: each author's gap-free prefix, plus runs past it while ops are in flight. Persisted, so
32-
/// a late copy of a retired op is dropped.
31+
/// Which hot ops are retired or taken back, as each author's gap-free prefix plus runs past it while ops are in flight.
32+
/// Persisted, so a late copy of such an op is dropped.
3333
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
3434
pub struct RetiredMarks {
3535
pub retired_up_to: HashMap<PeerId, HotSequence>,
3636
/// retired ops past their author's prefix, as inclusive runs, one per gap in flight.
3737
pub retired_runs: HashMap<PeerId, Vec<(HotSequence, HotSequence)>>,
38+
/// Which covered ops their author took back rather than retired, as inclusive runs.
39+
#[serde(default)]
40+
pub taken_back: HashMap<PeerId, Vec<(HotSequence, HotSequence)>>,
3841
}

0 commit comments

Comments
 (0)