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,138 @@
|
|
|
1
|
+
//! `MergeMachines::GCounter`: a counter that only goes up.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
|
|
5
|
+
use bytes::Bytes;
|
|
6
|
+
use magnus::scan_args::{get_kwargs, scan_args};
|
|
7
|
+
use magnus::typed_data::Obj;
|
|
8
|
+
use magnus::{Error, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
|
|
9
|
+
use merge_machines::{GCounter, ReplicaId};
|
|
10
|
+
|
|
11
|
+
use crate::shape::shaped;
|
|
12
|
+
use crate::{
|
|
13
|
+
REPLICAS, Whole, blob_shape, capacity, mismatch, required_kwarg, unsupported, unsupported_blob,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/// The counter at a single replica count.
|
|
17
|
+
type Held<const R: usize> = GCounter<R>;
|
|
18
|
+
|
|
19
|
+
shaped!(Counter over Held { R4 = 4, R8 = 8, R16 = 16, R32 = 32 });
|
|
20
|
+
|
|
21
|
+
/// Runs `body` against whichever variant is held.
|
|
22
|
+
macro_rules! counter {
|
|
23
|
+
($counter:expr, |$inner:ident| $body:expr) => {
|
|
24
|
+
match $counter {
|
|
25
|
+
Counter::R4($inner) => $body,
|
|
26
|
+
Counter::R8($inner) => $body,
|
|
27
|
+
Counter::R16($inner) => $body,
|
|
28
|
+
Counter::R32($inner) => $body,
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#[magnus::wrap(class = "MergeMachines::GCounter", free_immediately, size)]
|
|
34
|
+
struct Wrapper(RefCell<Counter>);
|
|
35
|
+
|
|
36
|
+
const fn replicas_of<const R: usize>(_: &Held<R>) -> usize {
|
|
37
|
+
R
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl Wrapper {
|
|
41
|
+
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
42
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
43
|
+
let Whole(replicas): Whole<usize> = required_kwarg(args.keywords, "replicas")?;
|
|
44
|
+
Ok(Self(RefCell::new(dispatch(ruby, replicas, None)?)))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Reads a blob written by [`dump`](Self::dump).
|
|
48
|
+
fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
|
|
49
|
+
let replicas = blob_shape(ruby, &blob)?;
|
|
50
|
+
Ok(Self(RefCell::new(dispatch(ruby, replicas, Some(&blob))?)))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn replicas(&self) -> usize {
|
|
54
|
+
counter!(&*self.0.borrow(), |counter| replicas_of(&**counter))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Adds `by:` to `replica:`'s own count.
|
|
58
|
+
fn increment(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<(), Error> {
|
|
59
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
60
|
+
let (replica, by) = increment_args(args.keywords)?;
|
|
61
|
+
capacity(
|
|
62
|
+
ruby,
|
|
63
|
+
counter!(&mut *rb_self.0.borrow_mut(), |counter| counter
|
|
64
|
+
.increment(replica, by)),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// What `replica:` has added.
|
|
69
|
+
fn count(&self, args: &[Value]) -> Result<u64, Error> {
|
|
70
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
71
|
+
let Whole(replica): Whole<ReplicaId> = required_kwarg(args.keywords, "replica")?;
|
|
72
|
+
Ok(counter!(&*self.0.borrow(), |counter| counter.count(replica)))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Every replica's count.
|
|
76
|
+
fn value(&self) -> u128 {
|
|
77
|
+
counter!(&*self.0.borrow(), |counter| counter.value())
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
|
|
81
|
+
let source = other.0.borrow().clone();
|
|
82
|
+
let Some(folded) = rb_self.0.borrow_mut().absorb(&source) else {
|
|
83
|
+
return Err(mismatch(ruby, "counter"));
|
|
84
|
+
};
|
|
85
|
+
capacity(ruby, folded)?;
|
|
86
|
+
Ok(rb_self)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// The core frame, tag 0x61.
|
|
90
|
+
fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
91
|
+
let frame = counter!(&*rb_self.0.borrow(), |counter| counter.encoded());
|
|
92
|
+
ruby.str_from_slice(&frame)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn inspect(&self) -> String {
|
|
96
|
+
format!(
|
|
97
|
+
"#<MergeMachines::GCounter replicas={} value={}>",
|
|
98
|
+
self.replicas(),
|
|
99
|
+
self.value()
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// The `replica:` an increment is credited to and the `by:` it adds.
|
|
105
|
+
pub fn increment_args(keywords: RHash) -> Result<(ReplicaId, u64), Error> {
|
|
106
|
+
let keywords = get_kwargs::<_, (Whole<ReplicaId>,), (Option<Whole<u64>>,), ()>(
|
|
107
|
+
keywords,
|
|
108
|
+
&["replica"],
|
|
109
|
+
&["by"],
|
|
110
|
+
)?;
|
|
111
|
+
let (Whole(replica),) = keywords.required;
|
|
112
|
+
let (by,) = keywords.optional;
|
|
113
|
+
Ok((replica, by.map_or(1, |Whole(by)| by)))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// The variant for `replicas`, read from `blob` when there is one.
|
|
117
|
+
fn dispatch(ruby: &Ruby, replicas: usize, blob: Option<&[u8]>) -> Result<Counter, Error> {
|
|
118
|
+
let Some(frame) = blob else {
|
|
119
|
+
return Counter::blank(replicas)
|
|
120
|
+
.ok_or_else(|| unsupported(ruby, "replicas", replicas, &REPLICAS));
|
|
121
|
+
};
|
|
122
|
+
Counter::decoded(ruby, replicas, frame)
|
|
123
|
+
.unwrap_or_else(|| Err(unsupported_blob(ruby, "replica count", &REPLICAS)))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
127
|
+
let class = module.define_class("GCounter", ruby.class_object())?;
|
|
128
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
129
|
+
class.define_singleton_method("load", function!(Wrapper::load, 1))?;
|
|
130
|
+
class.define_method("replicas", method!(Wrapper::replicas, 0))?;
|
|
131
|
+
class.define_method("increment", method!(Wrapper::increment, -1))?;
|
|
132
|
+
class.define_method("count", method!(Wrapper::count, -1))?;
|
|
133
|
+
class.define_method("value", method!(Wrapper::value, 0))?;
|
|
134
|
+
class.define_method("merge", method!(Wrapper::merge, 1))?;
|
|
135
|
+
class.define_method("dump", method!(Wrapper::dump, 0))?;
|
|
136
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
137
|
+
Ok(())
|
|
138
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
//! `MergeMachines::GSet`: a set nothing is ever taken out of.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
|
|
5
|
+
use bytes::Bytes;
|
|
6
|
+
use magnus::scan_args::scan_args;
|
|
7
|
+
use magnus::typed_data::Obj;
|
|
8
|
+
use magnus::{Error, RArray, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
|
|
9
|
+
use merge_machines::GSet;
|
|
10
|
+
|
|
11
|
+
use crate::hash::hash64;
|
|
12
|
+
use crate::names::{self, Names};
|
|
13
|
+
use crate::shape::{frame_len_of, shaped};
|
|
14
|
+
use crate::{
|
|
15
|
+
CAPACITIES, Whole, blob_shape, capacity, mismatch, required_kwarg, unsupported,
|
|
16
|
+
unsupported_blob,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/// The set at a single capacity, over the 64-bit hash each key becomes.
|
|
20
|
+
type Held<const N: usize> = GSet<u64, N>;
|
|
21
|
+
|
|
22
|
+
shaped!(Set over Held { N64 = 64, N256 = 256, N1024 = 1024 });
|
|
23
|
+
|
|
24
|
+
/// Runs `body` against whichever variant is held.
|
|
25
|
+
macro_rules! set {
|
|
26
|
+
($set:expr, |$inner:ident| $body:expr) => {
|
|
27
|
+
match $set {
|
|
28
|
+
Set::N64($inner) => $body,
|
|
29
|
+
Set::N256($inner) => $body,
|
|
30
|
+
Set::N1024($inner) => $body,
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// The set counts 64-bit key hashes, and `names` holds the string each one
|
|
36
|
+
/// came from.
|
|
37
|
+
#[magnus::wrap(class = "MergeMachines::GSet", free_immediately, size)]
|
|
38
|
+
struct Wrapper {
|
|
39
|
+
set: RefCell<Set>,
|
|
40
|
+
names: RefCell<Names>,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fn capacity_of<const N: usize>(_: &Held<N>) -> usize {
|
|
44
|
+
N
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl Wrapper {
|
|
48
|
+
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
49
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
50
|
+
let Whole(capacity): Whole<usize> = required_kwarg(args.keywords, "capacity")?;
|
|
51
|
+
Ok(Self {
|
|
52
|
+
set: RefCell::new(dispatch(ruby, capacity, None)?),
|
|
53
|
+
names: RefCell::new(Names::new()),
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// Reads a blob written by [`dump`](Self::dump).
|
|
58
|
+
fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
|
|
59
|
+
let set = dispatch(ruby, blob_shape(ruby, &blob)?, Some(&blob))?;
|
|
60
|
+
let frame_len = set!(&set, |inner| frame_len_of(&**inner));
|
|
61
|
+
let names = names::read(ruby, &blob[frame_len..])?;
|
|
62
|
+
let elements = set!(&set, |inner| inner.iter().collect::<Vec<u64>>());
|
|
63
|
+
names::complete(ruby, &names, elements.into_iter())?;
|
|
64
|
+
|
|
65
|
+
let wrapper = Self {
|
|
66
|
+
set: RefCell::new(set),
|
|
67
|
+
names: RefCell::new(names),
|
|
68
|
+
};
|
|
69
|
+
wrapper.prune();
|
|
70
|
+
Ok(wrapper)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn capacity(&self) -> usize {
|
|
74
|
+
set!(&*self.set.borrow(), |set| capacity_of(&**set))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Adds `key`, which is a no-op if it is already in.
|
|
78
|
+
fn add(ruby: &Ruby, rb_self: &Self, key: String) -> Result<(), Error> {
|
|
79
|
+
let element = hash64(key.as_bytes());
|
|
80
|
+
capacity(
|
|
81
|
+
ruby,
|
|
82
|
+
set!(&mut *rb_self.set.borrow_mut(), |set| set.insert(element)),
|
|
83
|
+
)?;
|
|
84
|
+
rb_self.names.borrow_mut().insert(element, key);
|
|
85
|
+
Ok(())
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fn includes(&self, key: String) -> bool {
|
|
89
|
+
let element = hash64(key.as_bytes());
|
|
90
|
+
set!(&*self.set.borrow(), |set| set.contains(&element))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The keys in the set, in no particular order.
|
|
94
|
+
fn members(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
95
|
+
let elements = set!(&*rb_self.set.borrow(), |set| set
|
|
96
|
+
.iter()
|
|
97
|
+
.collect::<Vec<u64>>());
|
|
98
|
+
names::members(ruby, &rb_self.names.borrow(), &elements)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn size(&self) -> usize {
|
|
102
|
+
set!(&*self.set.borrow(), |set| set.len())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn is_empty(&self) -> bool {
|
|
106
|
+
set!(&*self.set.borrow(), |set| set.is_empty())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
|
|
110
|
+
let source = other.set.borrow().clone();
|
|
111
|
+
let names = other.names.borrow().clone();
|
|
112
|
+
let Some(folded) = rb_self.set.borrow_mut().absorb(&source) else {
|
|
113
|
+
return Err(mismatch(ruby, "set"));
|
|
114
|
+
};
|
|
115
|
+
rb_self.names.borrow_mut().extend(names);
|
|
116
|
+
rb_self.prune();
|
|
117
|
+
capacity(ruby, folded)?;
|
|
118
|
+
Ok(rb_self)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// The core frame followed by the names of the keys it holds.
|
|
122
|
+
///
|
|
123
|
+
/// ```text
|
|
124
|
+
/// core frame GSet::<u64, capacity>::ENCODED_LEN bytes, tag 0x81
|
|
125
|
+
/// name record u64 little-endian byte length, then that many UTF-8 bytes
|
|
126
|
+
/// ```
|
|
127
|
+
///
|
|
128
|
+
/// One record per key, in the core frame's order, filling the rest of the
|
|
129
|
+
/// blob. The frame holds 64-bit hashes, so [`load`](Self::load) rehashes
|
|
130
|
+
/// each name to rebuild what [`members`](Self::members) reads.
|
|
131
|
+
fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
132
|
+
let (frame, elements) = set!(&*rb_self.set.borrow(), |set| (
|
|
133
|
+
set.encoded(),
|
|
134
|
+
set.iter().collect::<Vec<u64>>()
|
|
135
|
+
));
|
|
136
|
+
let mut blob = Vec::from(frame);
|
|
137
|
+
names::write(&mut blob, &rb_self.names.borrow(), elements.into_iter());
|
|
138
|
+
ruby.str_from_slice(&blob)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fn inspect(&self) -> String {
|
|
142
|
+
format!(
|
|
143
|
+
"#<MergeMachines::GSet capacity={} size={}>",
|
|
144
|
+
self.capacity(),
|
|
145
|
+
self.size()
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// Leaves a name for every key the core holds and for nothing else.
|
|
150
|
+
///
|
|
151
|
+
/// A merge extends the table before the core reports whether everything
|
|
152
|
+
/// fit, because a merge that overran capacity still took whatever it had
|
|
153
|
+
/// room for and those keys need their names. Reconciling afterwards keeps
|
|
154
|
+
/// the names of the keys that landed and drops the ones that did not.
|
|
155
|
+
fn prune(&self) {
|
|
156
|
+
let set = self.set.borrow();
|
|
157
|
+
self.names
|
|
158
|
+
.borrow_mut()
|
|
159
|
+
.retain(|element, _| set!(&*set, |held| held.contains(element)));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// The variant for `capacity`, read from `blob` when there is one.
|
|
164
|
+
fn dispatch(ruby: &Ruby, capacity: usize, blob: Option<&[u8]>) -> Result<Set, Error> {
|
|
165
|
+
let Some(frame) = blob else {
|
|
166
|
+
return Set::blank(capacity)
|
|
167
|
+
.ok_or_else(|| unsupported(ruby, "capacity", capacity, &CAPACITIES));
|
|
168
|
+
};
|
|
169
|
+
Set::decoded(ruby, capacity, frame)
|
|
170
|
+
.unwrap_or_else(|| Err(unsupported_blob(ruby, "capacity", &CAPACITIES)))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
174
|
+
let class = module.define_class("GSet", ruby.class_object())?;
|
|
175
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
176
|
+
class.define_singleton_method("load", function!(Wrapper::load, 1))?;
|
|
177
|
+
class.define_method("capacity", method!(Wrapper::capacity, 0))?;
|
|
178
|
+
class.define_method("add", method!(Wrapper::add, 1))?;
|
|
179
|
+
class.define_method("include?", method!(Wrapper::includes, 1))?;
|
|
180
|
+
class.define_method("members", method!(Wrapper::members, 0))?;
|
|
181
|
+
class.define_method("size", method!(Wrapper::size, 0))?;
|
|
182
|
+
class.define_method("empty?", method!(Wrapper::is_empty, 0))?;
|
|
183
|
+
class.define_method("merge", method!(Wrapper::merge, 1))?;
|
|
184
|
+
class.define_method("dump", method!(Wrapper::dump, 0))?;
|
|
185
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
186
|
+
Ok(())
|
|
187
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//! The binding's only hash. The core's elements are `Copy + Eq`, so a Ruby
|
|
2
|
+
//! string crosses as a 64-bit hash of its UTF-8 bytes.
|
|
3
|
+
|
|
4
|
+
use crate::{FIELD_LEN, field};
|
|
5
|
+
|
|
6
|
+
const PRIME: u64 = 0x9E37_79B9_7F4A_7C15;
|
|
7
|
+
|
|
8
|
+
/// `splitmix64`'s finalizer: a bijection on [`u64`].
|
|
9
|
+
const fn mix64(value: u64) -> u64 {
|
|
10
|
+
let mut z = value.wrapping_add(PRIME);
|
|
11
|
+
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
|
12
|
+
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
|
13
|
+
z ^ (z >> 31)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Hashes bytes, folding [`FIELD_LEN`] at a time through [`mix64`], then the
|
|
17
|
+
/// remainder and the length separately.
|
|
18
|
+
pub fn hash64(bytes: &[u8]) -> u64 {
|
|
19
|
+
let (lanes, tail) = bytes.as_chunks::<FIELD_LEN>();
|
|
20
|
+
let mut accumulator = PRIME;
|
|
21
|
+
|
|
22
|
+
for lane in lanes {
|
|
23
|
+
accumulator = (accumulator ^ mix64(u64::from_le_bytes(*lane)))
|
|
24
|
+
.rotate_left(27)
|
|
25
|
+
.wrapping_mul(PRIME);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let mut last = [0_u8; FIELD_LEN];
|
|
29
|
+
last[..tail.len()].copy_from_slice(tail);
|
|
30
|
+
let length = u64::from_le_bytes(field(bytes.len()));
|
|
31
|
+
|
|
32
|
+
mix64(mix64(accumulator ^ length.wrapping_mul(PRIME)) ^ mix64(u64::from_le_bytes(last)))
|
|
33
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
//! `MergeMachines::HybridClock`: timestamps near physical time that still
|
|
2
|
+
//! order.
|
|
3
|
+
|
|
4
|
+
use std::cell::RefCell;
|
|
5
|
+
|
|
6
|
+
use bytes::Bytes;
|
|
7
|
+
use magnus::scan_args::{get_kwargs, scan_args};
|
|
8
|
+
use magnus::{Error, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
|
|
9
|
+
use merge_machines::{HybridClock, ReplicaId};
|
|
10
|
+
|
|
11
|
+
use crate::data::{stamped, timestamp};
|
|
12
|
+
use crate::{
|
|
13
|
+
Whole, codec_error, exhausted, optional_kwarg, required_kwarg, trailing_replica, wall,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/// A clock and the replica whose events it stamps, which the core does not
|
|
17
|
+
/// hold.
|
|
18
|
+
#[magnus::wrap(class = "MergeMachines::HybridClock", free_immediately, size)]
|
|
19
|
+
struct Wrapper {
|
|
20
|
+
clock: RefCell<HybridClock>,
|
|
21
|
+
replica: ReplicaId,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Wrapper {
|
|
25
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
26
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
27
|
+
let Whole(replica): Whole<ReplicaId> = required_kwarg(args.keywords, "replica")?;
|
|
28
|
+
Ok(Self {
|
|
29
|
+
clock: RefCell::new(HybridClock::new()),
|
|
30
|
+
replica,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Reads a blob written by [`dump`](Self::dump).
|
|
35
|
+
fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
|
|
36
|
+
let mut clock = HybridClock::new();
|
|
37
|
+
clock
|
|
38
|
+
.decode_into(&blob)
|
|
39
|
+
.map_err(|error| codec_error(ruby, error))?;
|
|
40
|
+
|
|
41
|
+
Ok(Self {
|
|
42
|
+
clock: RefCell::new(clock),
|
|
43
|
+
replica: trailing_replica(ruby, &blob, HybridClock::ENCODED_LEN)?,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fn replica(&self) -> ReplicaId {
|
|
48
|
+
self.replica
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// The last timestamp this clock issued.
|
|
52
|
+
fn now(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
|
|
53
|
+
timestamp(ruby, rb_self.clock.borrow().last())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Stamps a local event at `wall_ns:`, defaulting to the realtime clock.
|
|
57
|
+
fn tick(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
58
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
59
|
+
let wall_ns = match optional_kwarg::<Whole<u64>>(args.keywords, "wall_ns")? {
|
|
60
|
+
Some(Whole(nanoseconds)) => nanoseconds,
|
|
61
|
+
None => wall::now_ns(ruby)?,
|
|
62
|
+
};
|
|
63
|
+
let issued = rb_self.clock.borrow_mut().tick(wall_ns);
|
|
64
|
+
timestamp(ruby, exhausted(ruby, issued)?)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Stamps the receipt of the timestamp `wall_ns:` and `counter:` name, at
|
|
68
|
+
/// the realtime clock's reading.
|
|
69
|
+
fn observe(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<Value, Error> {
|
|
70
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
71
|
+
let (Whole(wall_ns), Whole(counter)) = get_kwargs::<_, (Whole<u64>, Whole<u64>), (), ()>(
|
|
72
|
+
args.keywords,
|
|
73
|
+
&["wall_ns", "counter"],
|
|
74
|
+
&[],
|
|
75
|
+
)?
|
|
76
|
+
.required;
|
|
77
|
+
let now = wall::now_ns(ruby)?;
|
|
78
|
+
let observed = stamped(wall_ns, counter);
|
|
79
|
+
let issued = rb_self.clock.borrow_mut().observe(now, observed);
|
|
80
|
+
timestamp(ruby, exhausted(ruby, issued)?)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// The core frame followed by the replica id, 64-bit little-endian.
|
|
84
|
+
fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
85
|
+
let mut blob = Vec::from(rb_self.clock.borrow().encoded());
|
|
86
|
+
blob.extend_from_slice(&rb_self.replica.to_le_bytes());
|
|
87
|
+
ruby.str_from_slice(&blob)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn inspect(&self) -> String {
|
|
91
|
+
let last = self.clock.borrow().last();
|
|
92
|
+
format!(
|
|
93
|
+
"#<MergeMachines::HybridClock replica={} wall_ns={} counter={}>",
|
|
94
|
+
self.replica, last.wall_ns, last.counter
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
100
|
+
let class = module.define_class("HybridClock", ruby.class_object())?;
|
|
101
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
102
|
+
class.define_singleton_method("load", function!(Wrapper::load, 1))?;
|
|
103
|
+
class.define_method("replica", method!(Wrapper::replica, 0))?;
|
|
104
|
+
class.define_method("now", method!(Wrapper::now, 0))?;
|
|
105
|
+
class.define_method("tick", method!(Wrapper::tick, -1))?;
|
|
106
|
+
class.define_method("observe", method!(Wrapper::observe, -1))?;
|
|
107
|
+
class.define_method("dump", method!(Wrapper::dump, 0))?;
|
|
108
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
109
|
+
Ok(())
|
|
110
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
//! Ruby bindings for the `merge-machines` replicated states.
|
|
2
|
+
//!
|
|
3
|
+
//! Replica counts and set capacities are const generics, so every class
|
|
4
|
+
//! dispatches the number it is given to a fixed set of monomorphised variants
|
|
5
|
+
//! and raises `ArgumentError` for any other number.
|
|
6
|
+
//!
|
|
7
|
+
//! Wall-clock readings are nanoseconds on both sides of this boundary.
|
|
8
|
+
//! `NS_PER_SECOND` is exposed for callers holding seconds.
|
|
9
|
+
|
|
10
|
+
#![allow(clippy::needless_pass_by_value)]
|
|
11
|
+
|
|
12
|
+
mod data;
|
|
13
|
+
mod gcounter;
|
|
14
|
+
mod gset;
|
|
15
|
+
mod hash;
|
|
16
|
+
mod hybrid;
|
|
17
|
+
mod logical;
|
|
18
|
+
mod lww;
|
|
19
|
+
mod names;
|
|
20
|
+
mod orset;
|
|
21
|
+
mod pncounter;
|
|
22
|
+
mod shape;
|
|
23
|
+
mod wall;
|
|
24
|
+
|
|
25
|
+
use std::sync::OnceLock;
|
|
26
|
+
|
|
27
|
+
use magnus::scan_args::get_kwargs;
|
|
28
|
+
use magnus::value::Opaque;
|
|
29
|
+
use magnus::{Error, ExceptionClass, Integer, RHash, RModule, Ruby, TryConvert, Value, prelude::*};
|
|
30
|
+
use merge_machines::{CapacityError, ClockExhausted, CodecError, ReplicaId};
|
|
31
|
+
|
|
32
|
+
/// `MergeMachines::Full`, defined in [`init`].
|
|
33
|
+
static FULL: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
|
|
34
|
+
|
|
35
|
+
/// `MergeMachines::Exhausted`, defined in [`init`].
|
|
36
|
+
static EXHAUSTED: OnceLock<Opaque<ExceptionClass>> = OnceLock::new();
|
|
37
|
+
|
|
38
|
+
/// Nanoseconds in a second.
|
|
39
|
+
pub(crate) const NS_PER_SECOND: u32 = 1_000_000_000;
|
|
40
|
+
|
|
41
|
+
/// Replica counts the counters are monomorphised for.
|
|
42
|
+
pub(crate) const REPLICAS: [usize; 4] = [4, 8, 16, 32];
|
|
43
|
+
|
|
44
|
+
/// Capacities the sets are monomorphised for.
|
|
45
|
+
pub(crate) const CAPACITIES: [usize; 3] = [64, 256, 1024];
|
|
46
|
+
|
|
47
|
+
/// Where a core frame names the shape it was written at.
|
|
48
|
+
pub(crate) const SHAPE: core::ops::Range<usize> = 1..3;
|
|
49
|
+
|
|
50
|
+
/// Bytes a 64-bit field occupies in a blob.
|
|
51
|
+
pub(crate) const FIELD_LEN: usize = 8;
|
|
52
|
+
|
|
53
|
+
/// The low [`FIELD_LEN`] bytes of `value`, little-endian.
|
|
54
|
+
pub(crate) fn field(value: usize) -> [u8; FIELD_LEN] {
|
|
55
|
+
let mut bytes = [0_u8; FIELD_LEN];
|
|
56
|
+
for (slot, byte) in bytes.iter_mut().zip(value.to_le_bytes()) {
|
|
57
|
+
*slot = byte;
|
|
58
|
+
}
|
|
59
|
+
bytes
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// A whole number of things: a stamp, a replica, a capacity, a counter, a
|
|
63
|
+
/// nanosecond reading.
|
|
64
|
+
///
|
|
65
|
+
/// magnus reads an integer through Ruby's `NUM2*` family, which truncates a
|
|
66
|
+
/// `Float` towards zero instead of refusing it, so an argument that means an
|
|
67
|
+
/// integer is read as one of these and anything but an `Integer` is turned
|
|
68
|
+
/// away. An `Integer` outside the target width still comes back as the
|
|
69
|
+
/// `RangeError` magnus raises for it.
|
|
70
|
+
pub(crate) struct Whole<T>(pub(crate) T);
|
|
71
|
+
|
|
72
|
+
impl<T: TryConvert> TryConvert for Whole<T> {
|
|
73
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
74
|
+
if Integer::from_value(value).is_none() {
|
|
75
|
+
return Err(not_whole(value));
|
|
76
|
+
}
|
|
77
|
+
T::try_convert(value).map(Self)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn not_whole(value: Value) -> Error {
|
|
82
|
+
let ruby = Ruby::get_with(value);
|
|
83
|
+
Error::new(
|
|
84
|
+
ruby.exception_type_error(),
|
|
85
|
+
format!(
|
|
86
|
+
"no implicit conversion of {} into Integer: {}",
|
|
87
|
+
value.class(),
|
|
88
|
+
value.inspect()
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The one required keyword of a constructor.
|
|
94
|
+
pub(crate) fn required_kwarg<T: TryConvert>(keywords: RHash, name: &str) -> Result<T, Error> {
|
|
95
|
+
Ok(get_kwargs::<_, (T,), (), ()>(keywords, &[name], &[])?
|
|
96
|
+
.required
|
|
97
|
+
.0)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// A keyword with a default the caller may leave out.
|
|
101
|
+
pub(crate) fn optional_kwarg<T: TryConvert>(
|
|
102
|
+
keywords: RHash,
|
|
103
|
+
name: &str,
|
|
104
|
+
) -> Result<Option<T>, Error> {
|
|
105
|
+
Ok(
|
|
106
|
+
get_kwargs::<_, (), (Option<T>,), ()>(keywords, &[], &[name])?
|
|
107
|
+
.optional
|
|
108
|
+
.0,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/// The shape a blob was written at, read from its core frame.
|
|
113
|
+
pub(crate) fn blob_shape(ruby: &Ruby, blob: &[u8]) -> Result<usize, Error> {
|
|
114
|
+
let Some(field) = blob.get(SHAPE) else {
|
|
115
|
+
return Err(codec_error(ruby, CodecError::Truncated));
|
|
116
|
+
};
|
|
117
|
+
Ok(usize::from(u16::from_le_bytes([field[0], field[1]])))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// The replica id a clock's blob carries after its core frame.
|
|
121
|
+
pub(crate) fn trailing_replica(
|
|
122
|
+
ruby: &Ruby,
|
|
123
|
+
blob: &[u8],
|
|
124
|
+
frame_len: usize,
|
|
125
|
+
) -> Result<ReplicaId, Error> {
|
|
126
|
+
let Some((field, _)) = blob
|
|
127
|
+
.get(frame_len..)
|
|
128
|
+
.and_then(<[u8]>::split_first_chunk::<FIELD_LEN>)
|
|
129
|
+
else {
|
|
130
|
+
return Err(codec_error(ruby, CodecError::Truncated));
|
|
131
|
+
};
|
|
132
|
+
Ok(u64::from_le_bytes(*field))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pub(crate) fn unsupported(ruby: &Ruby, name: &str, given: usize, supported: &[usize]) -> Error {
|
|
136
|
+
Error::new(
|
|
137
|
+
ruby.exception_arg_error(),
|
|
138
|
+
format!("{name} {given} is not supported, expected one of {supported:?}"),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pub(crate) fn unsupported_blob(ruby: &Ruby, name: &str, supported: &[usize]) -> Error {
|
|
143
|
+
Error::new(
|
|
144
|
+
ruby.exception_arg_error(),
|
|
145
|
+
format!("blob was written with a {name} outside {supported:?}"),
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub(crate) fn codec_error(ruby: &Ruby, error: CodecError) -> Error {
|
|
150
|
+
Error::new(ruby.exception_arg_error(), format!("codec: {error}"))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
pub(crate) fn mismatch(ruby: &Ruby, name: &str) -> Error {
|
|
154
|
+
Error::new(
|
|
155
|
+
ruby.exception_arg_error(),
|
|
156
|
+
format!("the other {name} has a different shape"),
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pub(crate) fn uninitialised(ruby: &Ruby) -> Error {
|
|
161
|
+
Error::new(
|
|
162
|
+
ruby.exception_runtime_error(),
|
|
163
|
+
"the merge_machines extension is not initialised",
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pub(crate) fn defined_twice(ruby: &Ruby, name: &str) -> Error {
|
|
168
|
+
Error::new(
|
|
169
|
+
ruby.exception_runtime_error(),
|
|
170
|
+
format!("MergeMachines::{name} was initialised twice"),
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Raises `MergeMachines::Full` for a state that ran out of slots.
|
|
175
|
+
pub(crate) fn capacity(ruby: &Ruby, outcome: Result<(), CapacityError>) -> Result<(), Error> {
|
|
176
|
+
outcome.map_err(|error| raised(ruby, &FULL, error.to_string()))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Raises `MergeMachines::Exhausted` for a clock that ran out of timestamps.
|
|
180
|
+
pub(crate) fn exhausted<T>(ruby: &Ruby, outcome: Result<T, ClockExhausted>) -> Result<T, Error> {
|
|
181
|
+
outcome.map_err(|error| raised(ruby, &EXHAUSTED, error.to_string()))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Raises `MergeMachines::Exhausted` for a set that ran out of tags.
|
|
185
|
+
pub(crate) fn tags_exhausted(ruby: &Ruby) -> Error {
|
|
186
|
+
raised(ruby, &EXHAUSTED, "no tag left".to_owned())
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fn raised(ruby: &Ruby, class: &OnceLock<Opaque<ExceptionClass>>, message: String) -> Error {
|
|
190
|
+
class.get().map_or_else(
|
|
191
|
+
|| uninitialised(ruby),
|
|
192
|
+
|class| Error::new(ruby.get_inner(*class), message),
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn define_errors(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
197
|
+
let base = module.define_error("Error", ruby.exception_standard_error())?;
|
|
198
|
+
let full = module.define_error("Full", base)?;
|
|
199
|
+
FULL.set(full.into())
|
|
200
|
+
.map_err(|_| defined_twice(ruby, "Full"))?;
|
|
201
|
+
let exhausted = module.define_error("Exhausted", base)?;
|
|
202
|
+
EXHAUSTED
|
|
203
|
+
.set(exhausted.into())
|
|
204
|
+
.map_err(|_| defined_twice(ruby, "Exhausted"))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[magnus::init]
|
|
208
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
209
|
+
let module = ruby.define_module("MergeMachines")?;
|
|
210
|
+
module.const_set("NS_PER_SECOND", NS_PER_SECOND)?;
|
|
211
|
+
define_errors(ruby, module)?;
|
|
212
|
+
wall::define(ruby)?;
|
|
213
|
+
data::define(ruby, module)?;
|
|
214
|
+
logical::define(ruby, module)?;
|
|
215
|
+
hybrid::define(ruby, module)?;
|
|
216
|
+
gcounter::define(ruby, module)?;
|
|
217
|
+
pncounter::define(ruby, module)?;
|
|
218
|
+
lww::define(ruby, module)?;
|
|
219
|
+
gset::define(ruby, module)?;
|
|
220
|
+
orset::define(ruby, module)
|
|
221
|
+
}
|