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.
@@ -7,17 +7,17 @@ mod sem;
7
7
 
8
8
  use counter::AtomicCounter;
9
9
  use fixed_size_object_pool::FixedSizeObjectPool;
10
- use hashmap::ConcurrentHashMap;
10
+ use hashmap::MapStore;
11
11
  use magnus::{
12
- data_type_builder, method, IntoValue,
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.define_module("Ratomic")
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
- data_type_builder!(Counter, "ratomic/counter").frozen_shareable().build();
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(ConcurrentHashMap);
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(ConcurrentHashMap::new()), class).as_value();
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 set(&self, key: Value, value: Value) {
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 clear(&self) {
102
- self.0.clear();
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.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
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.define_module("Ratomic")
254
+ let class = ruby
255
+ .define_module("Ratomic")
147
256
  .unwrap()
148
- .define_class("ConcurrentHashMap", ruby.class_object())
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(&self, item: Value) {
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: &self.0,
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.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
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.define_module("Ratomic")
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!("wrong number of arguments (given {}, expected 0..2)", args.len()),
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(ruby.exception_arg_error(), "pool size must be positive"));
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.define_module("Ratomic")
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("ConcurrentHashMap", ruby.class_object())?;
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();
@@ -1,51 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratomic
4
- # Ruby convenience methods for {Counter}.
5
- module CounterMethods
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