dedup_machines 0.2.2
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 +68 -0
- data/Cargo.toml +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +208 -0
- data/ext/dedup_machines_native/core/CHANGELOG.md +54 -0
- data/ext/dedup_machines_native/core/Cargo.toml +21 -0
- data/ext/dedup_machines_native/core/README.md +70 -0
- data/ext/dedup_machines_native/core/src/hashed.rs +127 -0
- data/ext/dedup_machines_native/core/src/lib.rs +470 -0
- data/ext/dedup_machines_native/core/src/test_support.rs +16 -0
- data/ext/dedup_machines_native/extconf.rb +2 -0
- data/ext/dedup_machines_native/ffi/Cargo.toml +20 -0
- data/ext/dedup_machines_native/ffi/extconf.rb +14 -0
- data/ext/dedup_machines_native/ffi/src/init.rs +18 -0
- data/ext/dedup_machines_native/ffi/src/lib.rs +96 -0
- data/lib/dedup_machines/version.rb +5 -0
- data/lib/dedup_machines.rb +5 -0
- data/sig/dedup_machines.rbs +12 -0
- metadata +84 -0
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
#![no_std]
|
|
2
|
+
#![doc = include_str!("../README.md")]
|
|
3
|
+
|
|
4
|
+
#[cfg(feature = "alloc")]
|
|
5
|
+
extern crate alloc;
|
|
6
|
+
|
|
7
|
+
#[cfg(feature = "alloc")]
|
|
8
|
+
mod hashed;
|
|
9
|
+
|
|
10
|
+
use core::time::Duration;
|
|
11
|
+
|
|
12
|
+
#[cfg(feature = "alloc")]
|
|
13
|
+
pub use hashed::{HashDedup, Hashed};
|
|
14
|
+
|
|
15
|
+
/// Opaque generation-checked claim, scoped to one ledger lifetime.
|
|
16
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
17
|
+
pub struct ClaimId {
|
|
18
|
+
slot: usize,
|
|
19
|
+
generation: u64,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Claim outcome.
|
|
23
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
24
|
+
#[non_exhaustive]
|
|
25
|
+
pub enum Claim<'a, V> {
|
|
26
|
+
/// Caller owns a new pending claim.
|
|
27
|
+
Acquired(ClaimId),
|
|
28
|
+
/// Another live attempt owns this key.
|
|
29
|
+
InProgress,
|
|
30
|
+
/// This key completed within its retention interval.
|
|
31
|
+
Completed(&'a V),
|
|
32
|
+
/// A live or completed record for this key carries a different fingerprint.
|
|
33
|
+
Conflict,
|
|
34
|
+
/// All slots contain unexpired records.
|
|
35
|
+
Full,
|
|
36
|
+
/// The attempt's partition holds its quota of unexpired records.
|
|
37
|
+
PartitionFull,
|
|
38
|
+
/// Invalid clock/TTL or exhausted generation space.
|
|
39
|
+
Rejected,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// A key plus the optional fingerprint and partition it is claimed under.
|
|
43
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
44
|
+
pub struct Attempt<K> {
|
|
45
|
+
key: K,
|
|
46
|
+
fingerprint: Option<u64>,
|
|
47
|
+
partition: Option<u64>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl<K> Attempt<K> {
|
|
51
|
+
/// Plain attempt: no fingerprint, no partition.
|
|
52
|
+
#[must_use]
|
|
53
|
+
pub const fn new(key: K) -> Self {
|
|
54
|
+
Self {
|
|
55
|
+
key,
|
|
56
|
+
fingerprint: None,
|
|
57
|
+
partition: None,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Payload identity; a record under the same key with another fingerprint conflicts.
|
|
62
|
+
#[must_use]
|
|
63
|
+
pub const fn fingerprint(mut self, fingerprint: u64) -> Self {
|
|
64
|
+
self.fingerprint = Some(fingerprint);
|
|
65
|
+
self
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Quota bucket counted against the ledger's partition cap.
|
|
69
|
+
#[must_use]
|
|
70
|
+
pub const fn partition(mut self, partition: u64) -> Self {
|
|
71
|
+
self.partition = Some(partition);
|
|
72
|
+
self
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
77
|
+
enum State<V> {
|
|
78
|
+
Pending,
|
|
79
|
+
Completed(V),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// One record in a [`Storage`] backend: a claimed key, its generation and
|
|
83
|
+
/// expiry, and whether the claim is still pending or carries a completed value.
|
|
84
|
+
///
|
|
85
|
+
/// A [`Ledger`] builds entries and hands them to its storage; a backend that
|
|
86
|
+
/// persists them elsewhere reads them back through the accessors and rebuilds
|
|
87
|
+
/// them with [`Entry::new`].
|
|
88
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
89
|
+
pub struct Entry<K, V> {
|
|
90
|
+
key: K,
|
|
91
|
+
fingerprint: Option<u64>,
|
|
92
|
+
partition: Option<u64>,
|
|
93
|
+
generation: u64,
|
|
94
|
+
expires: Duration,
|
|
95
|
+
state: State<V>,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl<K, V> Entry<K, V> {
|
|
99
|
+
/// Rebuild a stored record; `value` is `None` while the claim is pending.
|
|
100
|
+
#[must_use]
|
|
101
|
+
pub fn new(
|
|
102
|
+
key: K,
|
|
103
|
+
fingerprint: Option<u64>,
|
|
104
|
+
partition: Option<u64>,
|
|
105
|
+
generation: u64,
|
|
106
|
+
expires: Duration,
|
|
107
|
+
value: Option<V>,
|
|
108
|
+
) -> Self {
|
|
109
|
+
Self {
|
|
110
|
+
key,
|
|
111
|
+
fingerprint,
|
|
112
|
+
partition,
|
|
113
|
+
generation,
|
|
114
|
+
expires,
|
|
115
|
+
state: value.map_or(State::Pending, State::Completed),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// The claimed key.
|
|
120
|
+
#[must_use]
|
|
121
|
+
pub const fn key(&self) -> &K {
|
|
122
|
+
&self.key
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Payload fingerprint the key was claimed under, if any.
|
|
126
|
+
#[must_use]
|
|
127
|
+
pub const fn fingerprint(&self) -> Option<u64> {
|
|
128
|
+
self.fingerprint
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Partition the record is counted against, if any.
|
|
132
|
+
#[must_use]
|
|
133
|
+
pub const fn partition(&self) -> Option<u64> {
|
|
134
|
+
self.partition
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Generation the claim was issued with; stale [`ClaimId`]s carry an older one.
|
|
138
|
+
#[must_use]
|
|
139
|
+
pub const fn generation(&self) -> u64 {
|
|
140
|
+
self.generation
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Instant after which the record is dropped on the next observation.
|
|
144
|
+
#[must_use]
|
|
145
|
+
pub const fn expires(&self) -> Duration {
|
|
146
|
+
self.expires
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// `true` while the claim is live and uncompleted.
|
|
150
|
+
#[must_use]
|
|
151
|
+
pub const fn is_pending(&self) -> bool {
|
|
152
|
+
matches!(self.state, State::Pending)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The completed value, once there is one.
|
|
156
|
+
#[must_use]
|
|
157
|
+
pub const fn value(&self) -> Option<&V> {
|
|
158
|
+
match &self.state {
|
|
159
|
+
State::Completed(value) => Some(value),
|
|
160
|
+
State::Pending => None,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Where a [`Ledger`] keeps its records.
|
|
166
|
+
///
|
|
167
|
+
/// [`Fixed`] and [`Hashed`] are in-memory. Implement this to put the ledger
|
|
168
|
+
/// over something else, such as a database table or a shared-memory arena;
|
|
169
|
+
/// the ledger only ever addresses records by the slot `insert` returned.
|
|
170
|
+
///
|
|
171
|
+
/// Slots are opaque: a backend may reuse one after `remove`, because the
|
|
172
|
+
/// ledger's generation check tells a stale [`ClaimId`] from a fresh record.
|
|
173
|
+
pub trait Storage {
|
|
174
|
+
/// Claimed key type.
|
|
175
|
+
type Key: Eq;
|
|
176
|
+
/// Value stored on completion.
|
|
177
|
+
type Value;
|
|
178
|
+
|
|
179
|
+
/// The record at `slot`, if one is live.
|
|
180
|
+
fn get(&self, slot: usize) -> Option<&Entry<Self::Key, Self::Value>>;
|
|
181
|
+
/// Mutable access to the record at `slot`, if one is live.
|
|
182
|
+
fn get_mut(&mut self, slot: usize) -> Option<&mut Entry<Self::Key, Self::Value>>;
|
|
183
|
+
/// The slot holding `key`, if any.
|
|
184
|
+
fn find(&self, key: &Self::Key) -> Option<usize>;
|
|
185
|
+
/// Store `entry` in a free slot, or `None` when the backend is full.
|
|
186
|
+
fn insert(&mut self, entry: Entry<Self::Key, Self::Value>) -> Option<usize>;
|
|
187
|
+
/// Drop the record at `slot`; `false` when there was none.
|
|
188
|
+
fn remove(&mut self, slot: usize) -> bool;
|
|
189
|
+
/// Drop every record `keep` rejects, returning how many were dropped.
|
|
190
|
+
fn retain(&mut self, keep: impl FnMut(&Entry<Self::Key, Self::Value>) -> bool) -> usize;
|
|
191
|
+
/// Every live record, in no particular order.
|
|
192
|
+
fn iter(&self) -> impl Iterator<Item = &Entry<Self::Key, Self::Value>>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Fixed-capacity inline storage.
|
|
196
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
197
|
+
pub struct Fixed<K, V, const N: usize> {
|
|
198
|
+
entries: [Option<Entry<K, V>>; N],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
impl<K: Eq, V, const N: usize> Storage for Fixed<K, V, N> {
|
|
202
|
+
type Key = K;
|
|
203
|
+
type Value = V;
|
|
204
|
+
|
|
205
|
+
fn get(&self, slot: usize) -> Option<&Entry<K, V>> {
|
|
206
|
+
self.entries.get(slot)?.as_ref()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn get_mut(&mut self, slot: usize) -> Option<&mut Entry<K, V>> {
|
|
210
|
+
self.entries.get_mut(slot)?.as_mut()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn find(&self, key: &K) -> Option<usize> {
|
|
214
|
+
self.entries
|
|
215
|
+
.iter()
|
|
216
|
+
.position(|entry| entry.as_ref().is_some_and(|entry| entry.key == *key))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fn insert(&mut self, entry: Entry<K, V>) -> Option<usize> {
|
|
220
|
+
let slot = self.entries.iter().position(Option::is_none)?;
|
|
221
|
+
self.entries[slot] = Some(entry);
|
|
222
|
+
Some(slot)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn remove(&mut self, slot: usize) -> bool {
|
|
226
|
+
self.entries.get_mut(slot).and_then(Option::take).is_some()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn retain(&mut self, mut keep: impl FnMut(&Entry<K, V>) -> bool) -> usize {
|
|
230
|
+
let mut removed = 0;
|
|
231
|
+
for slot in &mut self.entries {
|
|
232
|
+
if slot.as_ref().is_some_and(|entry| !keep(entry)) {
|
|
233
|
+
*slot = None;
|
|
234
|
+
removed += 1;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
removed
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn iter(&self) -> impl Iterator<Item = &Entry<K, V>> {
|
|
241
|
+
self.entries.iter().flatten()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// Exact-key claim ledger over a storage backend.
|
|
246
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
247
|
+
pub struct Ledger<S> {
|
|
248
|
+
store: S,
|
|
249
|
+
generation: u64,
|
|
250
|
+
observed: Duration,
|
|
251
|
+
partition_cap: Option<usize>,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// Fixed-capacity exact-key ledger.
|
|
255
|
+
pub type Dedup<K, const N: usize, V = ()> = Ledger<Fixed<K, V, N>>;
|
|
256
|
+
|
|
257
|
+
impl<K: Eq, V, const N: usize> Default for Dedup<K, N, V> {
|
|
258
|
+
fn default() -> Self {
|
|
259
|
+
Self::new()
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
impl<K: Eq, V, const N: usize> Dedup<K, N, V> {
|
|
264
|
+
/// Start an empty ledger with a fresh handle namespace.
|
|
265
|
+
#[must_use]
|
|
266
|
+
pub const fn new() -> Self {
|
|
267
|
+
Self::over(Fixed {
|
|
268
|
+
entries: [const { None }; N],
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
impl<S: Storage> Ledger<S> {
|
|
274
|
+
/// Start an empty ledger over `store` with a fresh handle namespace.
|
|
275
|
+
///
|
|
276
|
+
/// The store is expected to be empty: records it already holds would
|
|
277
|
+
/// outrank every [`ClaimId`] this ledger issues. Use [`Ledger::restore`]
|
|
278
|
+
/// to continue a persisted one.
|
|
279
|
+
#[must_use]
|
|
280
|
+
pub const fn over(store: S) -> Self {
|
|
281
|
+
Self::restore(store, 0, Duration::ZERO)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Continue a ledger whose records live in `store`, from the highest
|
|
285
|
+
/// `generation` it ever issued and the last clock value it `observed`.
|
|
286
|
+
///
|
|
287
|
+
/// Both must come from the same committed state as the records, so no
|
|
288
|
+
/// stored [`ClaimId`] can be reissued.
|
|
289
|
+
#[must_use]
|
|
290
|
+
pub const fn restore(store: S, generation: u64, observed: Duration) -> Self {
|
|
291
|
+
Self {
|
|
292
|
+
store,
|
|
293
|
+
generation,
|
|
294
|
+
observed,
|
|
295
|
+
partition_cap: None,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// Highest generation issued so far; persist it with the records.
|
|
300
|
+
#[must_use]
|
|
301
|
+
pub const fn generation(&self) -> u64 {
|
|
302
|
+
self.generation
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/// Latest clock value accepted; persist it with the records.
|
|
306
|
+
#[must_use]
|
|
307
|
+
pub const fn observed(&self) -> Duration {
|
|
308
|
+
self.observed
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// The storage backend, for a persisting adapter to read through.
|
|
312
|
+
#[must_use]
|
|
313
|
+
pub const fn store(&self) -> &S {
|
|
314
|
+
&self.store
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// Give the storage back, to persist or hand to [`Ledger::restore`].
|
|
318
|
+
#[must_use]
|
|
319
|
+
pub fn into_store(self) -> S {
|
|
320
|
+
self.store
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// Cap unexpired records per partition; attempts without a partition are uncounted.
|
|
324
|
+
#[must_use]
|
|
325
|
+
pub const fn with_partition_cap(mut self, cap: usize) -> Self {
|
|
326
|
+
self.partition_cap = Some(cap);
|
|
327
|
+
self
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/// Claim a key without evicting any unexpired entry.
|
|
331
|
+
#[must_use]
|
|
332
|
+
pub fn claim(&mut self, key: S::Key, now: Duration, ttl: Duration) -> Claim<'_, S::Value> {
|
|
333
|
+
self.admit(Attempt::new(key), now, ttl)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// Claim a key under a payload fingerprint; a mismatching record conflicts.
|
|
337
|
+
#[must_use]
|
|
338
|
+
pub fn claim_with(
|
|
339
|
+
&mut self,
|
|
340
|
+
key: S::Key,
|
|
341
|
+
fingerprint: u64,
|
|
342
|
+
now: Duration,
|
|
343
|
+
ttl: Duration,
|
|
344
|
+
) -> Claim<'_, S::Value> {
|
|
345
|
+
self.admit(Attempt::new(key).fingerprint(fingerprint), now, ttl)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/// Claim an attempt without evicting any unexpired entry.
|
|
349
|
+
#[must_use]
|
|
350
|
+
pub fn admit(
|
|
351
|
+
&mut self,
|
|
352
|
+
attempt: Attempt<S::Key>,
|
|
353
|
+
now: Duration,
|
|
354
|
+
ttl: Duration,
|
|
355
|
+
) -> Claim<'_, S::Value> {
|
|
356
|
+
if !self.observe(now) || ttl.is_zero() {
|
|
357
|
+
return Claim::Rejected;
|
|
358
|
+
}
|
|
359
|
+
let Some(expires) = now.checked_add(ttl) else {
|
|
360
|
+
return Claim::Rejected;
|
|
361
|
+
};
|
|
362
|
+
self.store.retain(|entry| now < entry.expires);
|
|
363
|
+
let found = self.store.find(&attempt.key);
|
|
364
|
+
if let Some(slot) = found {
|
|
365
|
+
return self.revisit(slot, attempt.fingerprint);
|
|
366
|
+
}
|
|
367
|
+
if let (Some(cap), Some(partition)) = (self.partition_cap, attempt.partition)
|
|
368
|
+
&& self
|
|
369
|
+
.store
|
|
370
|
+
.iter()
|
|
371
|
+
.filter(|entry| entry.partition == Some(partition))
|
|
372
|
+
.count()
|
|
373
|
+
>= cap
|
|
374
|
+
{
|
|
375
|
+
return Claim::PartitionFull;
|
|
376
|
+
}
|
|
377
|
+
let Some(generation) = self.generation.checked_add(1) else {
|
|
378
|
+
return Claim::Rejected;
|
|
379
|
+
};
|
|
380
|
+
let Some(slot) = self.store.insert(Entry {
|
|
381
|
+
key: attempt.key,
|
|
382
|
+
fingerprint: attempt.fingerprint,
|
|
383
|
+
partition: attempt.partition,
|
|
384
|
+
generation,
|
|
385
|
+
expires,
|
|
386
|
+
state: State::Pending,
|
|
387
|
+
}) else {
|
|
388
|
+
return Claim::Full;
|
|
389
|
+
};
|
|
390
|
+
self.generation = generation;
|
|
391
|
+
Claim::Acquired(ClaimId { slot, generation })
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/// Complete a live pending claim. Repeated or stale completions return `false`.
|
|
395
|
+
#[must_use]
|
|
396
|
+
pub fn complete(&mut self, claim: ClaimId, now: Duration) -> bool
|
|
397
|
+
where
|
|
398
|
+
S::Value: Default,
|
|
399
|
+
{
|
|
400
|
+
self.complete_with(claim, S::Value::default(), now).is_ok()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/// Complete a live pending claim, storing `value` for duplicate attempts.
|
|
404
|
+
///
|
|
405
|
+
/// # Errors
|
|
406
|
+
///
|
|
407
|
+
/// Repeated, stale, or expired completions hand `value` back.
|
|
408
|
+
pub fn complete_with(
|
|
409
|
+
&mut self,
|
|
410
|
+
claim: ClaimId,
|
|
411
|
+
value: S::Value,
|
|
412
|
+
now: Duration,
|
|
413
|
+
) -> Result<(), S::Value> {
|
|
414
|
+
if !self.observe(now) {
|
|
415
|
+
return Err(value);
|
|
416
|
+
}
|
|
417
|
+
let Some(entry) = self.store.get_mut(claim.slot) else {
|
|
418
|
+
return Err(value);
|
|
419
|
+
};
|
|
420
|
+
if entry.generation != claim.generation
|
|
421
|
+
|| !matches!(entry.state, State::Pending)
|
|
422
|
+
|| now >= entry.expires
|
|
423
|
+
{
|
|
424
|
+
return Err(value);
|
|
425
|
+
}
|
|
426
|
+
entry.state = State::Completed(value);
|
|
427
|
+
Ok(())
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/// Free a matching pending claim after a known failure, not an uncertain effect.
|
|
431
|
+
#[must_use]
|
|
432
|
+
pub fn abandon(&mut self, claim: ClaimId) -> bool {
|
|
433
|
+
let pending = self.store.get(claim.slot).is_some_and(|entry| {
|
|
434
|
+
entry.generation == claim.generation && matches!(entry.state, State::Pending)
|
|
435
|
+
});
|
|
436
|
+
pending && self.store.remove(claim.slot)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/// Drop every record, pending or completed, whose key `dead` marks as ownerless.
|
|
440
|
+
/// Returns how many were dropped.
|
|
441
|
+
pub fn expire_where(&mut self, mut dead: impl FnMut(&S::Key) -> bool) -> usize {
|
|
442
|
+
self.store.retain(|entry| !dead(&entry.key))
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
fn revisit(&self, slot: usize, fingerprint: Option<u64>) -> Claim<'_, S::Value> {
|
|
446
|
+
let Some(entry) = self.store.get(slot) else {
|
|
447
|
+
return Claim::Rejected;
|
|
448
|
+
};
|
|
449
|
+
if entry.fingerprint != fingerprint {
|
|
450
|
+
return Claim::Conflict;
|
|
451
|
+
}
|
|
452
|
+
match &entry.state {
|
|
453
|
+
State::Pending => Claim::InProgress,
|
|
454
|
+
State::Completed(value) => Claim::Completed(value),
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
fn observe(&mut self, now: Duration) -> bool {
|
|
459
|
+
if now < self.observed {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
self.observed = now;
|
|
463
|
+
true
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
#[cfg(test)]
|
|
468
|
+
mod test_support;
|
|
469
|
+
#[cfg(test)]
|
|
470
|
+
mod tests;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//! Helpers shared by the crate's test modules.
|
|
2
|
+
|
|
3
|
+
use core::time::Duration;
|
|
4
|
+
|
|
5
|
+
use crate::{Claim, ClaimId};
|
|
6
|
+
|
|
7
|
+
pub fn at(seconds: u64) -> Duration {
|
|
8
|
+
Duration::from_secs(seconds)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
pub fn acquired<V>(claim: &Claim<'_, V>) -> ClaimId {
|
|
12
|
+
let Claim::Acquired(id) = claim else {
|
|
13
|
+
panic!("expected acquired claim");
|
|
14
|
+
};
|
|
15
|
+
*id
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "dedup_machines_native"
|
|
3
|
+
version = "0.1.6"
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
rust-version.workspace = true
|
|
6
|
+
license.workspace = true
|
|
7
|
+
publish = false
|
|
8
|
+
|
|
9
|
+
[lib]
|
|
10
|
+
crate-type = ["cdylib"]
|
|
11
|
+
test = false
|
|
12
|
+
doctest = false
|
|
13
|
+
|
|
14
|
+
[dependencies]
|
|
15
|
+
dedup-machines.workspace = true
|
|
16
|
+
magnus.workspace = true
|
|
17
|
+
rb-sys.workspace = true
|
|
18
|
+
|
|
19
|
+
[lints]
|
|
20
|
+
workspace = true
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mkmf'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'rb_sys/mkmf'
|
|
5
|
+
|
|
6
|
+
create_rust_makefile('dedup_machines_native/dedup_machines_native') do |e|
|
|
7
|
+
e.ext_dir = Pathname(__dir__).relative_path_from(Pathname(Dir.pwd)).to_s
|
|
8
|
+
e.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
|
|
9
|
+
end
|
|
10
|
+
makefile = File.join(Dir.pwd, 'Makefile')
|
|
11
|
+
File.write(makefile, File.read(makefile).gsub(
|
|
12
|
+
/^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/,
|
|
13
|
+
"RB_SYS_CARGO_MANIFEST_DIR ?= #{File.expand_path(__dir__)}"
|
|
14
|
+
))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//! Private native entry point.
|
|
2
|
+
use crate::Ledger;
|
|
3
|
+
use magnus::{Error, Ruby, function, method, prelude::*};
|
|
4
|
+
|
|
5
|
+
#[magnus::init]
|
|
6
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
7
|
+
let module = ruby.define_module("DedupMachines")?;
|
|
8
|
+
let handle = module.define_class("Claim", ruby.class_object())?;
|
|
9
|
+
handle.undef_default_alloc_func();
|
|
10
|
+
let class = module.define_class("Ledger", ruby.class_object())?;
|
|
11
|
+
class.undef_default_alloc_func();
|
|
12
|
+
class.define_singleton_method("new", function!(Ledger::new, 0))?;
|
|
13
|
+
class.define_method("claim", method!(Ledger::claim, 3))?;
|
|
14
|
+
class.define_method("complete", method!(Ledger::complete, 2))?;
|
|
15
|
+
class.define_method("abandon", method!(Ledger::abandon, 1))?;
|
|
16
|
+
class.define_method("capacity", function!(Ledger::capacity, 0))?;
|
|
17
|
+
Ok(())
|
|
18
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
//! Thin binding to the exact bounded claim ledger.
|
|
2
|
+
mod init;
|
|
3
|
+
|
|
4
|
+
use std::{cell::RefCell, sync::Arc, time::Duration};
|
|
5
|
+
|
|
6
|
+
use dedup_machines::{Claim, ClaimId, Dedup};
|
|
7
|
+
use magnus::{Error, Integer, Ruby, TryConvert, Value, prelude::*, typed_data::Obj};
|
|
8
|
+
|
|
9
|
+
const CAPACITY: usize = 64;
|
|
10
|
+
|
|
11
|
+
#[derive(Clone, Copy)]
|
|
12
|
+
struct Number(u64);
|
|
13
|
+
|
|
14
|
+
impl TryConvert for Number {
|
|
15
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
16
|
+
if Integer::from_value(value).is_none() {
|
|
17
|
+
return Err(Error::new(
|
|
18
|
+
Ruby::get_with(value).exception_type_error(),
|
|
19
|
+
"expected an Integer (unsigned 64-bit)",
|
|
20
|
+
));
|
|
21
|
+
}
|
|
22
|
+
u64::try_convert(value).map(Self)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#[magnus::wrap(class = "DedupMachines::Claim", free_immediately, size)]
|
|
27
|
+
struct Handle {
|
|
28
|
+
owner: Arc<()>,
|
|
29
|
+
id: ClaimId,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[magnus::wrap(class = "DedupMachines::Ledger", free_immediately, size)]
|
|
33
|
+
struct Ledger {
|
|
34
|
+
owner: Arc<()>,
|
|
35
|
+
core: RefCell<Dedup<u64, CAPACITY>>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl Ledger {
|
|
39
|
+
fn new() -> Self {
|
|
40
|
+
Self {
|
|
41
|
+
owner: Arc::new(()),
|
|
42
|
+
core: RefCell::new(Dedup::new()),
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn claim(
|
|
47
|
+
ruby: &Ruby,
|
|
48
|
+
this: Obj<Self>,
|
|
49
|
+
key: Number,
|
|
50
|
+
now_ns: Number,
|
|
51
|
+
ttl_ns: Number,
|
|
52
|
+
) -> Result<Value, Error> {
|
|
53
|
+
this.check_frozen()?;
|
|
54
|
+
let mut core = this.core.borrow_mut();
|
|
55
|
+
let outcome = core.claim(
|
|
56
|
+
key.0,
|
|
57
|
+
Duration::from_nanos(now_ns.0),
|
|
58
|
+
Duration::from_nanos(ttl_ns.0),
|
|
59
|
+
);
|
|
60
|
+
Ok(match outcome {
|
|
61
|
+
Claim::Acquired(id) => ruby
|
|
62
|
+
.obj_wrap(Handle {
|
|
63
|
+
owner: Arc::clone(&this.owner),
|
|
64
|
+
id,
|
|
65
|
+
})
|
|
66
|
+
.as_value(),
|
|
67
|
+
Claim::InProgress => ruby.to_symbol("in_progress").as_value(),
|
|
68
|
+
Claim::Completed(()) => ruby.to_symbol("completed").as_value(),
|
|
69
|
+
Claim::Full => ruby.to_symbol("full").as_value(),
|
|
70
|
+
_ => ruby.to_symbol("rejected").as_value(),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn complete(this: Obj<Self>, handle: Obj<Handle>, now_ns: Number) -> Result<bool, Error> {
|
|
75
|
+
this.check_frozen()?;
|
|
76
|
+
if !Arc::ptr_eq(&this.owner, &handle.owner) {
|
|
77
|
+
return Ok(false);
|
|
78
|
+
}
|
|
79
|
+
Ok(this
|
|
80
|
+
.core
|
|
81
|
+
.borrow_mut()
|
|
82
|
+
.complete(handle.id, Duration::from_nanos(now_ns.0)))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn abandon(this: Obj<Self>, handle: Obj<Handle>) -> Result<bool, Error> {
|
|
86
|
+
this.check_frozen()?;
|
|
87
|
+
if !Arc::ptr_eq(&this.owner, &handle.owner) {
|
|
88
|
+
return Ok(false);
|
|
89
|
+
}
|
|
90
|
+
Ok(this.core.borrow_mut().abandon(handle.id))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const fn capacity() -> usize {
|
|
94
|
+
CAPACITY
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module DedupMachines
|
|
2
|
+
VERSION: String
|
|
3
|
+
class Claim
|
|
4
|
+
end
|
|
5
|
+
class Ledger
|
|
6
|
+
def self.new: () -> Ledger
|
|
7
|
+
def capacity: () -> Integer
|
|
8
|
+
def claim: (Integer key, Integer now_ns, Integer ttl_ns) -> (Claim | :in_progress | :completed | :full | :rejected)
|
|
9
|
+
def complete: (Claim, Integer now_ns) -> bool
|
|
10
|
+
def abandon: (Claim) -> bool
|
|
11
|
+
end
|
|
12
|
+
end
|