ratomic 0.2.1 → 0.3.1
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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +165 -98
- data/ext/ratomic/src/hashmap.rs +79 -3
- data/ext/ratomic/src/lib.rs +154 -25
- data/lib/ratomic/counter.rb +35 -29
- data/lib/ratomic/map.rb +295 -17
- data/lib/ratomic/pool.rb +10 -8
- data/lib/ratomic/queue.rb +60 -6
- data/lib/ratomic/undefined.rb +3 -1
- data/lib/ratomic/version.rb +2 -1
- data/lib/ratomic.rb +16 -11
- data/ratomic.gemspec +6 -5
- metadata +11 -7
data/ext/ratomic/src/lib.rs
CHANGED
|
@@ -7,17 +7,17 @@ mod sem;
|
|
|
7
7
|
|
|
8
8
|
use counter::AtomicCounter;
|
|
9
9
|
use fixed_size_object_pool::FixedSizeObjectPool;
|
|
10
|
-
use hashmap::
|
|
10
|
+
use hashmap::MapStore;
|
|
11
11
|
use magnus::{
|
|
12
|
-
data_type_builder, method,
|
|
12
|
+
data_type_builder, method,
|
|
13
13
|
prelude::*,
|
|
14
14
|
typed_data::{DataType, DataTypeFunctions},
|
|
15
15
|
value::Lazy,
|
|
16
|
-
Error, RClass, Ruby, TryConvert, TypedData, Value,
|
|
16
|
+
Error, IntoValue, RClass, Ruby, TryConvert, TypedData, Value,
|
|
17
17
|
};
|
|
18
18
|
use mpmc_queue::MpmcQueue;
|
|
19
|
-
use rb_sys::{rb_ext_ractor_safe, rb_thread_call_without_gvl, ruby_special_consts, VALUE};
|
|
20
19
|
use parking_lot::Mutex;
|
|
20
|
+
use rb_sys::{rb_ext_ractor_safe, rb_thread_call_without_gvl, ruby_special_consts, VALUE};
|
|
21
21
|
use std::{ffi::c_void, mem::transmute};
|
|
22
22
|
|
|
23
23
|
fn value_to_raw(value: Value) -> VALUE {
|
|
@@ -46,12 +46,14 @@ impl Counter {
|
|
|
46
46
|
make_shareable(ruby, value)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
fn increment(&self, amt: u64) {
|
|
49
|
+
fn increment(&self, amt: u64) -> u64 {
|
|
50
50
|
self.0.inc(amt);
|
|
51
|
+
self.0.read()
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
fn decrement(&self, amt: u64) {
|
|
54
|
+
fn decrement(&self, amt: u64) -> u64 {
|
|
54
55
|
self.0.dec(amt);
|
|
56
|
+
self.0.read()
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
fn read(&self) -> u64 {
|
|
@@ -64,7 +66,8 @@ impl DataTypeFunctions for Counter {}
|
|
|
64
66
|
unsafe impl TypedData for Counter {
|
|
65
67
|
fn class(ruby: &Ruby) -> RClass {
|
|
66
68
|
static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
67
|
-
let class = ruby
|
|
69
|
+
let class = ruby
|
|
70
|
+
.define_module("Ratomic")
|
|
68
71
|
.unwrap()
|
|
69
72
|
.define_class("Counter", ruby.class_object())
|
|
70
73
|
.unwrap();
|
|
@@ -75,17 +78,18 @@ unsafe impl TypedData for Counter {
|
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
fn data_type() -> &'static DataType {
|
|
78
|
-
static DATA_TYPE: DataType =
|
|
79
|
-
|
|
81
|
+
static DATA_TYPE: DataType = data_type_builder!(Counter, "ratomic/counter")
|
|
82
|
+
.frozen_shareable()
|
|
83
|
+
.build();
|
|
80
84
|
&DATA_TYPE
|
|
81
85
|
}
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
struct HashMap(
|
|
88
|
+
struct HashMap(MapStore);
|
|
85
89
|
|
|
86
90
|
impl HashMap {
|
|
87
91
|
fn new(ruby: &Ruby, class: RClass) -> Result<Value, Error> {
|
|
88
|
-
let value = ruby.wrap_as(Self(
|
|
92
|
+
let value = ruby.wrap_as(Self(MapStore::new()), class).as_value();
|
|
89
93
|
make_shareable(ruby, value)
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -94,12 +98,24 @@ impl HashMap {
|
|
|
94
98
|
unsafe { value_from_raw(raw) }.into_value_with(ruby)
|
|
95
99
|
}
|
|
96
100
|
|
|
97
|
-
fn
|
|
101
|
+
fn contains_key(&self, key: Value) -> bool {
|
|
102
|
+
self.0.contains_key(value_to_raw(key))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn set(&self, key: Value, value: Value) -> Value {
|
|
98
106
|
self.0.set(value_to_raw(key), value_to_raw(value));
|
|
107
|
+
value
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
fn
|
|
102
|
-
|
|
110
|
+
fn delete(ruby: &Ruby, rb_self: &Self, key: Value) -> Value {
|
|
111
|
+
let raw = rb_self.0.delete(value_to_raw(key)).unwrap_or_else(qnil_raw);
|
|
112
|
+
unsafe { value_from_raw(raw) }.into_value_with(ruby)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn clear(_ruby: &Ruby, value: Value) -> Result<Value, Error> {
|
|
116
|
+
let rb_self: &Self = TryConvert::try_convert(value)?;
|
|
117
|
+
rb_self.0.clear();
|
|
118
|
+
Ok(value)
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
fn size(&self) -> usize {
|
|
@@ -132,20 +148,113 @@ impl HashMap {
|
|
|
132
148
|
Ok(())
|
|
133
149
|
}
|
|
134
150
|
}
|
|
151
|
+
|
|
152
|
+
fn compute(ruby: &Ruby, rb_self: &Self, key: Value) -> Result<Value, Error> {
|
|
153
|
+
if !ruby.block_given() {
|
|
154
|
+
return Err(Error::new(
|
|
155
|
+
ruby.exception_local_jump_error(),
|
|
156
|
+
"no block given",
|
|
157
|
+
));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let proc = ruby.block_proc()?;
|
|
161
|
+
let raw = rb_self.0.compute(value_to_raw(key), qnil_raw(), |value| {
|
|
162
|
+
proc.call::<_, Value>((unsafe { value_from_raw(value) },))
|
|
163
|
+
.map(value_to_raw)
|
|
164
|
+
})?;
|
|
165
|
+
|
|
166
|
+
Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn fetch_or_store(ruby: &Ruby, rb_self: &Self, key: Value) -> Result<Value, Error> {
|
|
170
|
+
if !ruby.block_given() {
|
|
171
|
+
return Err(Error::new(
|
|
172
|
+
ruby.exception_local_jump_error(),
|
|
173
|
+
"no block given",
|
|
174
|
+
));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let proc = ruby.block_proc()?;
|
|
178
|
+
let raw = rb_self.0.fetch_or_store(value_to_raw(key), || {
|
|
179
|
+
proc.call::<_, Value>(()).map(value_to_raw)
|
|
180
|
+
})?;
|
|
181
|
+
|
|
182
|
+
Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn upsert(ruby: &Ruby, rb_self: &Self, key: Value, initial: Value) -> Result<Value, Error> {
|
|
186
|
+
if !ruby.block_given() {
|
|
187
|
+
return Err(Error::new(
|
|
188
|
+
ruby.exception_local_jump_error(),
|
|
189
|
+
"no block given",
|
|
190
|
+
));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
let proc = ruby.block_proc()?;
|
|
194
|
+
let raw = rb_self
|
|
195
|
+
.0
|
|
196
|
+
.upsert(value_to_raw(key), value_to_raw(initial), |value| {
|
|
197
|
+
proc.call::<_, Value>((unsafe { value_from_raw(value) },))
|
|
198
|
+
.map(value_to_raw)
|
|
199
|
+
})?;
|
|
200
|
+
|
|
201
|
+
Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fn increment_numeric(
|
|
205
|
+
ruby: &Ruby,
|
|
206
|
+
rb_self: &Self,
|
|
207
|
+
key: Value,
|
|
208
|
+
by: Value,
|
|
209
|
+
) -> Result<Value, Error> {
|
|
210
|
+
let key_inspect = key.funcall::<_, _, String>("inspect", ())?;
|
|
211
|
+
let numeric_class: RClass = ruby.class_object().const_get("Numeric")?;
|
|
212
|
+
let raw = rb_self.0.update(value_to_raw(key), |current| {
|
|
213
|
+
let next = match current {
|
|
214
|
+
Some(value) if value == qnil_raw() => {
|
|
215
|
+
return Err(Error::new(
|
|
216
|
+
ruby.exception_type_error(),
|
|
217
|
+
format!("existing value for {key_inspect} must be numeric: nil"),
|
|
218
|
+
));
|
|
219
|
+
}
|
|
220
|
+
Some(value) => {
|
|
221
|
+
let old_value = unsafe { value_from_raw(value) };
|
|
222
|
+
if !old_value.funcall::<_, _, bool>("is_a?", (numeric_class,))? {
|
|
223
|
+
let old_value_inspect = old_value.funcall::<_, _, String>("inspect", ())?;
|
|
224
|
+
return Err(Error::new(
|
|
225
|
+
ruby.exception_type_error(),
|
|
226
|
+
format!(
|
|
227
|
+
"existing value for {key_inspect} must be numeric: {old_value_inspect}"
|
|
228
|
+
),
|
|
229
|
+
));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
old_value.funcall::<_, _, Value>("+", (by,))?
|
|
233
|
+
}
|
|
234
|
+
None => by,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
Ok(value_to_raw(next))
|
|
238
|
+
})?;
|
|
239
|
+
|
|
240
|
+
Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
|
|
241
|
+
}
|
|
135
242
|
}
|
|
136
243
|
|
|
137
244
|
impl DataTypeFunctions for HashMap {
|
|
138
245
|
fn mark(&self, marker: &magnus::gc::Marker) {
|
|
139
|
-
self.0
|
|
246
|
+
self.0
|
|
247
|
+
.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
|
|
140
248
|
}
|
|
141
249
|
}
|
|
142
250
|
|
|
143
251
|
unsafe impl TypedData for HashMap {
|
|
144
252
|
fn class(ruby: &Ruby) -> RClass {
|
|
145
253
|
static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
146
|
-
let class = ruby
|
|
254
|
+
let class = ruby
|
|
255
|
+
.define_module("Ratomic")
|
|
147
256
|
.unwrap()
|
|
148
|
-
.define_class("
|
|
257
|
+
.define_class("Map", ruby.class_object())
|
|
149
258
|
.unwrap();
|
|
150
259
|
class.undef_default_alloc_func();
|
|
151
260
|
class
|
|
@@ -203,9 +312,10 @@ impl Queue {
|
|
|
203
312
|
make_shareable(ruby, value)
|
|
204
313
|
}
|
|
205
314
|
|
|
206
|
-
fn push(&
|
|
315
|
+
fn push(ruby: &Ruby, rb_self: Value, item: Value) -> Result<Value, Error> {
|
|
316
|
+
let queue: &Self = TryConvert::try_convert(rb_self)?;
|
|
207
317
|
let mut payload = PushPayload {
|
|
208
|
-
queue: &
|
|
318
|
+
queue: &queue.0,
|
|
209
319
|
item: value_to_raw(item),
|
|
210
320
|
};
|
|
211
321
|
unsafe {
|
|
@@ -216,6 +326,7 @@ impl Queue {
|
|
|
216
326
|
std::ptr::null_mut(),
|
|
217
327
|
);
|
|
218
328
|
}
|
|
329
|
+
Ok(rb_self.into_value_with(ruby))
|
|
219
330
|
}
|
|
220
331
|
|
|
221
332
|
fn pop(&self) -> Value {
|
|
@@ -245,14 +356,16 @@ impl Queue {
|
|
|
245
356
|
|
|
246
357
|
impl DataTypeFunctions for Queue {
|
|
247
358
|
fn mark(&self, marker: &magnus::gc::Marker) {
|
|
248
|
-
self.0
|
|
359
|
+
self.0
|
|
360
|
+
.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
|
|
249
361
|
}
|
|
250
362
|
}
|
|
251
363
|
|
|
252
364
|
unsafe impl TypedData for Queue {
|
|
253
365
|
fn class(ruby: &Ruby) -> RClass {
|
|
254
366
|
static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
255
|
-
let class = ruby
|
|
367
|
+
let class = ruby
|
|
368
|
+
.define_module("Ratomic")
|
|
256
369
|
.unwrap()
|
|
257
370
|
.define_class("Queue", ruby.class_object())
|
|
258
371
|
.unwrap();
|
|
@@ -278,7 +391,10 @@ impl Pool {
|
|
|
278
391
|
if args.len() > 2 {
|
|
279
392
|
return Err(Error::new(
|
|
280
393
|
ruby.exception_arg_error(),
|
|
281
|
-
format!(
|
|
394
|
+
format!(
|
|
395
|
+
"wrong number of arguments (given {}, expected 0..2)",
|
|
396
|
+
args.len()
|
|
397
|
+
),
|
|
282
398
|
));
|
|
283
399
|
}
|
|
284
400
|
let size = args
|
|
@@ -296,7 +412,10 @@ impl Pool {
|
|
|
296
412
|
.unwrap_or(1000);
|
|
297
413
|
|
|
298
414
|
if size == 0 {
|
|
299
|
-
return Err(Error::new(
|
|
415
|
+
return Err(Error::new(
|
|
416
|
+
ruby.exception_arg_error(),
|
|
417
|
+
"pool size must be positive",
|
|
418
|
+
));
|
|
300
419
|
}
|
|
301
420
|
if !ruby.block_given() {
|
|
302
421
|
return Err(Error::new(
|
|
@@ -351,7 +470,8 @@ impl DataTypeFunctions for Pool {
|
|
|
351
470
|
unsafe impl TypedData for Pool {
|
|
352
471
|
fn class(ruby: &Ruby) -> RClass {
|
|
353
472
|
static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
|
|
354
|
-
let class = ruby
|
|
473
|
+
let class = ruby
|
|
474
|
+
.define_module("Ratomic")
|
|
355
475
|
.unwrap()
|
|
356
476
|
.define_class("FixedSizeObjectPool", ruby.class_object())
|
|
357
477
|
.unwrap();
|
|
@@ -383,14 +503,23 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
383
503
|
counter.define_method("decrement", method!(Counter::decrement, 1))?;
|
|
384
504
|
counter.define_method("read", method!(Counter::read, 0))?;
|
|
385
505
|
|
|
386
|
-
let hashmap = root.define_class("
|
|
506
|
+
let hashmap = root.define_class("Map", ruby.class_object())?;
|
|
387
507
|
hashmap.undef_default_alloc_func();
|
|
388
508
|
hashmap.define_singleton_method("new", method!(HashMap::new, 0))?;
|
|
389
509
|
hashmap.define_method("get", method!(HashMap::get, 1))?;
|
|
510
|
+
hashmap.define_method("key?", method!(HashMap::contains_key, 1))?;
|
|
390
511
|
hashmap.define_method("set", method!(HashMap::set, 2))?;
|
|
512
|
+
hashmap.define_method("delete", method!(HashMap::delete, 1))?;
|
|
391
513
|
hashmap.define_method("clear", method!(HashMap::clear, 0))?;
|
|
392
514
|
hashmap.define_method("size", method!(HashMap::size, 0))?;
|
|
393
515
|
hashmap.define_method("fetch_and_modify", method!(HashMap::fetch_and_modify, 1))?;
|
|
516
|
+
hashmap.define_method("compute", method!(HashMap::compute, 1))?;
|
|
517
|
+
hashmap.define_method("fetch_or_store", method!(HashMap::fetch_or_store, 1))?;
|
|
518
|
+
hashmap.define_method("upsert", method!(HashMap::upsert, 2))?;
|
|
519
|
+
hashmap.define_private_method(
|
|
520
|
+
"__increment_numeric",
|
|
521
|
+
method!(HashMap::increment_numeric, 2),
|
|
522
|
+
)?;
|
|
394
523
|
|
|
395
524
|
let queue = root.define_class("Queue", ruby.class_object())?;
|
|
396
525
|
queue.undef_default_alloc_func();
|
data/lib/ratomic/counter.rb
CHANGED
|
@@ -1,51 +1,57 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Ratomic
|
|
4
|
-
#
|
|
5
|
-
|
|
4
|
+
# A Ractor-shareable atomic counter.
|
|
5
|
+
#
|
|
6
|
+
# Counter stores an unsigned integer in native Rust atomics and can be shared
|
|
7
|
+
# safely across Ractors.
|
|
8
|
+
#
|
|
9
|
+
# @example Count work across Ractors
|
|
10
|
+
# counter = Ratomic::Counter.new
|
|
11
|
+
# counter.increment(1) # => 1
|
|
12
|
+
# counter.read # => 1
|
|
13
|
+
#
|
|
14
|
+
# @!method self.new
|
|
15
|
+
# Create a counter initialized to zero.
|
|
16
|
+
#
|
|
17
|
+
# @return [Ratomic::Counter] a new shareable counter
|
|
18
|
+
#
|
|
19
|
+
# @!method read
|
|
20
|
+
# Read the current counter value.
|
|
21
|
+
#
|
|
22
|
+
# @return [Integer] the current counter value
|
|
23
|
+
#
|
|
24
|
+
# @!method increment(amt)
|
|
25
|
+
# Increment the counter by +amt+.
|
|
26
|
+
#
|
|
27
|
+
# @param amt [Integer] amount to add to the counter
|
|
28
|
+
# @return [Integer] the updated counter value
|
|
29
|
+
#
|
|
30
|
+
# @!method decrement(amt)
|
|
31
|
+
# Decrement the counter by +amt+.
|
|
32
|
+
#
|
|
33
|
+
# @param amt [Integer] amount to subtract from the counter
|
|
34
|
+
# @return [Integer] the updated counter value
|
|
35
|
+
class Counter
|
|
6
36
|
# Read the current counter value.
|
|
7
37
|
#
|
|
8
|
-
# @return [Integer]
|
|
38
|
+
# @return [Integer] the current counter value
|
|
9
39
|
def value
|
|
10
40
|
read
|
|
11
41
|
end
|
|
12
42
|
|
|
13
43
|
# Coerce the counter to an Integer snapshot.
|
|
14
44
|
#
|
|
15
|
-
# @return [Integer]
|
|
45
|
+
# @return [Integer] the current counter value
|
|
16
46
|
def to_i
|
|
17
47
|
read
|
|
18
48
|
end
|
|
19
49
|
|
|
20
50
|
# Check whether the current counter value is zero.
|
|
21
51
|
#
|
|
22
|
-
# @return [Boolean]
|
|
52
|
+
# @return [Boolean] true when the counter currently reads zero
|
|
23
53
|
def zero?
|
|
24
54
|
read.zero?
|
|
25
55
|
end
|
|
26
|
-
|
|
27
|
-
# Increment the counter.
|
|
28
|
-
#
|
|
29
|
-
# @param amt [Integer] amount to add
|
|
30
|
-
# @raise [ArgumentError] if +amt+ is negative
|
|
31
|
-
# @return [void]
|
|
32
|
-
def inc(amt = 1)
|
|
33
|
-
raise ArgumentError, "amount must be positive: #{amt}" if amt.negative?
|
|
34
|
-
|
|
35
|
-
increment(amt)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Decrement the counter.
|
|
39
|
-
#
|
|
40
|
-
# @param amt [Integer] amount to subtract
|
|
41
|
-
# @raise [ArgumentError] if +amt+ is negative
|
|
42
|
-
# @return [void]
|
|
43
|
-
def dec(amt = 1)
|
|
44
|
-
raise ArgumentError, "amount must be positive: #{amt}" if amt.negative?
|
|
45
|
-
|
|
46
|
-
decrement(amt)
|
|
47
|
-
end
|
|
48
56
|
end
|
|
49
|
-
|
|
50
|
-
Counter.prepend(CounterMethods)
|
|
51
57
|
end
|