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,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,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
|
+
}
|