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,334 @@
1
+ //! Observed-remove set: a remove only takes out the adds it has seen.
2
+
3
+ #[cfg(feature = "alloc")]
4
+ use crate::boxed_frame;
5
+ use crate::{
6
+ COUNT_LEN, CapacityError, CodecError, FIELD_LEN, Merge, ReplicaId, SHAPE_LEN, TAG, TAG_LEN,
7
+ TAG_ORSET, free_slot, read_small, read_u64, same_elements, small_field, tagged_frame,
8
+ write_into,
9
+ };
10
+
11
+ /// Bytes one tombstone record occupies: the tag's replica then its counter.
12
+ const TOMBSTONE_LEN: usize = 2 * FIELD_LEN;
13
+
14
+ /// Bytes one add record occupies: the element then the tag it came under.
15
+ const ADD_LEN: usize = FIELD_LEN + TOMBSTONE_LEN;
16
+
17
+ /// Where the element sits in an add record.
18
+ const ELEMENT: core::ops::Range<usize> = 0..FIELD_LEN;
19
+
20
+ /// Where the tag sits in an add record.
21
+ const TAGGED: core::ops::Range<usize> = FIELD_LEN..ADD_LEN;
22
+
23
+ /// Where the tag's replica sits in a tombstone record.
24
+ const REPLICA: core::ops::Range<usize> = 0..FIELD_LEN;
25
+
26
+ /// Where the tag's counter sits in a tombstone record.
27
+ const COUNTER: core::ops::Range<usize> = FIELD_LEN..TOMBSTONE_LEN;
28
+
29
+ /// Where the shape sits in a frame.
30
+ const SHAPE: core::ops::Range<usize> = TAG_LEN..TAG_LEN + SHAPE_LEN;
31
+
32
+ /// Where the number of records carrying an add sits in a frame.
33
+ const ADDS_HELD: core::ops::Range<usize> = SHAPE.end..SHAPE.end + COUNT_LEN;
34
+
35
+ /// Where the number of records carrying a tombstone sits in a frame.
36
+ const REMOVES_HELD: core::ops::Range<usize> = ADDS_HELD.end..ADDS_HELD.end + COUNT_LEN;
37
+
38
+ /// Where the add records start in a frame.
39
+ const ADDS: usize = REMOVES_HELD.end;
40
+
41
+ /// Which add a remove is talking about.
42
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
43
+ pub struct Tag {
44
+ /// The replica that made the add.
45
+ pub replica: ReplicaId,
46
+ /// That replica's counter when it made the add.
47
+ pub counter: u64,
48
+ }
49
+
50
+ impl Tag {
51
+ #[must_use]
52
+ pub const fn new(replica: ReplicaId, counter: u64) -> Self {
53
+ Self { replica, counter }
54
+ }
55
+ }
56
+
57
+ /// An element and the tag it was added under.
58
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
59
+ pub struct Tagged<T> {
60
+ /// The element added.
61
+ pub element: T,
62
+ /// The tag that add was made under.
63
+ pub tag: Tag,
64
+ }
65
+
66
+ /// A set elements can leave, where concurrent adds survive a remove.
67
+ ///
68
+ /// ```
69
+ /// use merge_machines::{Merge, OrSet, Tag};
70
+ ///
71
+ /// let mut left = OrSet::<u16, 8>::new();
72
+ /// assert_eq!(left.add(1, Tag::new(1, 1)), Ok(()));
73
+ /// let mut right = left;
74
+ ///
75
+ /// // One side removes what it has seen while the other adds the same element
76
+ /// // again under its own tag.
77
+ /// assert_eq!(left.remove(&1), Ok(()));
78
+ /// assert_eq!(right.add(1, Tag::new(2, 1)), Ok(()));
79
+ ///
80
+ /// assert_eq!(left.merge(&right), Ok(()));
81
+ /// assert!(left.contains(&1));
82
+ /// ```
83
+ #[derive(Debug, Clone, Copy, Eq)]
84
+ pub struct OrSet<T, const N: usize> {
85
+ adds: [Option<Tagged<T>>; N],
86
+ removes: [Option<Tag>; N],
87
+ }
88
+
89
+ impl<T: Copy + PartialEq, const N: usize> OrSet<T, N> {
90
+ #[must_use]
91
+ pub const fn new() -> Self {
92
+ Self {
93
+ adds: [const { None }; N],
94
+ removes: [None; N],
95
+ }
96
+ }
97
+
98
+ /// Records `element` added under `tag`.
99
+ ///
100
+ /// # Errors
101
+ ///
102
+ /// Returns [`CapacityError`] when `tag` is new and all `N` add slots are
103
+ /// taken.
104
+ pub fn add(&mut self, element: T, tag: Tag) -> Result<(), CapacityError> {
105
+ if self.added(tag) {
106
+ return Ok(());
107
+ }
108
+ let free = free_slot(&self.adds).ok_or(CapacityError)?;
109
+ self.adds[free] = Some(Tagged { element, tag });
110
+ Ok(())
111
+ }
112
+
113
+ /// Tombstones every tag this replica has seen for `element`.
114
+ ///
115
+ /// # Errors
116
+ ///
117
+ /// Returns [`CapacityError`] when the tombstones do not fit in `N` slots.
118
+ pub fn remove(&mut self, element: &T) -> Result<(), CapacityError> {
119
+ let mut cursor = 0;
120
+ while cursor < N {
121
+ if let Some(add) = self.adds[cursor]
122
+ && add.element == *element
123
+ {
124
+ self.tombstone(add.tag)?;
125
+ }
126
+ cursor += 1;
127
+ }
128
+ Ok(())
129
+ }
130
+
131
+ #[must_use]
132
+ pub fn contains(&self, element: &T) -> bool {
133
+ self.adds
134
+ .iter()
135
+ .flatten()
136
+ .any(|add| add.element == *element && !self.removed(add.tag))
137
+ }
138
+
139
+ /// How many distinct elements are in the set.
140
+ #[must_use]
141
+ pub fn len(&self) -> usize {
142
+ self.iter().count()
143
+ }
144
+
145
+ #[must_use]
146
+ pub fn is_empty(&self) -> bool {
147
+ self.len() == 0
148
+ }
149
+
150
+ /// The elements still in the set, each once, in no particular order.
151
+ pub fn iter(&self) -> impl Iterator<Item = T> + '_ {
152
+ self.adds
153
+ .iter()
154
+ .enumerate()
155
+ .filter_map(move |(index, slot)| {
156
+ let add = (*slot)?;
157
+ if self.removed(add.tag) {
158
+ return None;
159
+ }
160
+ let earlier = self.adds[..index]
161
+ .iter()
162
+ .flatten()
163
+ .any(|other| other.element == add.element && !self.removed(other.tag));
164
+ if earlier { None } else { Some(add.element) }
165
+ })
166
+ }
167
+
168
+ /// Every add this replica has seen, tombstoned or not.
169
+ pub fn adds(&self) -> impl Iterator<Item = Tagged<T>> + '_ {
170
+ self.adds.iter().flatten().copied()
171
+ }
172
+
173
+ /// Every tag this replica has tombstoned.
174
+ pub fn removes(&self) -> impl Iterator<Item = Tag> + '_ {
175
+ self.removes.iter().flatten().copied()
176
+ }
177
+
178
+ fn added(&self, tag: Tag) -> bool {
179
+ self.adds.iter().flatten().any(|add| add.tag == tag)
180
+ }
181
+
182
+ fn removed(&self, tag: Tag) -> bool {
183
+ self.removes.iter().flatten().any(|held| *held == tag)
184
+ }
185
+
186
+ fn tombstone(&mut self, tag: Tag) -> Result<(), CapacityError> {
187
+ if self.removed(tag) {
188
+ return Ok(());
189
+ }
190
+ let free = free_slot(&self.removes).ok_or(CapacityError)?;
191
+ self.removes[free] = Some(tag);
192
+ Ok(())
193
+ }
194
+ }
195
+
196
+ impl<const N: usize> OrSet<u64, N> {
197
+ /// Where the tombstone records start in a frame.
198
+ const REMOVES: usize = ADDS + N * ADD_LEN;
199
+
200
+ /// Length of the [`encode`](Self::encode) frame.
201
+ pub const ENCODED_LEN: usize = Self::REMOVES + N * TOMBSTONE_LEN;
202
+
203
+ /// Writes the set into `out`, returning the number of bytes written.
204
+ ///
205
+ /// # Errors
206
+ ///
207
+ /// [`CodecError::Truncated`] when `out` is shorter than
208
+ /// [`ENCODED_LEN`](Self::ENCODED_LEN).
209
+ pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
210
+ write_into(out, Self::ENCODED_LEN, |frame| self.write_frame(frame))
211
+ }
212
+
213
+ /// The set's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
214
+ #[cfg(feature = "alloc")]
215
+ #[must_use]
216
+ pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
217
+ boxed_frame(Self::ENCODED_LEN, |frame| self.write_frame(frame))
218
+ }
219
+
220
+ /// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
221
+ fn write_frame(&self, frame: &mut [u8]) {
222
+ frame.fill(0);
223
+ frame[TAG] = TAG_ORSET;
224
+ frame[SHAPE].copy_from_slice(&small_field(N));
225
+ frame[ADDS_HELD].copy_from_slice(&small_field(self.adds().count()));
226
+ frame[REMOVES_HELD].copy_from_slice(&small_field(self.removes().count()));
227
+
228
+ let (records, _) = frame[ADDS..Self::REMOVES].as_chunks_mut::<ADD_LEN>();
229
+ for (record, add) in records.iter_mut().zip(self.adds()) {
230
+ record[ELEMENT].copy_from_slice(&add.element.to_le_bytes());
231
+ write_tag(add.tag, &mut record[TAGGED]);
232
+ }
233
+
234
+ let (records, _) = frame[Self::REMOVES..].as_chunks_mut::<TOMBSTONE_LEN>();
235
+ for (record, tag) in records.iter_mut().zip(self.removes()) {
236
+ write_tag(tag, record);
237
+ }
238
+ }
239
+
240
+ /// Reads a set written by [`encode`](Self::encode).
241
+ ///
242
+ /// # Errors
243
+ ///
244
+ /// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
245
+ /// when it holds another structure, [`CodecError::Parameters`] when it was
246
+ /// written with another capacity.
247
+ pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
248
+ let mut set = Self::new();
249
+ set.decode_into(bytes)?;
250
+ Ok(set)
251
+ }
252
+
253
+ /// Reads a frame over a set the caller already holds.
254
+ ///
255
+ /// The set is untouched when the frame is rejected.
256
+ ///
257
+ /// # Errors
258
+ ///
259
+ /// The same as [`decode`](Self::decode).
260
+ pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
261
+ let frame = tagged_frame(bytes, Self::ENCODED_LEN, TAG_ORSET)?;
262
+ if read_small(&frame[SHAPE]) != N {
263
+ return Err(CodecError::Parameters);
264
+ }
265
+
266
+ let adds_held = read_small(&frame[ADDS_HELD]);
267
+ let removes_held = read_small(&frame[REMOVES_HELD]);
268
+
269
+ let (records, _) = frame[ADDS..Self::REMOVES].as_chunks::<ADD_LEN>();
270
+ self.adds = [const { None }; N];
271
+ for (slot, record) in self.adds.iter_mut().zip(records.iter().take(adds_held)) {
272
+ *slot = Some(Tagged {
273
+ element: read_u64(&record[ELEMENT]),
274
+ tag: read_tag(&record[TAGGED]),
275
+ });
276
+ }
277
+
278
+ let (records, _) = frame[Self::REMOVES..].as_chunks::<TOMBSTONE_LEN>();
279
+ self.removes = [None; N];
280
+ for (slot, record) in self
281
+ .removes
282
+ .iter_mut()
283
+ .zip(records.iter().take(removes_held))
284
+ {
285
+ *slot = Some(read_tag(record));
286
+ }
287
+ Ok(())
288
+ }
289
+ }
290
+
291
+ /// Writes `tag` into `field`, which is [`TOMBSTONE_LEN`] long.
292
+ fn write_tag(tag: Tag, field: &mut [u8]) {
293
+ field[REPLICA].copy_from_slice(&tag.replica.to_le_bytes());
294
+ field[COUNTER].copy_from_slice(&tag.counter.to_le_bytes());
295
+ }
296
+
297
+ /// Reads a tag from `field`, which is [`TOMBSTONE_LEN`] long.
298
+ fn read_tag(field: &[u8]) -> Tag {
299
+ Tag {
300
+ replica: read_u64(&field[REPLICA]),
301
+ counter: read_u64(&field[COUNTER]),
302
+ }
303
+ }
304
+
305
+ impl<T: Copy + PartialEq, const N: usize> Merge for OrSet<T, N> {
306
+ type Error = CapacityError;
307
+
308
+ fn merge(&mut self, other: &Self) -> Result<(), Self::Error> {
309
+ for add in other.adds() {
310
+ self.add(add.element, add.tag)?;
311
+ }
312
+ for tag in other.removes() {
313
+ self.tombstone(tag)?;
314
+ }
315
+ Ok(())
316
+ }
317
+ }
318
+
319
+ /// Two sets are equal when they have seen the same adds and the same
320
+ /// tombstones, whatever order the slots were filled in.
321
+ impl<T: PartialEq, const N: usize> PartialEq for OrSet<T, N> {
322
+ fn eq(&self, other: &Self) -> bool {
323
+ same_elements(&self.adds, &other.adds) && same_elements(&self.removes, &other.removes)
324
+ }
325
+ }
326
+
327
+ impl<T: Copy + PartialEq, const N: usize> Default for OrSet<T, N> {
328
+ fn default() -> Self {
329
+ Self::new()
330
+ }
331
+ }
332
+
333
+ #[cfg(test)]
334
+ mod tests;
@@ -0,0 +1,165 @@
1
+ //! A counter that goes both ways, from two that only go up.
2
+
3
+ #[cfg(feature = "alloc")]
4
+ use crate::boxed_frame;
5
+ use crate::{
6
+ CapacityError, CodecError, GCounter, Merge, ReplicaId, SHAPE_LEN, TAG, TAG_LEN, TAG_PNCOUNTER,
7
+ read_small, small_field, tagged_frame, write_into,
8
+ };
9
+
10
+ /// Where the shape sits in a frame.
11
+ const SHAPE: core::ops::Range<usize> = TAG_LEN..TAG_LEN + SHAPE_LEN;
12
+
13
+ /// Where the slot table of what went up starts in a frame.
14
+ const INCREMENTS: usize = SHAPE.end;
15
+
16
+ /// A counter replicas can both raise and lower.
17
+ ///
18
+ /// ```
19
+ /// use merge_machines::{Merge, PNCounter};
20
+ ///
21
+ /// let mut left = PNCounter::<4>::new();
22
+ /// let mut right = PNCounter::<4>::new();
23
+ ///
24
+ /// assert_eq!(left.increment(1, 10), Ok(()));
25
+ /// assert_eq!(right.decrement(2, 4), Ok(()));
26
+ ///
27
+ /// assert_eq!(left.merge(&right), Ok(()));
28
+ /// assert_eq!(left.value(), 6);
29
+ /// ```
30
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
31
+ pub struct PNCounter<const R: usize> {
32
+ increments: GCounter<R>,
33
+ decrements: GCounter<R>,
34
+ }
35
+
36
+ impl<const R: usize> PNCounter<R> {
37
+ #[must_use]
38
+ pub const fn new() -> Self {
39
+ Self {
40
+ increments: GCounter::new(),
41
+ decrements: GCounter::new(),
42
+ }
43
+ }
44
+
45
+ /// # Errors
46
+ ///
47
+ /// Returns [`CapacityError`] when `replica` has no slot yet and all `R` are
48
+ /// taken.
49
+ pub fn increment(&mut self, replica: ReplicaId, by: u64) -> Result<(), CapacityError> {
50
+ self.increments.increment(replica, by)
51
+ }
52
+
53
+ /// # Errors
54
+ ///
55
+ /// Returns [`CapacityError`] when `replica` has no slot yet and all `R` are
56
+ /// taken.
57
+ pub fn decrement(&mut self, replica: ReplicaId, by: u64) -> Result<(), CapacityError> {
58
+ self.decrements.increment(replica, by)
59
+ }
60
+
61
+ /// Everything added, minus everything taken away.
62
+ #[must_use]
63
+ pub fn value(&self) -> i128 {
64
+ total(&self.increments) - total(&self.decrements)
65
+ }
66
+
67
+ /// Everything added, by every replica.
68
+ #[must_use]
69
+ pub fn added(&self) -> u128 {
70
+ self.increments.value()
71
+ }
72
+
73
+ /// Everything taken away, by every replica.
74
+ #[must_use]
75
+ pub fn removed(&self) -> u128 {
76
+ self.decrements.value()
77
+ }
78
+
79
+ /// Where the slot table of what came down starts in a frame.
80
+ const DECREMENTS: usize = INCREMENTS + GCounter::<R>::BODY_LEN;
81
+
82
+ /// Length of the [`encode`](Self::encode) frame.
83
+ pub const ENCODED_LEN: usize = Self::DECREMENTS + GCounter::<R>::BODY_LEN;
84
+
85
+ /// Writes the counter into `out`, returning the number of bytes written.
86
+ ///
87
+ /// # Errors
88
+ ///
89
+ /// [`CodecError::Truncated`] when `out` is shorter than
90
+ /// [`ENCODED_LEN`](Self::ENCODED_LEN).
91
+ pub fn encode(&self, out: &mut [u8]) -> Result<usize, CodecError> {
92
+ write_into(out, Self::ENCODED_LEN, |frame| self.write_frame(frame))
93
+ }
94
+
95
+ /// The counter's frame, exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
96
+ #[cfg(feature = "alloc")]
97
+ #[must_use]
98
+ pub fn encoded(&self) -> alloc::boxed::Box<[u8]> {
99
+ boxed_frame(Self::ENCODED_LEN, |frame| self.write_frame(frame))
100
+ }
101
+
102
+ /// Fills a frame of exactly [`ENCODED_LEN`](Self::ENCODED_LEN) bytes.
103
+ fn write_frame(&self, frame: &mut [u8]) {
104
+ frame[TAG] = TAG_PNCOUNTER;
105
+ frame[SHAPE].copy_from_slice(&small_field(R));
106
+ self.increments
107
+ .write_body(&mut frame[INCREMENTS..Self::DECREMENTS]);
108
+ self.decrements.write_body(&mut frame[Self::DECREMENTS..]);
109
+ }
110
+
111
+ /// Reads a counter written by [`encode`](Self::encode).
112
+ ///
113
+ /// # Errors
114
+ ///
115
+ /// [`CodecError::Truncated`] when `bytes` is too short, [`CodecError::Tag`]
116
+ /// when it holds another structure, [`CodecError::Parameters`] when it was
117
+ /// written with another replica count.
118
+ pub fn decode(bytes: &[u8]) -> Result<Self, CodecError> {
119
+ let mut counter = Self::new();
120
+ counter.decode_into(bytes)?;
121
+ Ok(counter)
122
+ }
123
+
124
+ /// Reads a frame over a counter the caller already holds.
125
+ ///
126
+ /// The counter is untouched when the frame is rejected.
127
+ ///
128
+ /// # Errors
129
+ ///
130
+ /// The same as [`decode`](Self::decode).
131
+ pub fn decode_into(&mut self, bytes: &[u8]) -> Result<(), CodecError> {
132
+ let frame = tagged_frame(bytes, Self::ENCODED_LEN, TAG_PNCOUNTER)?;
133
+ if read_small(&frame[SHAPE]) != R {
134
+ return Err(CodecError::Parameters);
135
+ }
136
+
137
+ self.increments
138
+ .read_body(&frame[INCREMENTS..Self::DECREMENTS]);
139
+ self.decrements.read_body(&frame[Self::DECREMENTS..]);
140
+ Ok(())
141
+ }
142
+ }
143
+
144
+ /// One side's count, signed so the other side can be taken from it.
145
+ fn total<const R: usize>(side: &GCounter<R>) -> i128 {
146
+ side.entries().map(|entry| i128::from(entry.count)).sum()
147
+ }
148
+
149
+ impl<const R: usize> Merge for PNCounter<R> {
150
+ type Error = CapacityError;
151
+
152
+ fn merge(&mut self, other: &Self) -> Result<(), Self::Error> {
153
+ self.increments.merge(&other.increments)?;
154
+ self.decrements.merge(&other.decrements)
155
+ }
156
+ }
157
+
158
+ impl<const R: usize> Default for PNCounter<R> {
159
+ fn default() -> Self {
160
+ Self::new()
161
+ }
162
+ }
163
+
164
+ #[cfg(test)]
165
+ mod tests;
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -0,0 +1,20 @@
1
+ [package]
2
+ name = "merge_machines_native"
3
+ version = "0.1.1"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ authors = ["Abdelkader Boudih <oss@seuros.com>"]
7
+ license = "MIT"
8
+ description = "Ruby bindings for the merge-machines replicated states"
9
+ publish = false
10
+
11
+ [lib]
12
+ crate-type = ["cdylib"]
13
+
14
+ [lints]
15
+ workspace = true
16
+
17
+ [dependencies]
18
+ merge-machines = { workspace = true, features = ["std"] }
19
+ magnus = { version = ">= 0.9.3", default-features = false, features = ["bytes", "io"] }
20
+ bytes = "1"
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+ require 'pathname'
5
+ require 'rb_sys/mkmf'
6
+
7
+ create_rust_makefile('merge_machines_native/merge_machines_native') do |r|
8
+ r.ext_dir = Pathname(__dir__).relative_path_from(Pathname(Dir.pwd)).to_s
9
+ r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
10
+ end
11
+
12
+ # rake-compiler builds from tmp/, where the generated relative manifest path no
13
+ # longer resolves.
14
+ makefile = File.join(Dir.pwd, 'Makefile')
15
+ File.write(
16
+ makefile,
17
+ File.read(makefile).gsub(
18
+ /^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/,
19
+ "RB_SYS_CARGO_MANIFEST_DIR ?= #{File.expand_path(__dir__)}"
20
+ )
21
+ )
@@ -0,0 +1,44 @@
1
+ //! The frozen `Data` object the hybrid clock hands back.
2
+
3
+ use std::sync::OnceLock;
4
+
5
+ use magnus::value::Opaque;
6
+ use magnus::{Error, RClass, RModule, RStruct, Ruby, Value, prelude::*};
7
+ use merge_machines::HybridTimestamp;
8
+
9
+ use crate::{defined_twice, uninitialised};
10
+
11
+ static TIMESTAMP: OnceLock<Opaque<RClass>> = OnceLock::new();
12
+
13
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
14
+ let timestamp = ruby.define_data(None, ("wall_ns", "counter"))?;
15
+ module.const_set("Timestamp", timestamp)?;
16
+ TIMESTAMP
17
+ .set(timestamp.into())
18
+ .map_err(|_| defined_twice(ruby, "Timestamp"))
19
+ }
20
+
21
+ /// A wall-clock reading in nanoseconds and the counter that breaks its ties.
22
+ pub fn timestamp(ruby: &Ruby, stamp: HybridTimestamp) -> Result<Value, Error> {
23
+ let class = TIMESTAMP
24
+ .get()
25
+ .map(|class| ruby.get_inner(*class))
26
+ .ok_or_else(|| uninitialised(ruby))?;
27
+ let instance = RStruct::try_convert(class.obj_alloc()?)?;
28
+ for (index, member) in [
29
+ ruby.into_value(stamp.wall_ns),
30
+ ruby.into_value(stamp.counter),
31
+ ]
32
+ .into_iter()
33
+ .enumerate()
34
+ {
35
+ instance.aset(index, member)?;
36
+ }
37
+ instance.freeze();
38
+ Ok(instance.as_value())
39
+ }
40
+
41
+ /// The timestamp a `wall:` and `counter:` name.
42
+ pub const fn stamped(wall_ns: u64, counter: u64) -> HybridTimestamp {
43
+ HybridTimestamp { wall_ns, counter }
44
+ }