ratomic 0.1.0 → 0.2.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.
@@ -0,0 +1,26 @@
1
+ use std::sync::atomic::{AtomicU64, Ordering};
2
+
3
+ #[derive(Debug)]
4
+ pub struct AtomicCounter {
5
+ value: AtomicU64,
6
+ }
7
+
8
+ impl AtomicCounter {
9
+ pub fn new(n: u64) -> Self {
10
+ Self {
11
+ value: AtomicU64::new(n),
12
+ }
13
+ }
14
+
15
+ pub fn inc(&self, amt: u64) {
16
+ self.value.fetch_add(amt, Ordering::Relaxed);
17
+ }
18
+
19
+ pub fn dec(&self, amt: u64) {
20
+ self.value.fetch_sub(amt, Ordering::Relaxed);
21
+ }
22
+
23
+ pub fn read(&self) -> u64 {
24
+ self.value.load(Ordering::Relaxed)
25
+ }
26
+ }
@@ -0,0 +1,52 @@
1
+ use crossbeam_channel::{Receiver, Sender};
2
+ use rb_sys::VALUE;
3
+ use std::time::Duration;
4
+
5
+ pub struct FixedSizeObjectPool {
6
+ pool: Vec<VALUE>,
7
+ tx: Sender<usize>,
8
+ rx: Receiver<usize>,
9
+ timeout: Duration,
10
+ }
11
+
12
+ pub struct PooledItem {
13
+ pub idx: usize,
14
+ pub rbobj: VALUE,
15
+ }
16
+
17
+ impl FixedSizeObjectPool {
18
+ pub fn new(pool: Vec<VALUE>, timeout_in_ms: u64) -> Self {
19
+ let (tx, rx) = crossbeam_channel::unbounded();
20
+ for idx in 0..pool.len() {
21
+ tx.send(idx).unwrap();
22
+ }
23
+
24
+ Self {
25
+ pool,
26
+ tx,
27
+ rx,
28
+ timeout: Duration::from_millis(timeout_in_ms),
29
+ }
30
+ }
31
+
32
+ pub fn mark<F>(&self, f: F)
33
+ where
34
+ F: Fn(VALUE),
35
+ {
36
+ for item in self.pool.iter() {
37
+ f(*item);
38
+ }
39
+ }
40
+
41
+ pub fn checkout(&self) -> Option<PooledItem> {
42
+ let idx = self.rx.recv_timeout(self.timeout).ok()?;
43
+ Some(PooledItem {
44
+ idx,
45
+ rbobj: self.pool[idx],
46
+ })
47
+ }
48
+
49
+ pub fn checkin(&self, idx: usize) {
50
+ self.tx.send(idx).unwrap();
51
+ }
52
+ }
@@ -0,0 +1,70 @@
1
+ use rb_sys::{rb_eql, rb_hash, VALUE};
2
+
3
+ #[derive(Debug)]
4
+ struct RubyHashEql(VALUE);
5
+
6
+ impl PartialEq for RubyHashEql {
7
+ fn eq(&self, other: &Self) -> bool {
8
+ unsafe { rb_eql(self.0, other.0) != 0 }
9
+ }
10
+ }
11
+
12
+ impl Eq for RubyHashEql {}
13
+
14
+ impl std::hash::Hash for RubyHashEql {
15
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
16
+ let ruby_hash = unsafe { rb_hash(self.0) };
17
+ ruby_hash.hash(state);
18
+ }
19
+ }
20
+
21
+ pub struct ConcurrentHashMap {
22
+ map: dashmap::DashMap<RubyHashEql, VALUE>,
23
+ }
24
+
25
+ impl ConcurrentHashMap {
26
+ pub fn new() -> Self {
27
+ Self {
28
+ map: dashmap::DashMap::new(),
29
+ }
30
+ }
31
+
32
+ pub fn get(&self, key: VALUE) -> Option<VALUE> {
33
+ self.map.get(&RubyHashEql(key)).map(|v| *v)
34
+ }
35
+
36
+ pub fn set(&self, key: VALUE, value: VALUE) {
37
+ self.map.insert(RubyHashEql(key), value);
38
+ }
39
+
40
+ pub fn clear(&self) {
41
+ self.map.clear()
42
+ }
43
+
44
+ pub fn size(&self) -> usize {
45
+ self.map.len()
46
+ }
47
+
48
+ pub fn fetch_and_modify<F>(&self, key: VALUE, f: F)
49
+ where
50
+ F: FnOnce(VALUE) -> VALUE,
51
+ {
52
+ self.map.alter(&RubyHashEql(key), |_, value| f(value));
53
+ }
54
+
55
+ pub fn mark<F>(&self, f: F)
56
+ where
57
+ F: Fn(VALUE),
58
+ {
59
+ for pair in self.map.iter() {
60
+ f(pair.key().0);
61
+ f(*pair.value());
62
+ }
63
+ }
64
+ }
65
+
66
+ impl Default for ConcurrentHashMap {
67
+ fn default() -> Self {
68
+ Self::new()
69
+ }
70
+ }
@@ -0,0 +1,412 @@
1
+ mod counter;
2
+ mod fixed_size_object_pool;
3
+ mod gc_guard;
4
+ mod hashmap;
5
+ mod mpmc_queue;
6
+ mod sem;
7
+
8
+ use counter::AtomicCounter;
9
+ use fixed_size_object_pool::FixedSizeObjectPool;
10
+ use hashmap::ConcurrentHashMap;
11
+ use magnus::{
12
+ data_type_builder, method, IntoValue,
13
+ prelude::*,
14
+ typed_data::{DataType, DataTypeFunctions},
15
+ value::Lazy,
16
+ Error, RClass, Ruby, TryConvert, TypedData, Value,
17
+ };
18
+ use mpmc_queue::MpmcQueue;
19
+ use rb_sys::{rb_ext_ractor_safe, rb_thread_call_without_gvl, ruby_special_consts, VALUE};
20
+ use parking_lot::Mutex;
21
+ use std::{ffi::c_void, mem::transmute};
22
+
23
+ fn value_to_raw(value: Value) -> VALUE {
24
+ unsafe { transmute::<Value, VALUE>(value) }
25
+ }
26
+
27
+ unsafe fn value_from_raw(raw: VALUE) -> Value {
28
+ transmute::<VALUE, Value>(raw)
29
+ }
30
+
31
+ fn qnil_raw() -> VALUE {
32
+ ruby_special_consts::RUBY_Qnil as VALUE
33
+ }
34
+
35
+ fn make_shareable(ruby: &Ruby, value: Value) -> Result<Value, Error> {
36
+ value.freeze();
37
+ let ractor: RClass = ruby.class_object().const_get("Ractor")?;
38
+ ractor.funcall("make_shareable", (value,))
39
+ }
40
+
41
+ struct Counter(AtomicCounter);
42
+
43
+ impl Counter {
44
+ fn new(ruby: &Ruby, class: RClass) -> Result<Value, Error> {
45
+ let value = ruby.wrap_as(Self(AtomicCounter::new(0)), class).as_value();
46
+ make_shareable(ruby, value)
47
+ }
48
+
49
+ fn increment(&self, amt: u64) {
50
+ self.0.inc(amt);
51
+ }
52
+
53
+ fn decrement(&self, amt: u64) {
54
+ self.0.dec(amt);
55
+ }
56
+
57
+ fn read(&self) -> u64 {
58
+ self.0.read()
59
+ }
60
+ }
61
+
62
+ impl DataTypeFunctions for Counter {}
63
+
64
+ unsafe impl TypedData for Counter {
65
+ fn class(ruby: &Ruby) -> RClass {
66
+ static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
67
+ let class = ruby.define_module("Ratomic")
68
+ .unwrap()
69
+ .define_class("Counter", ruby.class_object())
70
+ .unwrap();
71
+ class.undef_default_alloc_func();
72
+ class
73
+ });
74
+ ruby.get_inner(&CLASS)
75
+ }
76
+
77
+ fn data_type() -> &'static DataType {
78
+ static DATA_TYPE: DataType =
79
+ data_type_builder!(Counter, "ratomic/counter").frozen_shareable().build();
80
+ &DATA_TYPE
81
+ }
82
+ }
83
+
84
+ struct HashMap(ConcurrentHashMap);
85
+
86
+ impl HashMap {
87
+ fn new(ruby: &Ruby, class: RClass) -> Result<Value, Error> {
88
+ let value = ruby.wrap_as(Self(ConcurrentHashMap::new()), class).as_value();
89
+ make_shareable(ruby, value)
90
+ }
91
+
92
+ fn get(ruby: &Ruby, rb_self: &Self, key: Value) -> Value {
93
+ let raw = rb_self.0.get(value_to_raw(key)).unwrap_or_else(qnil_raw);
94
+ unsafe { value_from_raw(raw) }.into_value_with(ruby)
95
+ }
96
+
97
+ fn set(&self, key: Value, value: Value) {
98
+ self.0.set(value_to_raw(key), value_to_raw(value));
99
+ }
100
+
101
+ fn clear(&self) {
102
+ self.0.clear();
103
+ }
104
+
105
+ fn size(&self) -> usize {
106
+ self.0.size()
107
+ }
108
+
109
+ fn fetch_and_modify(ruby: &Ruby, rb_self: &Self, key: Value) -> Result<(), Error> {
110
+ if !ruby.block_given() {
111
+ return Err(Error::new(
112
+ ruby.exception_local_jump_error(),
113
+ "no block given",
114
+ ));
115
+ }
116
+
117
+ let proc = ruby.block_proc()?;
118
+ let mut error = None;
119
+ rb_self.0.fetch_and_modify(value_to_raw(key), |value| {
120
+ match proc.call::<_, Value>((unsafe { value_from_raw(value) },)) {
121
+ Ok(result) => value_to_raw(result),
122
+ Err(err) => {
123
+ error = Some(err);
124
+ value
125
+ }
126
+ }
127
+ });
128
+
129
+ if let Some(err) = error {
130
+ Err(err)
131
+ } else {
132
+ Ok(())
133
+ }
134
+ }
135
+ }
136
+
137
+ impl DataTypeFunctions for HashMap {
138
+ fn mark(&self, marker: &magnus::gc::Marker) {
139
+ self.0.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
140
+ }
141
+ }
142
+
143
+ unsafe impl TypedData for HashMap {
144
+ fn class(ruby: &Ruby) -> RClass {
145
+ static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
146
+ let class = ruby.define_module("Ratomic")
147
+ .unwrap()
148
+ .define_class("ConcurrentHashMap", ruby.class_object())
149
+ .unwrap();
150
+ class.undef_default_alloc_func();
151
+ class
152
+ });
153
+ ruby.get_inner(&CLASS)
154
+ }
155
+
156
+ fn data_type() -> &'static DataType {
157
+ static DATA_TYPE: DataType = data_type_builder!(HashMap, "ratomic/hashmap")
158
+ .mark()
159
+ .frozen_shareable()
160
+ .build();
161
+ &DATA_TYPE
162
+ }
163
+ }
164
+
165
+ struct Queue(MpmcQueue);
166
+
167
+ struct PushPayload<'a> {
168
+ queue: &'a MpmcQueue,
169
+ item: VALUE,
170
+ }
171
+
172
+ unsafe extern "C" fn push_without_gvl(payload: *mut c_void) -> *mut c_void {
173
+ let payload = &*(payload as *const PushPayload<'_>);
174
+ payload.queue.push(payload.item);
175
+ std::ptr::null_mut()
176
+ }
177
+
178
+ unsafe extern "C" fn pop_without_gvl(queue: *mut c_void) -> *mut c_void {
179
+ let queue = &*(queue as *const MpmcQueue);
180
+ queue.pop() as *mut c_void
181
+ }
182
+
183
+ impl Queue {
184
+ fn new(ruby: &Ruby, class: RClass, capacity: Value) -> Result<Value, Error> {
185
+ if !capacity.is_kind_of(ruby.class_integer()) {
186
+ return Err(Error::new(
187
+ ruby.exception_type_error(),
188
+ "no implicit conversion into Integer",
189
+ ));
190
+ }
191
+ let capacity = i64::try_convert(capacity)?;
192
+ if capacity < 1 || capacity > (1 << 20) {
193
+ return Err(Error::new(
194
+ ruby.exception_arg_error(),
195
+ "queue capacity must be between 1 and 1048576",
196
+ ));
197
+ }
198
+ let capacity = capacity as usize;
199
+
200
+ let value = ruby
201
+ .wrap_as(Self(MpmcQueue::new(capacity, qnil_raw())), class)
202
+ .as_value();
203
+ make_shareable(ruby, value)
204
+ }
205
+
206
+ fn push(&self, item: Value) {
207
+ let mut payload = PushPayload {
208
+ queue: &self.0,
209
+ item: value_to_raw(item),
210
+ };
211
+ unsafe {
212
+ rb_thread_call_without_gvl(
213
+ Some(push_without_gvl),
214
+ &mut payload as *mut PushPayload<'_> as *mut c_void,
215
+ None,
216
+ std::ptr::null_mut(),
217
+ );
218
+ }
219
+ }
220
+
221
+ fn pop(&self) -> Value {
222
+ let raw = unsafe {
223
+ rb_thread_call_without_gvl(
224
+ Some(pop_without_gvl),
225
+ &self.0 as *const MpmcQueue as *mut c_void,
226
+ None,
227
+ std::ptr::null_mut(),
228
+ ) as VALUE
229
+ };
230
+ unsafe { value_from_raw(raw) }
231
+ }
232
+
233
+ fn peek(&self) -> Option<Value> {
234
+ self.0.peek().map(|raw| unsafe { value_from_raw(raw) })
235
+ }
236
+
237
+ fn is_empty(&self) -> bool {
238
+ self.0.is_empty()
239
+ }
240
+
241
+ fn size(&self) -> usize {
242
+ self.0.size()
243
+ }
244
+ }
245
+
246
+ impl DataTypeFunctions for Queue {
247
+ fn mark(&self, marker: &magnus::gc::Marker) {
248
+ self.0.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
249
+ }
250
+ }
251
+
252
+ unsafe impl TypedData for Queue {
253
+ fn class(ruby: &Ruby) -> RClass {
254
+ static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
255
+ let class = ruby.define_module("Ratomic")
256
+ .unwrap()
257
+ .define_class("Queue", ruby.class_object())
258
+ .unwrap();
259
+ class.undef_default_alloc_func();
260
+ class
261
+ });
262
+ ruby.get_inner(&CLASS)
263
+ }
264
+
265
+ fn data_type() -> &'static DataType {
266
+ static DATA_TYPE: DataType = data_type_builder!(Queue, "ratomic/queue")
267
+ .mark()
268
+ .frozen_shareable()
269
+ .build();
270
+ &DATA_TYPE
271
+ }
272
+ }
273
+
274
+ struct Pool(Mutex<Option<FixedSizeObjectPool>>);
275
+
276
+ impl Pool {
277
+ fn new(ruby: &Ruby, class: RClass, args: &[Value]) -> Result<Value, Error> {
278
+ if args.len() > 2 {
279
+ return Err(Error::new(
280
+ ruby.exception_arg_error(),
281
+ format!("wrong number of arguments (given {}, expected 0..2)", args.len()),
282
+ ));
283
+ }
284
+ let size = args
285
+ .first()
286
+ .copied()
287
+ .map(usize::try_convert)
288
+ .transpose()?
289
+ .unwrap_or(5);
290
+ let timeout_ms = args
291
+ .get(1)
292
+ .copied()
293
+ .map(f64::try_convert)
294
+ .transpose()?
295
+ .map(|timeout| (timeout * 1000.0) as u64)
296
+ .unwrap_or(1000);
297
+
298
+ if size == 0 {
299
+ return Err(Error::new(ruby.exception_arg_error(), "pool size must be positive"));
300
+ }
301
+ if !ruby.block_given() {
302
+ return Err(Error::new(
303
+ ruby.exception_local_jump_error(),
304
+ "no block given",
305
+ ));
306
+ }
307
+
308
+ let value = ruby.wrap_as(Self(Mutex::new(None)), class).as_value();
309
+ let value = make_shareable(ruby, value)?;
310
+
311
+ let mut pool = Vec::with_capacity(size);
312
+ for _ in 0..size {
313
+ let value: Value = ruby.yield_value(())?;
314
+ pool.push(value_to_raw(value));
315
+ }
316
+
317
+ let rb_self: &Self = TryConvert::try_convert(value)?;
318
+ *rb_self.0.lock() = Some(FixedSizeObjectPool::new(pool, timeout_ms));
319
+ Ok(value)
320
+ }
321
+
322
+ fn checkout(ruby: &Ruby, rb_self: &Self) -> Option<magnus::RArray> {
323
+ rb_self
324
+ .0
325
+ .lock()
326
+ .as_ref()
327
+ .and_then(FixedSizeObjectPool::checkout)
328
+ .map(|item| {
329
+ ruby.ary_new_from_values(&[
330
+ unsafe { value_from_raw(item.rbobj) },
331
+ item.idx.into_value_with(ruby),
332
+ ])
333
+ })
334
+ }
335
+
336
+ fn checkin(&self, idx: usize) {
337
+ if let Some(pool) = self.0.lock().as_ref() {
338
+ pool.checkin(idx);
339
+ }
340
+ }
341
+ }
342
+
343
+ impl DataTypeFunctions for Pool {
344
+ fn mark(&self, marker: &magnus::gc::Marker) {
345
+ if let Some(pool) = self.0.lock().as_ref() {
346
+ pool.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
347
+ }
348
+ }
349
+ }
350
+
351
+ unsafe impl TypedData for Pool {
352
+ fn class(ruby: &Ruby) -> RClass {
353
+ static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
354
+ let class = ruby.define_module("Ratomic")
355
+ .unwrap()
356
+ .define_class("FixedSizeObjectPool", ruby.class_object())
357
+ .unwrap();
358
+ class.undef_default_alloc_func();
359
+ class
360
+ });
361
+ ruby.get_inner(&CLASS)
362
+ }
363
+
364
+ fn data_type() -> &'static DataType {
365
+ static DATA_TYPE: DataType = data_type_builder!(Pool, "ratomic/pool")
366
+ .mark()
367
+ .frozen_shareable()
368
+ .build();
369
+ &DATA_TYPE
370
+ }
371
+ }
372
+
373
+ #[magnus::init]
374
+ fn init(ruby: &Ruby) -> Result<(), Error> {
375
+ unsafe { rb_ext_ractor_safe(true) };
376
+
377
+ let root = ruby.define_module("Ratomic")?;
378
+
379
+ let counter = root.define_class("Counter", ruby.class_object())?;
380
+ counter.undef_default_alloc_func();
381
+ counter.define_singleton_method("new", method!(Counter::new, 0))?;
382
+ counter.define_method("increment", method!(Counter::increment, 1))?;
383
+ counter.define_method("decrement", method!(Counter::decrement, 1))?;
384
+ counter.define_method("read", method!(Counter::read, 0))?;
385
+
386
+ let hashmap = root.define_class("ConcurrentHashMap", ruby.class_object())?;
387
+ hashmap.undef_default_alloc_func();
388
+ hashmap.define_singleton_method("new", method!(HashMap::new, 0))?;
389
+ hashmap.define_method("get", method!(HashMap::get, 1))?;
390
+ hashmap.define_method("set", method!(HashMap::set, 2))?;
391
+ hashmap.define_method("clear", method!(HashMap::clear, 0))?;
392
+ hashmap.define_method("size", method!(HashMap::size, 0))?;
393
+ hashmap.define_method("fetch_and_modify", method!(HashMap::fetch_and_modify, 1))?;
394
+
395
+ let queue = root.define_class("Queue", ruby.class_object())?;
396
+ queue.undef_default_alloc_func();
397
+ queue.define_singleton_method("new", method!(Queue::new, 1))?;
398
+ queue.define_method("push", method!(Queue::push, 1))?;
399
+ queue.define_method("pop", method!(Queue::pop, 0))?;
400
+ queue.define_method("peek", method!(Queue::peek, 0))?;
401
+ queue.define_method("empty?", method!(Queue::is_empty, 0))?;
402
+ queue.define_method("length", method!(Queue::size, 0))?;
403
+ queue.define_method("size", method!(Queue::size, 0))?;
404
+
405
+ let pool = root.define_class("FixedSizeObjectPool", ruby.class_object())?;
406
+ pool.undef_default_alloc_func();
407
+ pool.define_singleton_method("new", method!(Pool::new, -1))?;
408
+ pool.define_method("checkout", method!(Pool::checkout, 0))?;
409
+ pool.define_method("checkin", method!(Pool::checkin, 1))?;
410
+
411
+ Ok(())
412
+ }