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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +57 -0
  3. data/Cargo.lock +294 -0
  4. data/Cargo.toml +24 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +252 -0
  7. data/ext/merge_machines_native/core/Cargo.toml +24 -0
  8. data/ext/merge_machines_native/core/src/gcounter.rs +242 -0
  9. data/ext/merge_machines_native/core/src/gset.rs +181 -0
  10. data/ext/merge_machines_native/core/src/hybrid.rs +222 -0
  11. data/ext/merge_machines_native/core/src/lib.rs +221 -0
  12. data/ext/merge_machines_native/core/src/logical.rs +139 -0
  13. data/ext/merge_machines_native/core/src/lww.rs +213 -0
  14. data/ext/merge_machines_native/core/src/orset.rs +334 -0
  15. data/ext/merge_machines_native/core/src/pncounter.rs +165 -0
  16. data/ext/merge_machines_native/extconf.rb +3 -0
  17. data/ext/merge_machines_native/ffi/Cargo.toml +20 -0
  18. data/ext/merge_machines_native/ffi/extconf.rb +21 -0
  19. data/ext/merge_machines_native/ffi/src/data.rs +44 -0
  20. data/ext/merge_machines_native/ffi/src/gcounter.rs +138 -0
  21. data/ext/merge_machines_native/ffi/src/gset.rs +187 -0
  22. data/ext/merge_machines_native/ffi/src/hash.rs +33 -0
  23. data/ext/merge_machines_native/ffi/src/hybrid.rs +110 -0
  24. data/ext/merge_machines_native/ffi/src/lib.rs +221 -0
  25. data/ext/merge_machines_native/ffi/src/logical.rs +89 -0
  26. data/ext/merge_machines_native/ffi/src/lww.rs +360 -0
  27. data/ext/merge_machines_native/ffi/src/names.rs +82 -0
  28. data/ext/merge_machines_native/ffi/src/orset.rs +240 -0
  29. data/ext/merge_machines_native/ffi/src/pncounter.rs +143 -0
  30. data/ext/merge_machines_native/ffi/src/shape.rs +116 -0
  31. data/ext/merge_machines_native/ffi/src/wall.rs +37 -0
  32. data/lib/merge_machines/version.rb +5 -0
  33. data/lib/merge_machines.rb +8 -0
  34. data/sig/merge_machines.rbs +238 -0
  35. metadata +98 -0
