merge_machines 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +57 -0
- data/Cargo.lock +294 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +252 -0
- data/ext/merge_machines_native/core/Cargo.toml +24 -0
- data/ext/merge_machines_native/core/src/gcounter.rs +242 -0
- data/ext/merge_machines_native/core/src/gset.rs +181 -0
- data/ext/merge_machines_native/core/src/hybrid.rs +222 -0
- data/ext/merge_machines_native/core/src/lib.rs +221 -0
- data/ext/merge_machines_native/core/src/logical.rs +139 -0
- data/ext/merge_machines_native/core/src/lww.rs +213 -0
- data/ext/merge_machines_native/core/src/orset.rs +334 -0
- data/ext/merge_machines_native/core/src/pncounter.rs +165 -0
- data/ext/merge_machines_native/extconf.rb +3 -0
- data/ext/merge_machines_native/ffi/Cargo.toml +20 -0
- data/ext/merge_machines_native/ffi/extconf.rb +21 -0
- data/ext/merge_machines_native/ffi/src/data.rs +44 -0
- data/ext/merge_machines_native/ffi/src/gcounter.rs +138 -0
- data/ext/merge_machines_native/ffi/src/gset.rs +187 -0
- data/ext/merge_machines_native/ffi/src/hash.rs +33 -0
- data/ext/merge_machines_native/ffi/src/hybrid.rs +110 -0
- data/ext/merge_machines_native/ffi/src/lib.rs +221 -0
- data/ext/merge_machines_native/ffi/src/logical.rs +89 -0
- data/ext/merge_machines_native/ffi/src/lww.rs +360 -0
- data/ext/merge_machines_native/ffi/src/names.rs +82 -0
- data/ext/merge_machines_native/ffi/src/orset.rs +240 -0
- data/ext/merge_machines_native/ffi/src/pncounter.rs +143 -0
- data/ext/merge_machines_native/ffi/src/shape.rs +116 -0
- data/ext/merge_machines_native/ffi/src/wall.rs +37 -0
- data/lib/merge_machines/version.rb +5 -0
- data/lib/merge_machines.rb +8 -0
- data/sig/merge_machines.rbs +238 -0
- metadata +98 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//! `MergeMachines::LogicalClock`: ordering events with no clock at all.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
|
|
5
|
+
use bytes::Bytes;
|
|
6
|
+
use magnus::scan_args::scan_args;
|
|
7
|
+
use magnus::{Error, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
|
|
8
|
+
use merge_machines::{LogicalClock, ReplicaId};
|
|
9
|
+
|
|
10
|
+
use crate::{Whole, codec_error, exhausted, required_kwarg, trailing_replica};
|
|
11
|
+
|
|
12
|
+
/// A clock and the replica whose events it stamps, which the core does not
|
|
13
|
+
/// hold.
|
|
14
|
+
#[magnus::wrap(class = "MergeMachines::LogicalClock", free_immediately, size)]
|
|
15
|
+
struct Wrapper {
|
|
16
|
+
clock: RefCell<LogicalClock>,
|
|
17
|
+
replica: ReplicaId,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl Wrapper {
|
|
21
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
22
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
23
|
+
let Whole(replica): Whole<ReplicaId> = required_kwarg(args.keywords, "replica")?;
|
|
24
|
+
Ok(Self {
|
|
25
|
+
clock: RefCell::new(LogicalClock::new()),
|
|
26
|
+
replica,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Reads a blob written by [`dump`](Self::dump).
|
|
31
|
+
fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
|
|
32
|
+
let mut clock = LogicalClock::new();
|
|
33
|
+
clock
|
|
34
|
+
.decode_into(&blob)
|
|
35
|
+
.map_err(|error| codec_error(ruby, error))?;
|
|
36
|
+
|
|
37
|
+
Ok(Self {
|
|
38
|
+
clock: RefCell::new(clock),
|
|
39
|
+
replica: trailing_replica(ruby, &blob, LogicalClock::ENCODED_LEN)?,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fn replica(&self) -> ReplicaId {
|
|
44
|
+
self.replica
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// The last stamp this clock issued.
|
|
48
|
+
fn now(&self) -> u64 {
|
|
49
|
+
self.clock.borrow().now()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Stamps a local event.
|
|
53
|
+
fn tick(ruby: &Ruby, rb_self: &Self) -> Result<u64, Error> {
|
|
54
|
+
exhausted(ruby, rb_self.clock.borrow_mut().tick())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Stamps the receipt of `observed`, a stamp from another replica.
|
|
58
|
+
fn observe(ruby: &Ruby, rb_self: &Self, Whole(observed): Whole<u64>) -> Result<u64, Error> {
|
|
59
|
+
exhausted(ruby, rb_self.clock.borrow_mut().observe(observed))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// The core frame followed by the replica id, 64-bit little-endian.
|
|
63
|
+
fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
64
|
+
let mut blob = Vec::from(rb_self.clock.borrow().encoded());
|
|
65
|
+
blob.extend_from_slice(&rb_self.replica.to_le_bytes());
|
|
66
|
+
ruby.str_from_slice(&blob)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn inspect(&self) -> String {
|
|
70
|
+
format!(
|
|
71
|
+
"#<MergeMachines::LogicalClock replica={} now={}>",
|
|
72
|
+
self.replica,
|
|
73
|
+
self.now()
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
79
|
+
let class = module.define_class("LogicalClock", ruby.class_object())?;
|
|
80
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
81
|
+
class.define_singleton_method("load", function!(Wrapper::load, 1))?;
|
|
82
|
+
class.define_method("replica", method!(Wrapper::replica, 0))?;
|
|
83
|
+
class.define_method("now", method!(Wrapper::now, 0))?;
|
|
84
|
+
class.define_method("tick", method!(Wrapper::tick, 0))?;
|
|
85
|
+
class.define_method("observe", method!(Wrapper::observe, 1))?;
|
|
86
|
+
class.define_method("dump", method!(Wrapper::dump, 0))?;
|
|
87
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
88
|
+
Ok(())
|
|
89
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
//! `MergeMachines::LwwRegister`: one value, held by whichever write was
|
|
2
|
+
//! stamped last.
|
|
3
|
+
|
|
4
|
+
use std::cell::RefCell;
|
|
5
|
+
|
|
6
|
+
use bytes::Bytes;
|
|
7
|
+
use magnus::scan_args::{get_kwargs, scan_args};
|
|
8
|
+
use magnus::typed_data::Obj;
|
|
9
|
+
use magnus::{Error, RHash, RModule, RString, Ruby, Symbol, Value, function, method, prelude::*};
|
|
10
|
+
use merge_machines::{
|
|
11
|
+
CodecError, HybridTimestamp, LwwRegister, Merge, ReplicaId, STAMP_HYBRID, STAMP_LOGICAL, Stamp,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
use crate::data::{stamped, timestamp};
|
|
15
|
+
use crate::hash::hash64;
|
|
16
|
+
use crate::names::{self, Names};
|
|
17
|
+
use crate::shape::{Framed, boxed_decode, boxed_new, frame_len_of};
|
|
18
|
+
use crate::{Whole, codec_error, mismatch, optional_kwarg};
|
|
19
|
+
|
|
20
|
+
/// Which clock stamps a register's writes.
|
|
21
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
22
|
+
enum Kind {
|
|
23
|
+
/// One 64-bit counter per write.
|
|
24
|
+
Logical,
|
|
25
|
+
/// A wall-clock reading and a counter per write.
|
|
26
|
+
Hybrid,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Kind {
|
|
30
|
+
const fn name(self) -> &'static str {
|
|
31
|
+
match self {
|
|
32
|
+
Self::Logical => "logical",
|
|
33
|
+
Self::Hybrid => "hybrid",
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The two registers are stamped by different clocks.
|
|
39
|
+
struct ClockMismatch;
|
|
40
|
+
|
|
41
|
+
/// A register stamped by one of the two clocks.
|
|
42
|
+
#[derive(Clone)]
|
|
43
|
+
enum Register {
|
|
44
|
+
Logical(Box<LwwRegister<u64, u64>>),
|
|
45
|
+
Hybrid(Box<LwwRegister<u64, HybridTimestamp>>),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Runs `body` against whichever variant is held.
|
|
49
|
+
macro_rules! register {
|
|
50
|
+
($register:expr, |$inner:ident| $body:expr) => {
|
|
51
|
+
match $register {
|
|
52
|
+
Register::Logical($inner) => $body,
|
|
53
|
+
Register::Hybrid($inner) => $body,
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
impl Register {
|
|
59
|
+
const fn kind(&self) -> Kind {
|
|
60
|
+
match self {
|
|
61
|
+
Self::Logical(_) => Kind::Logical,
|
|
62
|
+
Self::Hybrid(_) => Kind::Hybrid,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// The hash of the value being held.
|
|
67
|
+
fn value(&self) -> u64 {
|
|
68
|
+
register!(self, |inner| *inner.value())
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Keeps whichever of the two writes was stamped last.
|
|
72
|
+
fn merge(&mut self, other: &Self) -> Result<(), ClockMismatch> {
|
|
73
|
+
match (self, other) {
|
|
74
|
+
(Self::Logical(held), Self::Logical(source)) => {
|
|
75
|
+
let Ok(()) = held.merge(source);
|
|
76
|
+
Ok(())
|
|
77
|
+
}
|
|
78
|
+
(Self::Hybrid(held), Self::Hybrid(source)) => {
|
|
79
|
+
let Ok(()) = held.merge(source);
|
|
80
|
+
Ok(())
|
|
81
|
+
}
|
|
82
|
+
_ => Err(ClockMismatch),
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// A register, and whether a write has landed in it.
|
|
88
|
+
///
|
|
89
|
+
/// A write is kept only when it beats the one being held, so a register that
|
|
90
|
+
/// has had none has to say so rather than hold a zeroed write: stamp 0 from
|
|
91
|
+
/// replica 0 is a write like any other, and nothing beats it.
|
|
92
|
+
#[derive(Clone)]
|
|
93
|
+
enum Slot {
|
|
94
|
+
Empty(Register),
|
|
95
|
+
Held(Register),
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl Slot {
|
|
99
|
+
const fn register(&self) -> &Register {
|
|
100
|
+
match self {
|
|
101
|
+
Self::Empty(register) | Self::Held(register) => register,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const fn kind(&self) -> Kind {
|
|
106
|
+
self.register().kind()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// The hash of the value being held, `None` until the first write.
|
|
110
|
+
fn held(&self) -> Option<u64> {
|
|
111
|
+
match self {
|
|
112
|
+
Self::Empty(_) => None,
|
|
113
|
+
Self::Held(register) => Some(register.value()),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Folds `other` in: taken outright when no write has landed here yet,
|
|
118
|
+
/// otherwise kept only when it beats the write being held.
|
|
119
|
+
fn merge(&mut self, other: &Self) -> Result<(), ClockMismatch> {
|
|
120
|
+
if self.kind() != other.kind() {
|
|
121
|
+
return Err(ClockMismatch);
|
|
122
|
+
}
|
|
123
|
+
let Self::Held(source) = other else {
|
|
124
|
+
return Ok(());
|
|
125
|
+
};
|
|
126
|
+
if let Self::Held(held) = self {
|
|
127
|
+
held.merge(source)
|
|
128
|
+
} else {
|
|
129
|
+
*self = Self::Held(source.clone());
|
|
130
|
+
Ok(())
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// The register, and the string its value was hashed from.
|
|
136
|
+
#[magnus::wrap(class = "MergeMachines::LwwRegister", free_immediately, size)]
|
|
137
|
+
struct Wrapper {
|
|
138
|
+
slot: RefCell<Slot>,
|
|
139
|
+
names: RefCell<Names>,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
impl<S: Stamp> Framed for LwwRegister<u64, S> {
|
|
143
|
+
const FRAME_LEN: usize = Self::ENCODED_LEN;
|
|
144
|
+
|
|
145
|
+
fn blank() -> Self {
|
|
146
|
+
Self::new(0, S::default(), 0)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn read_frame(&mut self, frame: &[u8]) -> Result<(), CodecError> {
|
|
150
|
+
self.decode_into(frame)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Whether a frame carries a write rather than the state a register starts out
|
|
155
|
+
/// holding.
|
|
156
|
+
fn holds_write<S: Stamp>(register: &LwwRegister<u64, S>) -> bool {
|
|
157
|
+
*register != LwwRegister::blank()
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#[inline(never)]
|
|
161
|
+
fn boxed_write<S: Stamp>(element: u64, stamp: S, replica: ReplicaId) -> Box<LwwRegister<u64, S>> {
|
|
162
|
+
Box::new(LwwRegister::new(element, stamp, replica))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
impl Wrapper {
|
|
166
|
+
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
167
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
168
|
+
let kind = requested_kind(ruby, optional_kwarg(args.keywords, "stamp")?)?;
|
|
169
|
+
Ok(Self {
|
|
170
|
+
slot: RefCell::new(Slot::Empty(dispatch(ruby, kind, None)?)),
|
|
171
|
+
names: RefCell::new(Names::new()),
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/// Reads a blob written by [`dump`](Self::dump).
|
|
176
|
+
///
|
|
177
|
+
/// A blob names the value it holds, so one with no name record and a zeroed
|
|
178
|
+
/// frame came from a register nobody wrote.
|
|
179
|
+
fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
|
|
180
|
+
let kind = match blob.get(crate::SHAPE).map(|field| field[0]) {
|
|
181
|
+
Some(STAMP_LOGICAL) => Kind::Logical,
|
|
182
|
+
Some(STAMP_HYBRID) => Kind::Hybrid,
|
|
183
|
+
_ => return Err(codec_error(ruby, CodecError::Parameters)),
|
|
184
|
+
};
|
|
185
|
+
let register = dispatch(ruby, kind, Some(&blob))?;
|
|
186
|
+
let frame_len = register!(®ister, |inner| frame_len_of(&**inner));
|
|
187
|
+
let names = names::read(ruby, &blob[frame_len..])?;
|
|
188
|
+
let written = !names.is_empty() || register!(®ister, |inner| holds_write(&**inner));
|
|
189
|
+
let slot = if written {
|
|
190
|
+
names::complete(ruby, &names, core::iter::once(register.value()))?;
|
|
191
|
+
Slot::Held(register)
|
|
192
|
+
} else {
|
|
193
|
+
Slot::Empty(register)
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
Ok(Self {
|
|
197
|
+
slot: RefCell::new(slot),
|
|
198
|
+
names: RefCell::new(names),
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn kind(&self) -> Kind {
|
|
203
|
+
self.slot.borrow().kind()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/// Which clock stamps this register's writes.
|
|
207
|
+
fn stamp_kind(ruby: &Ruby, rb_self: &Self) -> Symbol {
|
|
208
|
+
ruby.to_symbol(rb_self.kind().name())
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// Writes `value` if `stamp:` and `replica:` beat the write already held.
|
|
212
|
+
fn set(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<(), Error> {
|
|
213
|
+
let args = scan_args::<(String,), (), (), (), RHash, ()>(args)?;
|
|
214
|
+
let (value,) = args.required;
|
|
215
|
+
let (Whole(replica), stamp) = get_kwargs::<_, (Whole<ReplicaId>, Value), (), ()>(
|
|
216
|
+
args.keywords,
|
|
217
|
+
&["replica", "stamp"],
|
|
218
|
+
&[],
|
|
219
|
+
)?
|
|
220
|
+
.required;
|
|
221
|
+
|
|
222
|
+
let element = hash64(value.as_bytes());
|
|
223
|
+
let kind = rb_self.kind();
|
|
224
|
+
let write = match kind {
|
|
225
|
+
Kind::Logical => Register::Logical(boxed_write(element, logical(stamp)?, replica)),
|
|
226
|
+
Kind::Hybrid => Register::Hybrid(boxed_write(element, hybrid(stamp)?, replica)),
|
|
227
|
+
};
|
|
228
|
+
folded(ruby, &mut rb_self.slot.borrow_mut(), &Slot::Held(write))?;
|
|
229
|
+
rb_self.names.borrow_mut().insert(element, value);
|
|
230
|
+
rb_self.prune();
|
|
231
|
+
Ok(())
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// The value being held, `nil` until the first write.
|
|
235
|
+
fn value(&self) -> Option<String> {
|
|
236
|
+
let held = self.slot.borrow().held()?;
|
|
237
|
+
self.names.borrow().get(&held).cloned()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// The stamp of the write being held.
|
|
241
|
+
fn stamp(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
242
|
+
match rb_self.slot.borrow().register() {
|
|
243
|
+
Register::Logical(register) => Ok(ruby.into_value(*register.stamp())),
|
|
244
|
+
Register::Hybrid(register) => timestamp(ruby, *register.stamp()),
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// The replica that wrote what is being held.
|
|
249
|
+
fn replica(&self) -> ReplicaId {
|
|
250
|
+
register!(self.slot.borrow().register(), |register| register.replica())
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
|
|
254
|
+
let source = other.slot.borrow().clone();
|
|
255
|
+
let names = other.names.borrow().clone();
|
|
256
|
+
folded(ruby, &mut rb_self.slot.borrow_mut(), &source)?;
|
|
257
|
+
rb_self.names.borrow_mut().extend(names);
|
|
258
|
+
rb_self.prune();
|
|
259
|
+
Ok(rb_self)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// The core frame followed by the name of the value it holds.
|
|
263
|
+
///
|
|
264
|
+
/// ```text
|
|
265
|
+
/// core frame LwwRegister::<u64, S>::ENCODED_LEN bytes, tag 0x71
|
|
266
|
+
/// name record u64 little-endian byte length, then that many UTF-8 bytes
|
|
267
|
+
/// ```
|
|
268
|
+
///
|
|
269
|
+
/// The frame holds the value's 64-bit hash, so [`load`](Self::load)
|
|
270
|
+
/// rehashes the name to give [`value`](Self::value) its string back.
|
|
271
|
+
fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
272
|
+
let slot = rb_self.slot.borrow();
|
|
273
|
+
let frame = register!(slot.register(), |register| register.encoded());
|
|
274
|
+
let mut blob = Vec::from(frame);
|
|
275
|
+
names::write(&mut blob, &rb_self.names.borrow(), slot.held().into_iter());
|
|
276
|
+
ruby.str_from_slice(&blob)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn inspect(&self) -> String {
|
|
280
|
+
format!(
|
|
281
|
+
"#<MergeMachines::LwwRegister stamp={} replica={} value={}>",
|
|
282
|
+
self.kind().name(),
|
|
283
|
+
self.replica(),
|
|
284
|
+
self.value()
|
|
285
|
+
.map_or_else(|| "nil".to_owned(), |value| format!("{value:?}"))
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Drops the names of writes that lost.
|
|
290
|
+
fn prune(&self) {
|
|
291
|
+
let held = self.slot.borrow().held();
|
|
292
|
+
self.names
|
|
293
|
+
.borrow_mut()
|
|
294
|
+
.retain(|element, _| Some(*element) == held);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Folds `other` into `slot`, rejecting a register the other clock stamps.
|
|
299
|
+
fn folded(ruby: &Ruby, slot: &mut Slot, other: &Slot) -> Result<(), Error> {
|
|
300
|
+
slot.merge(other)
|
|
301
|
+
.map_err(|ClockMismatch| mismatch(ruby, "register"))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/// The counter a `stamp:` names.
|
|
305
|
+
fn logical(value: Value) -> Result<u64, Error> {
|
|
306
|
+
Whole::try_convert(value).map(|Whole(stamp)| stamp)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// The timestamp a `Timestamp` names.
|
|
310
|
+
fn hybrid(value: Value) -> Result<HybridTimestamp, Error> {
|
|
311
|
+
let Whole(wall_ns) = value.funcall::<_, _, Whole<u64>>("wall_ns", ())?;
|
|
312
|
+
let Whole(counter) = value.funcall::<_, _, Whole<u64>>("counter", ())?;
|
|
313
|
+
Ok(stamped(wall_ns, counter))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// Which clock a `stamp:` keyword asks for, defaulting to the logical one.
|
|
317
|
+
fn requested_kind(ruby: &Ruby, given: Option<Value>) -> Result<Kind, Error> {
|
|
318
|
+
let Some(given) = given else {
|
|
319
|
+
return Ok(Kind::Logical);
|
|
320
|
+
};
|
|
321
|
+
let name = Symbol::from_value(given).and_then(|symbol| symbol.name().ok());
|
|
322
|
+
match name.as_deref() {
|
|
323
|
+
Some(name) if name == Kind::Logical.name() => Ok(Kind::Logical),
|
|
324
|
+
Some(name) if name == Kind::Hybrid.name() => Ok(Kind::Hybrid),
|
|
325
|
+
_ => Err(Error::new(
|
|
326
|
+
ruby.exception_arg_error(),
|
|
327
|
+
format!(
|
|
328
|
+
"stamp {} is not supported, expected one of [:{}, :{}]",
|
|
329
|
+
given.inspect(),
|
|
330
|
+
Kind::Logical.name(),
|
|
331
|
+
Kind::Hybrid.name()
|
|
332
|
+
),
|
|
333
|
+
)),
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/// The variant for `kind`, read from `blob` when there is one.
|
|
338
|
+
fn dispatch(ruby: &Ruby, kind: Kind, blob: Option<&[u8]>) -> Result<Register, Error> {
|
|
339
|
+
match (kind, blob) {
|
|
340
|
+
(Kind::Logical, None) => Ok(Register::Logical(boxed_new())),
|
|
341
|
+
(Kind::Hybrid, None) => Ok(Register::Hybrid(boxed_new())),
|
|
342
|
+
(Kind::Logical, Some(frame)) => Ok(Register::Logical(boxed_decode(ruby, frame)?)),
|
|
343
|
+
(Kind::Hybrid, Some(frame)) => Ok(Register::Hybrid(boxed_decode(ruby, frame)?)),
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
348
|
+
let class = module.define_class("LwwRegister", ruby.class_object())?;
|
|
349
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
350
|
+
class.define_singleton_method("load", function!(Wrapper::load, 1))?;
|
|
351
|
+
class.define_method("stamp_kind", method!(Wrapper::stamp_kind, 0))?;
|
|
352
|
+
class.define_method("set", method!(Wrapper::set, -1))?;
|
|
353
|
+
class.define_method("value", method!(Wrapper::value, 0))?;
|
|
354
|
+
class.define_method("stamp", method!(Wrapper::stamp, 0))?;
|
|
355
|
+
class.define_method("replica", method!(Wrapper::replica, 0))?;
|
|
356
|
+
class.define_method("merge", method!(Wrapper::merge, 1))?;
|
|
357
|
+
class.define_method("dump", method!(Wrapper::dump, 0))?;
|
|
358
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
359
|
+
Ok(())
|
|
360
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//! The side table that gives a 64-bit element its Ruby string back.
|
|
2
|
+
|
|
3
|
+
use std::collections::HashMap;
|
|
4
|
+
|
|
5
|
+
use magnus::{Error, RArray, Ruby};
|
|
6
|
+
|
|
7
|
+
use crate::hash::hash64;
|
|
8
|
+
use crate::{FIELD_LEN, field};
|
|
9
|
+
|
|
10
|
+
/// What each element the core holds was hashed from.
|
|
11
|
+
pub type Names = HashMap<u64, String>;
|
|
12
|
+
|
|
13
|
+
/// Appends one record per named element to `blob`: a 64-bit little-endian byte
|
|
14
|
+
/// length, then that many UTF-8 bytes.
|
|
15
|
+
pub fn write(blob: &mut Vec<u8>, names: &Names, elements: impl Iterator<Item = u64>) {
|
|
16
|
+
for name in elements.filter_map(|element| names.get(&element)) {
|
|
17
|
+
blob.extend_from_slice(&field(name.len()));
|
|
18
|
+
blob.extend_from_slice(name.as_bytes());
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Reads the records that follow a core frame back into a side table.
|
|
23
|
+
pub fn read(ruby: &Ruby, mut records: &[u8]) -> Result<Names, Error> {
|
|
24
|
+
let mut names = Names::new();
|
|
25
|
+
while !records.is_empty() {
|
|
26
|
+
let Some((length, tail)) = records.split_first_chunk::<FIELD_LEN>() else {
|
|
27
|
+
return Err(rejected(ruby, "ends inside a record header"));
|
|
28
|
+
};
|
|
29
|
+
let Ok(length) = usize::try_from(u64::from_le_bytes(*length)) else {
|
|
30
|
+
return Err(rejected(ruby, "names a key this platform cannot hold"));
|
|
31
|
+
};
|
|
32
|
+
let Some((name, tail)) = tail.split_at_checked(length) else {
|
|
33
|
+
return Err(rejected(ruby, "ends inside a key"));
|
|
34
|
+
};
|
|
35
|
+
let Ok(name) = str::from_utf8(name) else {
|
|
36
|
+
return Err(rejected(ruby, "holds a key that is not UTF-8"));
|
|
37
|
+
};
|
|
38
|
+
names.insert(hash64(name.as_bytes()), name.to_owned());
|
|
39
|
+
records = tail;
|
|
40
|
+
}
|
|
41
|
+
Ok(names)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Checks that every element the core holds has a string to come back as.
|
|
45
|
+
pub fn complete(
|
|
46
|
+
ruby: &Ruby,
|
|
47
|
+
names: &Names,
|
|
48
|
+
mut elements: impl Iterator<Item = u64>,
|
|
49
|
+
) -> Result<(), Error> {
|
|
50
|
+
if elements.all(|element| names.contains_key(&element)) {
|
|
51
|
+
Ok(())
|
|
52
|
+
} else {
|
|
53
|
+
Err(rejected(ruby, "does not name every element"))
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// The strings `elements` came from, as a Ruby array in the order the core
|
|
58
|
+
/// reported them.
|
|
59
|
+
pub fn members(ruby: &Ruby, names: &Names, elements: &[u64]) -> Result<RArray, Error> {
|
|
60
|
+
let members = ruby.ary_new_capa(elements.len());
|
|
61
|
+
for element in elements {
|
|
62
|
+
members.push(ruby.str_new(&named(ruby, names, *element)?))?;
|
|
63
|
+
}
|
|
64
|
+
Ok(members)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// The string an element came from.
|
|
68
|
+
fn named(ruby: &Ruby, names: &Names, element: u64) -> Result<String, Error> {
|
|
69
|
+
names.get(&element).cloned().ok_or_else(|| {
|
|
70
|
+
Error::new(
|
|
71
|
+
ruby.exception_runtime_error(),
|
|
72
|
+
"an element lost the key it was hashed from",
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn rejected(ruby: &Ruby, reason: &str) -> Error {
|
|
78
|
+
Error::new(
|
|
79
|
+
ruby.exception_arg_error(),
|
|
80
|
+
format!("the blob's key table {reason}"),
|
|
81
|
+
)
|
|
82
|
+
}
|