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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/Cargo.lock +381 -0
- data/Cargo.toml +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +181 -0
- data/ext/ratomic/Cargo.toml +22 -0
- data/ext/ratomic/build.rs +3 -0
- data/ext/ratomic/extconf.rb +2 -12
- data/ext/ratomic/src/counter.rs +26 -0
- data/ext/ratomic/src/fixed_size_object_pool.rs +52 -0
- data/ext/ratomic/src/hashmap.rs +70 -0
- data/ext/ratomic/src/lib.rs +412 -0
- data/ext/ratomic/src/mpmc_queue.rs +164 -0
- data/ext/ratomic/src/sem.rs +72 -0
- data/lib/ratomic/counter.rb +51 -0
- data/lib/ratomic/map.rb +56 -0
- data/lib/ratomic/pool.rb +170 -0
- data/lib/ratomic/queue.rb +17 -0
- data/lib/ratomic/undefined.rb +15 -0
- data/lib/ratomic/version.rb +1 -1
- data/lib/ratomic.rb +8 -60
- data/ratomic.gemspec +19 -13
- metadata +41 -23
- data/ext/ratomic/counter.h +0 -45
- data/ext/ratomic/fixed-size-object-pool.h +0 -76
- data/ext/ratomic/hashmap.h +0 -74
- data/ext/ratomic/mpmc-queue.h +0 -68
- data/ext/ratomic/ratomic.c +0 -16
- data/lib/ratomic/ratomic.bundle +0 -0
- data/rs/Cargo.lock +0 -196
- data/rs/Cargo.toml +0 -26
- data/rs/cbindgen.toml +0 -12
- data/rs/rust-atomics.h +0 -89
- data/rs/src/bin/mpmc_queue.rs +0 -89
- data/rs/src/counter.rs +0 -57
- data/rs/src/fixed_size_object_pool.rs +0 -120
- data/rs/src/hashmap.rs +0 -129
- data/rs/src/lib.rs +0 -23
- data/rs/src/mpmc_queue.rs +0 -231
- data/rs/src/sem.rs +0 -75
- /data/{rs → ext/ratomic}/src/gc_guard.rs +0 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
use crate::{gc_guard::GcGuard, sem::Semaphore};
|
|
2
|
+
use rb_sys::VALUE;
|
|
3
|
+
use std::{
|
|
4
|
+
cell::Cell,
|
|
5
|
+
sync::atomic::{AtomicUsize, Ordering},
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
struct QueueElement {
|
|
9
|
+
sequence: AtomicUsize,
|
|
10
|
+
data: Cell<VALUE>,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
unsafe impl Send for QueueElement {}
|
|
14
|
+
unsafe impl Sync for QueueElement {}
|
|
15
|
+
|
|
16
|
+
pub struct MpmcQueue {
|
|
17
|
+
buffer: Vec<QueueElement>,
|
|
18
|
+
buffer_size: usize,
|
|
19
|
+
enqueue_pos: AtomicUsize,
|
|
20
|
+
dequeue_pos: AtomicUsize,
|
|
21
|
+
gc_guard: GcGuard,
|
|
22
|
+
read_sem: Semaphore,
|
|
23
|
+
write_sem: Semaphore,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl MpmcQueue {
|
|
27
|
+
pub fn new(buffer_size: usize, default: VALUE) -> Self {
|
|
28
|
+
let mut buffer = Vec::with_capacity(buffer_size);
|
|
29
|
+
for i in 0..buffer_size {
|
|
30
|
+
buffer.push(QueueElement {
|
|
31
|
+
sequence: AtomicUsize::new(i),
|
|
32
|
+
data: Cell::new(default),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let mut gc_guard = GcGuard::alloc();
|
|
37
|
+
gc_guard.init();
|
|
38
|
+
let mut read_sem = Semaphore::alloc();
|
|
39
|
+
read_sem.init(0);
|
|
40
|
+
let mut write_sem = Semaphore::alloc();
|
|
41
|
+
write_sem.init(buffer_size as u32);
|
|
42
|
+
|
|
43
|
+
Self {
|
|
44
|
+
buffer,
|
|
45
|
+
buffer_size,
|
|
46
|
+
enqueue_pos: AtomicUsize::new(0),
|
|
47
|
+
dequeue_pos: AtomicUsize::new(0),
|
|
48
|
+
gc_guard,
|
|
49
|
+
read_sem,
|
|
50
|
+
write_sem,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fn try_push(&self, data: VALUE) -> bool {
|
|
55
|
+
let mut cell;
|
|
56
|
+
let mut pos = self.enqueue_pos.load(Ordering::Relaxed);
|
|
57
|
+
loop {
|
|
58
|
+
cell = &self.buffer[pos % self.buffer_size];
|
|
59
|
+
let seq = cell.sequence.load(Ordering::Acquire);
|
|
60
|
+
let diff = seq as isize - pos as isize;
|
|
61
|
+
if diff == 0 {
|
|
62
|
+
if self
|
|
63
|
+
.enqueue_pos
|
|
64
|
+
.compare_exchange_weak(pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed)
|
|
65
|
+
.is_ok()
|
|
66
|
+
{
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
} else if diff < 0 {
|
|
70
|
+
return false;
|
|
71
|
+
} else {
|
|
72
|
+
pos = self.enqueue_pos.load(Ordering::Relaxed);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
cell.data.set(data);
|
|
76
|
+
cell.sequence.store(pos + 1, Ordering::Release);
|
|
77
|
+
self.read_sem.post();
|
|
78
|
+
true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn try_pop(&self) -> Option<VALUE> {
|
|
82
|
+
let mut cell;
|
|
83
|
+
let mut pos = self.dequeue_pos.load(Ordering::Relaxed);
|
|
84
|
+
loop {
|
|
85
|
+
cell = &self.buffer[pos % self.buffer_size];
|
|
86
|
+
let seq = cell.sequence.load(Ordering::Acquire);
|
|
87
|
+
let diff = seq as isize - (pos + 1) as isize;
|
|
88
|
+
if diff == 0 {
|
|
89
|
+
if self
|
|
90
|
+
.dequeue_pos
|
|
91
|
+
.compare_exchange_weak(pos, pos + 1, Ordering::Relaxed, Ordering::Relaxed)
|
|
92
|
+
.is_ok()
|
|
93
|
+
{
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
} else if diff < 0 {
|
|
97
|
+
return None;
|
|
98
|
+
} else {
|
|
99
|
+
pos = self.dequeue_pos.load(Ordering::Relaxed);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let data = cell.data.get();
|
|
104
|
+
cell.sequence
|
|
105
|
+
.store(pos + self.buffer_size, Ordering::Release);
|
|
106
|
+
self.write_sem.post();
|
|
107
|
+
|
|
108
|
+
#[cfg(feature = "simulation")]
|
|
109
|
+
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
110
|
+
|
|
111
|
+
Some(data)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pub fn push(&self, data: VALUE) {
|
|
115
|
+
loop {
|
|
116
|
+
if self.try_push(data) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
self.write_sem.wait();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
pub fn pop(&self) -> VALUE {
|
|
124
|
+
loop {
|
|
125
|
+
if let Some(data) = self.gc_guard.acquire_as_consumer(|| self.try_pop()) {
|
|
126
|
+
return data;
|
|
127
|
+
}
|
|
128
|
+
self.read_sem.wait();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
pub fn peek(&self) -> Option<VALUE> {
|
|
133
|
+
let pos = self.dequeue_pos.load(Ordering::Relaxed);
|
|
134
|
+
let cell = &self.buffer[pos % self.buffer_size];
|
|
135
|
+
let seq = cell.sequence.load(Ordering::Acquire);
|
|
136
|
+
let diff = seq as isize - (pos + 1) as isize;
|
|
137
|
+
if diff == 0 {
|
|
138
|
+
Some(cell.data.get())
|
|
139
|
+
} else {
|
|
140
|
+
None
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pub fn is_empty(&self) -> bool {
|
|
145
|
+
self.dequeue_pos.load(Ordering::Relaxed) == self.enqueue_pos.load(Ordering::Relaxed)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub fn size(&self) -> usize {
|
|
149
|
+
self.enqueue_pos
|
|
150
|
+
.load(Ordering::Relaxed)
|
|
151
|
+
.wrapping_sub(self.dequeue_pos.load(Ordering::Relaxed))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pub fn mark<F>(&self, mark: F)
|
|
155
|
+
where
|
|
156
|
+
F: Fn(VALUE),
|
|
157
|
+
{
|
|
158
|
+
self.gc_guard.acquire_as_gc(|| {
|
|
159
|
+
for item in self.buffer.iter() {
|
|
160
|
+
mark(item.data.get());
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
use std::sync::{Condvar, Mutex};
|
|
2
|
+
|
|
3
|
+
pub(crate) struct Semaphore {
|
|
4
|
+
// Wrap the state in a heap-allocated Box so the raw pointer `self.inner` remains stable
|
|
5
|
+
inner: *mut SemaphoreInner,
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
struct SemaphoreInner {
|
|
9
|
+
lock: Mutex<u32>, // the current count of permits
|
|
10
|
+
cvar: Condvar,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
impl Semaphore {
|
|
14
|
+
pub(crate) fn alloc() -> Self {
|
|
15
|
+
Self {
|
|
16
|
+
inner: std::ptr::null_mut(),
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
pub(crate) fn init(&mut self, initial: u32) {
|
|
21
|
+
let inner_struct = SemaphoreInner {
|
|
22
|
+
lock: Mutex::new(initial),
|
|
23
|
+
cvar: Condvar::new(),
|
|
24
|
+
};
|
|
25
|
+
// Box into raw pointer to manage memory life manually
|
|
26
|
+
self.inner = Box::into_raw(Box::new(inner_struct));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub(crate) fn post(&self) {
|
|
30
|
+
if self.inner.is_null() {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let inner = unsafe { &*self.inner };
|
|
35
|
+
let mut count = inner.lock.lock().unwrap();
|
|
36
|
+
*count += 1;
|
|
37
|
+
|
|
38
|
+
// notify to wake up a waiting thread
|
|
39
|
+
inner.cvar.notify_one();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(crate) fn wait(&self) {
|
|
43
|
+
if self.inner.is_null() {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let inner = unsafe { &*self.inner };
|
|
48
|
+
let mut count = inner.lock.lock().unwrap();
|
|
49
|
+
|
|
50
|
+
// Block the thread while there are no available permits
|
|
51
|
+
while *count == 0 {
|
|
52
|
+
count = inner.cvar.wait(count).unwrap();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
*count -= 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
impl Drop for Semaphore {
|
|
60
|
+
fn drop(&mut self) {
|
|
61
|
+
if !self.inner.is_null() {
|
|
62
|
+
unsafe {
|
|
63
|
+
// Safely reclaim and drop the heap allocated struct
|
|
64
|
+
let _ = Box::from_raw(self.inner);
|
|
65
|
+
}
|
|
66
|
+
self.inner = std::ptr::null_mut();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
unsafe impl Send for Semaphore {}
|
|
72
|
+
unsafe impl Sync for Semaphore {}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ratomic
|
|
4
|
+
# Ruby convenience methods for {Counter}.
|
|
5
|
+
module CounterMethods
|
|
6
|
+
# Read the current counter value.
|
|
7
|
+
#
|
|
8
|
+
# @return [Integer]
|
|
9
|
+
def value
|
|
10
|
+
read
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Coerce the counter to an Integer snapshot.
|
|
14
|
+
#
|
|
15
|
+
# @return [Integer]
|
|
16
|
+
def to_i
|
|
17
|
+
read
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Check whether the current counter value is zero.
|
|
21
|
+
#
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
def zero?
|
|
24
|
+
read.zero?
|
|
25
|
+
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
|
+
end
|
|
49
|
+
|
|
50
|
+
Counter.prepend(CounterMethods)
|
|
51
|
+
end
|
data/lib/ratomic/map.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ratomic
|
|
4
|
+
# A Ractor-shareable concurrent map.
|
|
5
|
+
#
|
|
6
|
+
# Map is a public alias for the native ConcurrentHashMap class. It is suitable
|
|
7
|
+
# for runtime state with shareable keys and values that are safe to access
|
|
8
|
+
# from multiple Ractors, such as counters or immutable offsets.
|
|
9
|
+
#
|
|
10
|
+
# This is not a full Hash replacement. Iteration and arbitrary mutable object
|
|
11
|
+
# borrowing are intentionally absent.
|
|
12
|
+
#
|
|
13
|
+
# @example Store pipeline offsets
|
|
14
|
+
# OFFSETS = Ratomic::Map.new
|
|
15
|
+
# OFFSETS[:source_a] = 42
|
|
16
|
+
# OFFSETS[:source_a] # => 42
|
|
17
|
+
Map = ConcurrentHashMap
|
|
18
|
+
|
|
19
|
+
# Ruby convenience methods for {Map}.
|
|
20
|
+
module MapMethods
|
|
21
|
+
# Set a value for +key+.
|
|
22
|
+
#
|
|
23
|
+
# @param key [Object]
|
|
24
|
+
# @param value [Object]
|
|
25
|
+
# @return [void]
|
|
26
|
+
def []=(key, value)
|
|
27
|
+
set(key, value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Read a value by +key+.
|
|
31
|
+
#
|
|
32
|
+
# Missing keys currently return nil, so storing nil is ambiguous.
|
|
33
|
+
#
|
|
34
|
+
# @param key [Object]
|
|
35
|
+
# @return [Object, nil]
|
|
36
|
+
def [](key)
|
|
37
|
+
get(key)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Alias for #size.
|
|
41
|
+
#
|
|
42
|
+
# @return [Integer]
|
|
43
|
+
def length
|
|
44
|
+
size
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check whether the map currently has no entries.
|
|
48
|
+
#
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def empty?
|
|
51
|
+
size.zero?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
ConcurrentHashMap.prepend(MapMethods)
|
|
56
|
+
end
|
data/lib/ratomic/pool.rb
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ratomic
|
|
4
|
+
# A Ractor-safe ownership-transfer pool for mutable Ruby objects.
|
|
5
|
+
#
|
|
6
|
+
# Pool follows a Rust-inspired ownership-transfer model: a pooled object has
|
|
7
|
+
# one active owner at a time. #checkout moves ownership from the pool to the
|
|
8
|
+
# caller; #checkin moves ownership back to the pool. Ruby enforces stale
|
|
9
|
+
# caller references dynamically with Ractor::MovedError.
|
|
10
|
+
#
|
|
11
|
+
# This is ownership transfer, not borrowing. Pool never lends shared mutable
|
|
12
|
+
# references across Ractors.
|
|
13
|
+
#
|
|
14
|
+
# Pool uses a private coordinator Ractor and caller-owned Ractor::Port reply
|
|
15
|
+
# ports. Objects are moved to callers on checkout and moved back to the pool
|
|
16
|
+
# on checkin. This is intentionally different from sharing the same mutable
|
|
17
|
+
# object between Ractors: at any instant, exactly one Ractor owns a checked-out
|
|
18
|
+
# object.
|
|
19
|
+
#
|
|
20
|
+
# @example Reuse mutable buffers safely
|
|
21
|
+
# BUFFERS = Ratomic::Pool.new(4, 1.0) { [] }
|
|
22
|
+
# BUFFERS.with do |buffer|
|
|
23
|
+
# buffer.clear
|
|
24
|
+
# buffer << :change
|
|
25
|
+
# end
|
|
26
|
+
class Pool
|
|
27
|
+
# Create a pool and seed it with +size+ objects from the factory block.
|
|
28
|
+
#
|
|
29
|
+
# @param size [Integer] number of pooled objects
|
|
30
|
+
# @param timeout [Numeric, nil] checkout timeout in seconds, or nil to wait indefinitely
|
|
31
|
+
# @yieldreturn [Object] mutable object to store in the pool
|
|
32
|
+
# @raise [ArgumentError] if +size+ is not positive
|
|
33
|
+
# @raise [LocalJumpError] if no factory block is given
|
|
34
|
+
def initialize(size = 5, timeout = 1.0)
|
|
35
|
+
raise ArgumentError, "pool size must be positive" if size <= 0
|
|
36
|
+
raise LocalJumpError, "no block given" unless block_given?
|
|
37
|
+
|
|
38
|
+
@timeout = timeout&.to_f
|
|
39
|
+
@control = self.class.send(:new_control_ractor)
|
|
40
|
+
size.times { @control.send([:checkin, yield], move: true) }
|
|
41
|
+
freeze
|
|
42
|
+
Ractor.make_shareable(self)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Checkout one object from the pool.
|
|
46
|
+
#
|
|
47
|
+
# The returned object has been moved from the pool to the caller. The caller
|
|
48
|
+
# owns it until it is passed to #checkin.
|
|
49
|
+
#
|
|
50
|
+
# @return [Object, nil] pooled object, or nil after timeout
|
|
51
|
+
def checkout
|
|
52
|
+
reply = Ractor::Port.new
|
|
53
|
+
request_id = reply.object_id
|
|
54
|
+
@control << [:checkout, request_id, reply]
|
|
55
|
+
receive_checkout_reply(reply)
|
|
56
|
+
rescue Timeout::Error
|
|
57
|
+
nil
|
|
58
|
+
ensure
|
|
59
|
+
@control << [:cancel, request_id] if request_id
|
|
60
|
+
reply&.close unless reply&.closed?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Return an object to the pool.
|
|
64
|
+
#
|
|
65
|
+
# This moves ownership from the caller back to the pool. The caller must not
|
|
66
|
+
# use the object after calling this method; Ruby raises Ractor::MovedError
|
|
67
|
+
# for stale references.
|
|
68
|
+
#
|
|
69
|
+
# @param object [Object] previously checked-out pooled object
|
|
70
|
+
# @return [nil]
|
|
71
|
+
def checkin(object)
|
|
72
|
+
@control.send([:checkin, object], move: true)
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Stop the private coordinator Ractor.
|
|
77
|
+
#
|
|
78
|
+
# This is primarily useful for tests and short-lived scripts. A closed pool
|
|
79
|
+
# should not be used for further checkout/checkin operations.
|
|
80
|
+
#
|
|
81
|
+
# @return [nil]
|
|
82
|
+
def close
|
|
83
|
+
@control << [:shutdown]
|
|
84
|
+
@control.value
|
|
85
|
+
nil
|
|
86
|
+
rescue Ractor::ClosedError, Ractor::Error
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Checkout an object, yield it, then move it back to the pool.
|
|
91
|
+
#
|
|
92
|
+
# This is the preferred API because it guarantees checkin through an ensure
|
|
93
|
+
# block. If checkout times out, raises Ratomic::Error and does not yield.
|
|
94
|
+
#
|
|
95
|
+
# @yieldparam object [Object] checked-out pooled object
|
|
96
|
+
# @raise [Ratomic::Error] if checkout times out
|
|
97
|
+
# @return [Object] block return value
|
|
98
|
+
def with
|
|
99
|
+
object = checkout
|
|
100
|
+
raise Ratomic::Error, "pool checkout timeout" if object.nil?
|
|
101
|
+
|
|
102
|
+
yield object
|
|
103
|
+
ensure
|
|
104
|
+
checkin(object) unless object.nil?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.new_control_ractor
|
|
108
|
+
Ractor.new { Ratomic::Pool.send(:run_control_loop) }
|
|
109
|
+
end
|
|
110
|
+
private_class_method :new_control_ractor
|
|
111
|
+
|
|
112
|
+
def self.run_control_loop
|
|
113
|
+
available = []
|
|
114
|
+
waiting = {}
|
|
115
|
+
|
|
116
|
+
loop do
|
|
117
|
+
command, *args = Ractor.receive
|
|
118
|
+
break if handle_command(command, args, available, waiting) == :shutdown
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
private_class_method :run_control_loop
|
|
122
|
+
|
|
123
|
+
def self.handle_command(command, args, available, waiting)
|
|
124
|
+
case command
|
|
125
|
+
when :checkout
|
|
126
|
+
handle_checkout(args, available, waiting)
|
|
127
|
+
when :checkin
|
|
128
|
+
handle_checkin(args.fetch(0), available, waiting)
|
|
129
|
+
when :cancel
|
|
130
|
+
waiting.delete(args.fetch(0))
|
|
131
|
+
when :shutdown
|
|
132
|
+
:shutdown
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
private_class_method :handle_command
|
|
136
|
+
|
|
137
|
+
def self.handle_checkout(args, available, waiting)
|
|
138
|
+
request_id, reply = args
|
|
139
|
+
if (object = available.shift)
|
|
140
|
+
reply.send(object, move: true)
|
|
141
|
+
else
|
|
142
|
+
waiting[request_id] = reply
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
private_class_method :handle_checkout
|
|
146
|
+
|
|
147
|
+
def self.handle_checkin(object, available, waiting)
|
|
148
|
+
loop do
|
|
149
|
+
_request_id, reply = waiting.shift
|
|
150
|
+
return available << object if reply.nil?
|
|
151
|
+
|
|
152
|
+
begin
|
|
153
|
+
reply.send(object, move: true)
|
|
154
|
+
return
|
|
155
|
+
rescue Ractor::ClosedError
|
|
156
|
+
next
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
private_class_method :handle_checkin
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def receive_checkout_reply(reply)
|
|
165
|
+
return reply.receive unless @timeout
|
|
166
|
+
|
|
167
|
+
Timeout.timeout(@timeout) { reply.receive }
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ratomic
|
|
4
|
+
# Ruby convenience methods for {Queue}.
|
|
5
|
+
module QueueMethods
|
|
6
|
+
# Push an item and return the queue for chaining.
|
|
7
|
+
#
|
|
8
|
+
# @param item [Object]
|
|
9
|
+
# @return [Ratomic::Queue]
|
|
10
|
+
def <<(item)
|
|
11
|
+
push(item)
|
|
12
|
+
self
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
Queue.prepend(QueueMethods)
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ratomic
|
|
4
|
+
# Internal sentinel object for future Hash-like APIs that need to distinguish
|
|
5
|
+
# missing keys from explicit nil values.
|
|
6
|
+
class Undefined
|
|
7
|
+
# @return [String]
|
|
8
|
+
def inspect
|
|
9
|
+
"#<Undefined>"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Internal shareable missing-value sentinel.
|
|
14
|
+
UNDEFINED = Ractor.make_shareable(Undefined.new)
|
|
15
|
+
end
|
data/lib/ratomic/version.rb
CHANGED
data/lib/ratomic.rb
CHANGED
|
@@ -1,68 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
3
5
|
require_relative "ratomic/version"
|
|
4
6
|
require_relative "ratomic/ratomic"
|
|
7
|
+
require_relative "ratomic/counter"
|
|
8
|
+
require_relative "ratomic/undefined"
|
|
9
|
+
require_relative "ratomic/pool"
|
|
10
|
+
require_relative "ratomic/map"
|
|
11
|
+
require_relative "ratomic/queue"
|
|
5
12
|
|
|
6
13
|
module Ratomic
|
|
14
|
+
# Base error for Ratomic-specific runtime failures.
|
|
7
15
|
class Error < StandardError; end
|
|
8
|
-
|
|
9
|
-
##
|
|
10
|
-
# An atomic counter which can be incremented and decremented
|
|
11
|
-
# safely by multiple Ractors concurrently.
|
|
12
|
-
class Counter
|
|
13
|
-
def value
|
|
14
|
-
read
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def inc(amt = 1)
|
|
18
|
-
raise ArgumentError, "amount must be positive: #{amt}" if amt < 0
|
|
19
|
-
increment(amt)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def dec(amt = 1)
|
|
23
|
-
raise ArgumentError, "amount must be positive: #{amt}" if amt < 0
|
|
24
|
-
decrement(amt)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
class Undefined
|
|
29
|
-
def inspect
|
|
30
|
-
"#<Undefined>"
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
UNDEFINED = Ractor.make_shareable(Undefined.new)
|
|
34
|
-
|
|
35
|
-
class Pool < FixedSizeObjectPool
|
|
36
|
-
def initialize(size = 5, timeout = 1.0)
|
|
37
|
-
super(size, (timeout * 1000).to_i)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def with
|
|
41
|
-
obj_and_idx = checkout
|
|
42
|
-
if obj_and_idx.nil?
|
|
43
|
-
raise Ratomic::Error, "pool checkout timeout"
|
|
44
|
-
else
|
|
45
|
-
yield obj_and_idx[0]
|
|
46
|
-
end
|
|
47
|
-
ensure
|
|
48
|
-
unless obj_and_idx.nil?
|
|
49
|
-
checkin(obj_and_idx[1])
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
class Map < ConcurrentHashMap
|
|
55
|
-
def []=(key, value)
|
|
56
|
-
set(key, value)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def [](key)
|
|
60
|
-
get(key)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# TODO add as much of the Hash API as possible.
|
|
64
|
-
# Stretch goal? Support Enumerable if DashMap can safely
|
|
65
|
-
# iterate.
|
|
66
|
-
end
|
|
67
|
-
|
|
68
16
|
end
|
data/ratomic.gemspec
CHANGED
|
@@ -5,35 +5,41 @@ require_relative "lib/ratomic/version"
|
|
|
5
5
|
Gem::Specification.new do |spec|
|
|
6
6
|
spec.name = "ratomic"
|
|
7
7
|
spec.version = Ratomic::VERSION
|
|
8
|
-
spec.authors = ["Mike Perham"]
|
|
8
|
+
spec.authors = ["Mike Perham", "Ken C. Demanawa"]
|
|
9
9
|
spec.email = ["mike@perham.net"]
|
|
10
|
+
spec.metadata["maintainers"] = "Ken C. Demanawa"
|
|
10
11
|
|
|
11
12
|
spec.summary = "Mutable data structures for Ractors"
|
|
12
13
|
spec.description = spec.summary
|
|
13
14
|
spec.homepage = "https://github.com/mperham/ratomic"
|
|
14
15
|
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = ">=
|
|
16
|
+
spec.required_ruby_version = ">= 4.0.0"
|
|
16
17
|
|
|
17
18
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
19
|
spec.metadata["source_code_uri"] = "https://github.com/mperham/ratomic"
|
|
19
20
|
spec.metadata["changelog_uri"] = "https://github.com/mperham/ratomic"
|
|
21
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
20
22
|
|
|
21
23
|
# Specify which files should be added to the gem when it is released.
|
|
22
24
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
spec.files = [
|
|
26
|
+
"Cargo.lock",
|
|
27
|
+
"Cargo.toml",
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"LICENSE.txt",
|
|
30
|
+
"README.md",
|
|
31
|
+
"ratomic.gemspec"
|
|
32
|
+
] + Dir[
|
|
33
|
+
"lib/**/*.rb",
|
|
34
|
+
"ext/ratomic/Cargo.toml",
|
|
35
|
+
"ext/ratomic/build.rs",
|
|
36
|
+
"ext/ratomic/extconf.rb",
|
|
37
|
+
"ext/ratomic/src/**/*.rs"
|
|
38
|
+
]
|
|
32
39
|
spec.require_paths = ["lib"]
|
|
33
40
|
spec.extensions = ["ext/ratomic/extconf.rb"]
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
# spec.add_dependency "example-gem", "~> 1.0"
|
|
42
|
+
spec.add_dependency "rb_sys", "~> 0.9.128"
|
|
37
43
|
|
|
38
44
|
# For more information and examples about making a new gem, check out our
|
|
39
45
|
# guide at: https://bundler.io/guides/creating_gem.html
|