@@ -0,0 +1,240 @@
1
+ //! `MergeMachines::OrSet`: a set elements can leave, where a concurrent add
2
+ //! survives a remove.
3
+
4
+ use std::cell::RefCell;
5
+ use std::sync::atomic::{AtomicU64, Ordering};
6
+
7
+ use bytes::Bytes;
8
+ use magnus::scan_args::scan_args;
9
+ use magnus::typed_data::Obj;
10
+ use magnus::{Error, RArray, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
11
+ use merge_machines::{OrSet, ReplicaId, Tag};
12
+
13
+ use crate::hash::hash64;
14
+ use crate::names::{self, Names};
15
+ use crate::shape::{frame_len_of, shaped};
16
+ use crate::{
17
+ CAPACITIES, Whole, blob_shape, capacity, mismatch, required_kwarg, tags_exhausted, unsupported,
18
+ unsupported_blob,
19
+ };
20
+
21
+ /// Tags minted in this process, so no two sets allocate the same one.
22
+ static MINTED: AtomicU64 = AtomicU64::new(0);
23
+
24
+ /// The set at a single capacity, over the 64-bit hash each key becomes.
25
+ type Held<const N: usize> = OrSet<u64, N>;
26
+
27
+ shaped!(Set over Held { N64 = 64, N256 = 256, N1024 = 1024 });
28
+
29
+ /// Runs `body` against whichever variant is held.
30
+ macro_rules! set {
31
+ ($set:expr, |$inner:ident| $body:expr) => {
32
+ match $set {
33
+ Set::N64($inner) => $body,
34
+ Set::N256($inner) => $body,
35
+ Set::N1024($inner) => $body,
36
+ }
37
+ };
38
+ }
39
+
40
+ /// The set holds 64-bit key hashes, and `names` holds the string each one came
41
+ /// from.
42
+ #[magnus::wrap(class = "MergeMachines::OrSet", free_immediately, size)]
43
+ struct Wrapper {
44
+ set: RefCell<Set>,
45
+ names: RefCell<Names>,
46
+ }
47
+
48
+ const fn capacity_of<const N: usize>(_: &Held<N>) -> usize {
49
+ N
50
+ }
51
+
52
+ impl Wrapper {
53
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
54
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
55
+ let Whole(capacity): Whole<usize> = required_kwarg(args.keywords, "capacity")?;
56
+ Ok(Self {
57
+ set: RefCell::new(dispatch(ruby, capacity, None)?),
58
+ names: RefCell::new(Names::new()),
59
+ })
60
+ }
61
+
62
+ /// Reads a blob written by [`dump`](Self::dump).
63
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
64
+ let set = dispatch(ruby, blob_shape(ruby, &blob)?, Some(&blob))?;
65
+ let frame_len = set!(&set, |inner| frame_len_of(&**inner));
66
+ let names = names::read(ruby, &blob[frame_len..])?;
67
+ let elements = set!(&set, |inner| inner
68
+ .adds()
69
+ .map(|add| add.element)
70
+ .collect::<Vec<u64>>());
71
+ names::complete(ruby, &names, elements.into_iter())?;
72
+
73
+ let wrapper = Self {
74
+ set: RefCell::new(set),
75
+ names: RefCell::new(names),
76
+ };
77
+ wrapper.prune();
78
+ Ok(wrapper)
79
+ }
80
+
81
+ fn capacity(&self) -> usize {
82
+ set!(&*self.set.borrow(), |set| capacity_of(&**set))
83
+ }
84
+
85
+ /// Adds `key` under a tag of `replica:` and the next counter that replica
86
+ /// has not used here.
87
+ fn add(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<(), Error> {
88
+ let args = scan_args::<(String,), (), (), (), RHash, ()>(args)?;
89
+ let (key,) = args.required;
90
+ let Whole(replica): Whole<ReplicaId> = required_kwarg(args.keywords, "replica")?;
91
+
92
+ let element = hash64(key.as_bytes());
93
+ let Some(counter) = rb_self.next_counter(replica) else {
94
+ return Err(tags_exhausted(ruby));
95
+ };
96
+ let tag = Tag::new(replica, counter);
97
+ capacity(
98
+ ruby,
99
+ set!(&mut *rb_self.set.borrow_mut(), |set| set.add(element, tag)),
100
+ )?;
101
+ rb_self.names.borrow_mut().insert(element, key);
102
+ Ok(())
103
+ }
104
+
105
+ /// Tombstones every tag this set has seen for `key`.
106
+ fn remove(ruby: &Ruby, rb_self: &Self, key: String) -> Result<(), Error> {
107
+ let element = hash64(key.as_bytes());
108
+ capacity(
109
+ ruby,
110
+ set!(&mut *rb_self.set.borrow_mut(), |set| set.remove(&element)),
111
+ )
112
+ }
113
+
114
+ fn includes(&self, key: String) -> bool {
115
+ let element = hash64(key.as_bytes());
116
+ set!(&*self.set.borrow(), |set| set.contains(&element))
117
+ }
118
+
119
+ /// The keys still in the set, each once, in no particular order.
120
+ fn members(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
121
+ let elements = set!(&*rb_self.set.borrow(), |set| set
122
+ .iter()
123
+ .collect::<Vec<u64>>());
124
+ names::members(ruby, &rb_self.names.borrow(), &elements)
125
+ }
126
+
127
+ fn size(&self) -> usize {
128
+ set!(&*self.set.borrow(), |set| set.len())
129
+ }
130
+
131
+ fn is_empty(&self) -> bool {
132
+ set!(&*self.set.borrow(), |set| set.is_empty())
133
+ }
134
+
135
+ fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
136
+ let source = other.set.borrow().clone();
137
+ let names = other.names.borrow().clone();
138
+ let Some(folded) = rb_self.set.borrow_mut().absorb(&source) else {
139
+ return Err(mismatch(ruby, "set"));
140
+ };
141
+ rb_self.names.borrow_mut().extend(names);
142
+ rb_self.prune();
143
+ capacity(ruby, folded)?;
144
+ Ok(rb_self)
145
+ }
146
+
147
+ /// The core frame followed by the names of every key it has seen added.
148
+ ///
149
+ /// ```text
150
+ /// core frame OrSet::<u64, capacity>::ENCODED_LEN bytes, tag 0x82
151
+ /// name record u64 little-endian byte length, then that many UTF-8 bytes
152
+ /// ```
153
+ ///
154
+ /// One record per add, tombstoned or not, in the core frame's order,
155
+ /// filling the rest of the blob. The frame holds 64-bit hashes, so
156
+ /// [`load`](Self::load) rehashes each name to rebuild what
157
+ /// [`members`](Self::members) reads.
158
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
159
+ let (frame, elements) = set!(&*rb_self.set.borrow(), |set| (
160
+ set.encoded(),
161
+ set.adds().map(|add| add.element).collect::<Vec<u64>>()
162
+ ));
163
+ let mut blob = Vec::from(frame);
164
+ names::write(&mut blob, &rb_self.names.borrow(), elements.into_iter());
165
+ ruby.str_from_slice(&blob)
166
+ }
167
+
168
+ fn inspect(&self) -> String {
169
+ format!(
170
+ "#<MergeMachines::OrSet capacity={} size={}>",
171
+ self.capacity(),
172
+ self.size()
173
+ )
174
+ }
175
+
176
+ /// Leaves a name for every add the core holds, tombstoned or not, and for
177
+ /// nothing else.
178
+ ///
179
+ /// A merge extends the table before the core reports whether everything
180
+ /// fit, because a merge that overran capacity still took whatever it had
181
+ /// room for and those keys need their names. Reconciling afterwards keeps
182
+ /// the names of the adds that landed and drops the ones that did not.
183
+ fn prune(&self) {
184
+ let set = self.set.borrow();
185
+ self.names.borrow_mut().retain(|element, _| {
186
+ set!(&*set, |held| held.adds().any(|add| add.element == *element))
187
+ });
188
+ }
189
+
190
+ /// A counter no tag in this process has used, and above anything this
191
+ /// replica already used in this set.
192
+ ///
193
+ /// `None` once either counter has nothing left to count: reusing a tag
194
+ /// would make the core take the add for one it already holds and drop the
195
+ /// key without a word.
196
+ fn next_counter(&self, replica: ReplicaId) -> Option<u64> {
197
+ let used = |tag: Tag| (tag.replica == replica).then_some(tag.counter);
198
+ let highest = set!(&*self.set.borrow(), |set| set
199
+ .adds()
200
+ .map(|add| add.tag)
201
+ .chain(set.removes())
202
+ .filter_map(used)
203
+ .max())
204
+ .unwrap_or(0);
205
+
206
+ let minted = MINTED
207
+ .try_update(Ordering::Relaxed, Ordering::Relaxed, |minted| {
208
+ minted.checked_add(1)
209
+ })
210
+ .ok()?;
211
+ highest.max(minted).checked_add(1)
212
+ }
213
+ }
214
+
215
+ /// The variant for `capacity`, read from `blob` when there is one.
216
+ fn dispatch(ruby: &Ruby, capacity: usize, blob: Option<&[u8]>) -> Result<Set, Error> {
217
+ let Some(frame) = blob else {
218
+ return Set::blank(capacity)
219
+ .ok_or_else(|| unsupported(ruby, "capacity", capacity, &CAPACITIES));
220
+ };
221
+ Set::decoded(ruby, capacity, frame)
222
+ .unwrap_or_else(|| Err(unsupported_blob(ruby, "capacity", &CAPACITIES)))
223
+ }
224
+
225
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
226
+ let class = module.define_class("OrSet", ruby.class_object())?;
227
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
228
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
229
+ class.define_method("capacity", method!(Wrapper::capacity, 0))?;
230
+ class.define_method("add", method!(Wrapper::add, -1))?;
231
+ class.define_method("remove", method!(Wrapper::remove, 1))?;
232
+ class.define_method("include?", method!(Wrapper::includes, 1))?;
233
+ class.define_method("members", method!(Wrapper::members, 0))?;
234
+ class.define_method("size", method!(Wrapper::size, 0))?;
235
+ class.define_method("empty?", method!(Wrapper::is_empty, 0))?;
236
+ class.define_method("merge", method!(Wrapper::merge, 1))?;
237
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
238
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
239
+ Ok(())
240
+ }
@@ -0,0 +1,143 @@
1
+ //! `MergeMachines::PNCounter`: a counter replicas can lower as well as raise.
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, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
9
+ use merge_machines::PNCounter;
10
+
11
+ use crate::gcounter::increment_args;
12
+ use crate::shape::shaped;
13
+ use crate::{
14
+ REPLICAS, Whole, blob_shape, capacity, mismatch, required_kwarg, unsupported, unsupported_blob,
15
+ };
16
+
17
+ /// The counter at a single replica count.
18
+ type Held<const R: usize> = PNCounter<R>;
19
+
20
+ shaped!(Counter over Held { R4 = 4, R8 = 8, R16 = 16, R32 = 32 });
21
+
22
+ /// Runs `body` against whichever variant is held.
23
+ macro_rules! counter {
24
+ ($counter:expr, |$inner:ident| $body:expr) => {
25
+ match $counter {
26
+ Counter::R4($inner) => $body,
27
+ Counter::R8($inner) => $body,
28
+ Counter::R16($inner) => $body,
29
+ Counter::R32($inner) => $body,
30
+ }
31
+ };
32
+ }
33
+
34
+ #[magnus::wrap(class = "MergeMachines::PNCounter", free_immediately, size)]
35
+ struct Wrapper(RefCell<Counter>);
36
+
37
+ const fn replicas_of<const R: usize>(_: &Held<R>) -> usize {
38
+ R
39
+ }
40
+
41
+ impl Wrapper {
42
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
43
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
44
+ let Whole(replicas): Whole<usize> = required_kwarg(args.keywords, "replicas")?;
45
+ Ok(Self(RefCell::new(dispatch(ruby, replicas, None)?)))
46
+ }
47
+
48
+ /// Reads a blob written by [`dump`](Self::dump).
49
+ fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
50
+ let replicas = blob_shape(ruby, &blob)?;
51
+ Ok(Self(RefCell::new(dispatch(ruby, replicas, Some(&blob))?)))
52
+ }
53
+
54
+ fn replicas(&self) -> usize {
55
+ counter!(&*self.0.borrow(), |counter| replicas_of(&**counter))
56
+ }
57
+
58
+ /// Adds `by:` to `replica:`'s own count.
59
+ fn increment(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<(), Error> {
60
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
61
+ let (replica, by) = increment_args(args.keywords)?;
62
+ capacity(
63
+ ruby,
64
+ counter!(&mut *rb_self.0.borrow_mut(), |counter| counter
65
+ .increment(replica, by)),
66
+ )
67
+ }
68
+
69
+ /// Takes `by:` away on `replica:`'s own account.
70
+ fn decrement(ruby: &Ruby, rb_self: &Self, args: &[Value]) -> Result<(), Error> {
71
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
72
+ let (replica, by) = increment_args(args.keywords)?;
73
+ capacity(
74
+ ruby,
75
+ counter!(&mut *rb_self.0.borrow_mut(), |counter| counter
76
+ .decrement(replica, by)),
77
+ )
78
+ }
79
+
80
+ /// Everything added, minus everything taken away.
81
+ fn value(&self) -> i128 {
82
+ counter!(&*self.0.borrow(), |counter| counter.value())
83
+ }
84
+
85
+ /// Everything added, by every replica.
86
+ fn added(&self) -> u128 {
87
+ counter!(&*self.0.borrow(), |counter| counter.added())
88
+ }
89
+
90
+ /// Everything taken away, by every replica.
91
+ fn removed(&self) -> u128 {
92
+ counter!(&*self.0.borrow(), |counter| counter.removed())
93
+ }
94
+
95
+ fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
96
+ let source = other.0.borrow().clone();
97
+ let Some(folded) = rb_self.0.borrow_mut().absorb(&source) else {
98
+ return Err(mismatch(ruby, "counter"));
99
+ };
100
+ capacity(ruby, folded)?;
101
+ Ok(rb_self)
102
+ }
103
+
104
+ /// The core frame, tag 0x62.
105
+ fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
106
+ let frame = counter!(&*rb_self.0.borrow(), |counter| counter.encoded());
107
+ ruby.str_from_slice(&frame)
108
+ }
109
+
110
+ fn inspect(&self) -> String {
111
+ format!(
112
+ "#<MergeMachines::PNCounter replicas={} value={}>",
113
+ self.replicas(),
114
+ self.value()
115
+ )
116
+ }
117
+ }
118
+
119
+ /// The variant for `replicas`, read from `blob` when there is one.
120
+ fn dispatch(ruby: &Ruby, replicas: usize, blob: Option<&[u8]>) -> Result<Counter, Error> {
121
+ let Some(frame) = blob else {
122
+ return Counter::blank(replicas)
123
+ .ok_or_else(|| unsupported(ruby, "replicas", replicas, &REPLICAS));
124
+ };
125
+ Counter::decoded(ruby, replicas, frame)
126
+ .unwrap_or_else(|| Err(unsupported_blob(ruby, "replica count", &REPLICAS)))
127
+ }
128
+
129
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
130
+ let class = module.define_class("PNCounter", ruby.class_object())?;
131
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
132
+ class.define_singleton_method("load", function!(Wrapper::load, 1))?;
133
+ class.define_method("replicas", method!(Wrapper::replicas, 0))?;
134
+ class.define_method("increment", method!(Wrapper::increment, -1))?;
135
+ class.define_method("decrement", method!(Wrapper::decrement, -1))?;
136
+ class.define_method("value", method!(Wrapper::value, 0))?;
137
+ class.define_method("added", method!(Wrapper::added, 0))?;
138
+ class.define_method("removed", method!(Wrapper::removed, 0))?;
139
+ class.define_method("merge", method!(Wrapper::merge, 1))?;
140
+ class.define_method("dump", method!(Wrapper::dump, 0))?;
141
+ class.define_method("inspect", method!(Wrapper::inspect, 0))?;
142
+ Ok(())
143
+ }
@@ -0,0 +1,116 @@
1
+ //! The shape a class is given, resolved to a monomorphised state.
2
+
3
+ use magnus::{Error, Ruby};
4
+ use merge_machines::CodecError;
5
+
6
+ use crate::codec_error;
7
+
8
+ /// A state at one const generic, and the core frame it encodes to.
9
+ pub trait Framed: Sized {
10
+ /// Length of the core frame this state encodes to.
11
+ const FRAME_LEN: usize;
12
+
13
+ /// The state a class starts out holding.
14
+ fn blank() -> Self;
15
+
16
+ /// Reads a core frame over a state the caller already holds.
17
+ ///
18
+ /// # Errors
19
+ ///
20
+ /// [`CodecError::Truncated`] when `frame` is too short,
21
+ /// [`CodecError::Tag`] when it holds another structure,
22
+ /// [`CodecError::Parameters`] when it was written at another shape.
23
+ fn read_frame(&mut self, frame: &[u8]) -> Result<(), CodecError>;
24
+ }
25
+
26
+ /// Length of the core frame `state` encodes to.
27
+ pub const fn frame_len_of<T: Framed>(_state: &T) -> usize {
28
+ T::FRAME_LEN
29
+ }
30
+
31
+ /// Each shape is constructed behind its own call frame.
32
+ #[inline(never)]
33
+ pub fn boxed_new<T: Framed>() -> Box<T> {
34
+ Box::new(T::blank())
35
+ }
36
+
37
+ /// Each shape is read behind its own call frame.
38
+ #[inline(never)]
39
+ pub fn boxed_decode<T: Framed>(ruby: &Ruby, frame: &[u8]) -> Result<Box<T>, Error> {
40
+ let mut state = boxed_new::<T>();
41
+ state
42
+ .read_frame(frame)
43
+ .map_err(|error| codec_error(ruby, error))?;
44
+ Ok(state)
45
+ }
46
+
47
+ /// Defines the enum a class holds: one boxed variant per shape `held` is
48
+ /// monomorphised at, where `held` is that state at a single shape, built by
49
+ /// `held::new` and read by `held::decode_into`.
50
+ ///
51
+ /// A class is a pointer wide whatever shape it was given, and every variant is
52
+ /// built and read through [`boxed_new`] and [`boxed_decode`]. A shape no
53
+ /// variant was monomorphised at, and a pairing of two different shapes, come
54
+ /// back as `None` for the class to turn into the error its own callers expect.
55
+ macro_rules! shaped {
56
+ ($name:ident over $held:ident { $($variant:ident = $shape:literal),+ $(,)? }) => {
57
+ impl<const SHAPE: usize> $crate::shape::Framed for $held<SHAPE> {
58
+ const FRAME_LEN: usize = Self::ENCODED_LEN;
59
+
60
+ fn blank() -> Self {
61
+ Self::new()
62
+ }
63
+
64
+ fn read_frame(&mut self, frame: &[u8]) -> Result<(), ::merge_machines::CodecError> {
65
+ self.decode_into(frame)
66
+ }
67
+ }
68
+
69
+ /// A state at one of the monomorphised shapes.
70
+ #[derive(Clone)]
71
+ enum $name {
72
+ $($variant(Box<$held<$shape>>),)+
73
+ }
74
+
75
+ impl $name {
76
+ /// The variant for `shape`, holding nothing, or `None` when no
77
+ /// variant was monomorphised at `shape`.
78
+ fn blank(shape: usize) -> Option<Self> {
79
+ match shape {
80
+ $($shape => Some(Self::$variant($crate::shape::boxed_new())),)+
81
+ _ => None,
82
+ }
83
+ }
84
+
85
+ /// The variant for `shape`, read from `frame`, or `None` when no
86
+ /// variant was monomorphised at `shape`.
87
+ fn decoded(
88
+ ruby: &::magnus::Ruby,
89
+ shape: usize,
90
+ frame: &[u8],
91
+ ) -> Option<Result<Self, ::magnus::Error>> {
92
+ match shape {
93
+ $($shape => Some(
94
+ $crate::shape::boxed_decode(ruby, frame).map(Self::$variant),
95
+ ),)+
96
+ _ => None,
97
+ }
98
+ }
99
+
100
+ /// Folds `other` in, or `None` when it holds another shape.
101
+ fn absorb(
102
+ &mut self,
103
+ other: &Self,
104
+ ) -> Option<Result<(), ::merge_machines::CapacityError>> {
105
+ match (self, other) {
106
+ $((Self::$variant(target), Self::$variant(source)) => Some(
107
+ <$held<$shape> as ::merge_machines::Merge>::merge(target, source),
108
+ ),)+
109
+ _ => None,
110
+ }
111
+ }
112
+ }
113
+ };
114
+ }
115
+
116
+ pub(crate) use shaped;
@@ -0,0 +1,37 @@
1
+ //! The wall-clock reading a hybrid timestamp is stamped at.
2
+ //!
3
+ //! `Process::CLOCK_REALTIME` is physical time two machines can compare. It
4
+ //! steps backwards when the host's clock is corrected, and no deadline may be
5
+ //! derived from it.
6
+
7
+ use std::sync::OnceLock;
8
+
9
+ use magnus::value::Opaque;
10
+ use magnus::{Error, RModule, Ruby, Value, prelude::*};
11
+
12
+ use crate::{defined_twice, uninitialised};
13
+
14
+ static PROCESS: OnceLock<Opaque<RModule>> = OnceLock::new();
15
+ static REALTIME: OnceLock<Opaque<Value>> = OnceLock::new();
16
+
17
+ pub fn define(ruby: &Ruby) -> Result<(), Error> {
18
+ let process: RModule = ruby.class_object().const_get("Process")?;
19
+ let realtime: Value = process.const_get("CLOCK_REALTIME")?;
20
+ PROCESS
21
+ .set(process.into())
22
+ .map_err(|_| defined_twice(ruby, "Process"))?;
23
+ REALTIME
24
+ .set(realtime.into())
25
+ .map_err(|_| defined_twice(ruby, "Process::CLOCK_REALTIME"))
26
+ }
27
+
28
+ /// `Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)`.
29
+ pub fn now_ns(ruby: &Ruby) -> Result<u64, Error> {
30
+ let (Some(process), Some(realtime)) = (PROCESS.get(), REALTIME.get()) else {
31
+ return Err(uninitialised(ruby));
32
+ };
33
+ ruby.get_inner(*process).funcall(
34
+ "clock_gettime",
35
+ (ruby.get_inner(*realtime), ruby.to_symbol("nanosecond")),
36
+ )
37
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MergeMachines
4
+ VERSION = '0.2.0'
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'merge_machines/version'
4
+
5
+ # GCounter, PNCounter, LwwRegister, GSet, OrSet, LogicalClock and HybridClock
6
+ # are defined by the native extension; there is no pure-Ruby implementation to
7
+ # fall back to.
8
+ require 'merge_machines_native/merge_machines_native'