ratomic 0.2.1 → 0.3.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4ca3b68838c5f869284756b01f00e56960cbb5dff8e640efc03682d10d28a50
4
- data.tar.gz: 959794b19074d1ecfba5bdb08223ba372a6443fbc96fe833d0ce324aa3c52d5e
3
+ metadata.gz: 5196316489486d3034c046db087ae5878dc7fcf2f2e3d4086d91c128f8a30dd5
4
+ data.tar.gz: 6bf7a6aed05756ff9a0f49f274d69ed91b1f722930c94a140cc5450746fec3f2
5
5
  SHA512:
6
- metadata.gz: 9697f964c65bdc3a951da84836ebadf947d370d74efd258015fb5401267acd6d78248de238f9da6db60edecccbe0a8bbd83a327e424d6fb6e74066ee60fe16de
7
- data.tar.gz: 8bbbba4064050e577b23b48e8363653b5438ea157a68cffc607a53588e2785fdc9c2d5879d846593fa9f2757667df1df77784811c239b4f5b5d9cc684f00ae74
6
+ metadata.gz: f0276e6c7e78c7bdcbabb3839d59eb70e58289f3662fe1df244fce9abc9b3a1eeb3454a4116081cd0f0426e470ed64a3d0108f8a57650455e17cc96f111f51e7
7
+ data.tar.gz: 8eb3b4c4f248569568ea7394a89156961aeb8108b0448c5ef3012045486537cd3c862fff684ac39192d2c31842159e5ad798b9005ce48909281f19d1c9043827
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.0] - 2026-06-05
4
+
5
+ - Promote the DashMap-backed `Ratomic::Map` API as the primary concurrent Hash primitive.
6
+ - Add `Ratomic::Map#key?`, `#include?`, `#member?`, and `#delete`.
7
+ - Add `Ratomic::Map#fetch`, `#compute`, `#fetch_or_store`, and `#upsert` for atomic per-key workflows.
8
+ - Add `Ratomic::Map#increment`, `#decrement`, `#append`, and `#add_to_set` convenience methods for shared counters and bucket-style values.
9
+ - Improve API documentation for `Counter`, `Map`, `Queue`, and `Pool`.
10
+ - Add GitHub Pages deployment for generated YARD API documentation.
11
+ - Add Redis POC scripts for exercising `Ratomic::Map`, `Ratomic::Counter`, and `Ratomic::Pool` under Thread and Ractor workloads.
12
+ - Remove `Counter#inc` and `Counter#dec` in favor of the native `#increment` and `#decrement` methods.
13
+
3
14
  ## [0.2.1] - 2026-06-04
4
15
 
5
16
  - Fix `Ratomic::Queue` slot indexing for non-power-of-two capacities.
data/README.md CHANGED
@@ -2,180 +2,249 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/ratomic.svg)](https://badge.fury.io/rb/ratomic)
4
4
  [![CI](https://github.com/mperham/ratomic/workflows/CI/badge.svg)](https://github.com/mperham/ratomic/actions)
5
- [![Coverage Status](https://codecov.io/gh/mperham/ratomic/branch/main/graph/badge.svg)](https://codecov.io/gh/mperham/ratomic)
6
5
  [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%204.0-ruby.svg)](https://www.ruby-lang.org/en/)
7
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
7
 
8
+ Ratomic provides mutable data structures for Ruby Ractors. Its primitives are backed by
9
+ native Rust concurrency libraries so Ruby code can share useful state across Ractors
10
+ without falling back to one global lock.
9
11
 
10
- Ratomic provides mutable data structures for use with Ruby's Ractors.
11
- This allows Ruby code to scale beyond the infamous GVL.
12
+ ## Project Direction
12
13
 
13
- # HELP WANTED!
14
+ Ratomic focuses on practical Ractor-safe primitives with a small API surface, clear
15
+ ownership semantics, and honest documentation about sharp edges.
14
16
 
15
- > If you know Rust and Ruby C-extensions, we need your help!
16
- > This project is brand new and could use your knowledge!
17
- > If you don't know Rust or C, consider this a challenge to learn and solve.
18
- > Read through the [issues](//github.com/mperham/ratomic/issues) to find work that sounds interesting to you.
17
+ `Ratomic::Map` is the current priority: a Ruby-facing concurrent Hash powered by
18
+ DashMap, with atomic per-key operations designed for real Ractor workloads.
19
19
 
20
- ## How to contribute
20
+ ## Requirements
21
21
 
22
- Please make sure to understand our [Code of Conduct](./CODE_OF_CONDUCT.md).
22
+ - Ruby 4.0 or newer
23
+ - Bundler
24
+ - Rust toolchain when building the native extension from source
23
25
 
24
- After changing code, you can give it a spin with:
26
+ ## Installation
27
+
28
+ Add Ratomic to your application's Gemfile:
25
29
 
26
30
  ```bash
27
- rake
31
+ bundle add ratomic
28
32
  ```
29
33
 
30
- This should compile the Rust code and run all tests.
31
- The test suite writes a SimpleCov report to `coverage/index.html` so you can see which Ruby wrapper paths are covered.
34
+ Then require it from Ruby:
32
35
 
33
- ## Installation
36
+ ```ruby
37
+ require "ratomic"
38
+ ```
34
39
 
35
- Install the gem and add to the application's Gemfile by executing:
40
+ ## Documentation
36
41
 
37
- ```bash
38
- bundle add ratomic
39
- ```
42
+ API documentation is published to GitHub Pages:
40
43
 
41
- ## Usage
44
+ - [mperham.github.io/ratomic](https://mperham.github.io/ratomic/)
45
+
46
+ ## Examples And Benchmarks
42
47
 
43
- Ratomic provides several useful Ractor-safe structures.
44
- Note the APIs available are frequently very limited compared to Ruby's broad API.
48
+ - [`redis_poc`](./redis_poc) contains local Redis scripts that exercise
49
+ `Ratomic::Map`, `Ratomic::Counter`, and `Ratomic::Pool` under Thread and
50
+ Ractor workloads.
51
+ - The [`cdc-parallel` Ratomic benchmark][cdc-parallel-ratomic] demonstrates
52
+ Ractor workers updating shared CDC processing metrics through `Ratomic::Map`
53
+ and `Ratomic::Counter`.
45
54
 
46
- These structures are designed for use as class-level constants so they can be shared by numerous Ractors.
55
+ ## Usage
47
56
 
48
- Ratomic has two different safety models:
57
+ Ratomic provides two safety models:
49
58
 
50
- * `Counter`, `Map`, and `Queue` are shared concurrent structures.
51
- * `Pool` transfers ownership of mutable objects between Ractors.
59
+ - `Counter`, `Map`, and `Queue` are shared concurrent structures.
60
+ - `Pool` transfers ownership of mutable objects between Ractors.
52
61
 
53
- That distinction matters. A mutable pooled object is not shared by multiple Ractors at the same time. It is moved to the caller on checkout and moved back to the pool on checkin.
62
+ That distinction matters. A mutable pooled object is not shared by multiple Ractors
63
+ at the same time. It is moved to the caller on checkout and moved back to the pool
64
+ on checkin.
65
+
66
+ These structures are designed for use as class-level constants so they can be
67
+ shared by many Ractors.
54
68
 
55
69
  ### `Ratomic::Counter`
56
70
 
71
+ `Ratomic::Counter` is a Ractor-shareable atomic counter.
72
+
57
73
  ```ruby
58
- c = Ratomic::Counter.new
59
- c.read # => 0
60
- c.inc
61
- c.inc(5)
62
- c.dec(1)
63
- c.dec
64
- c.read # => 4
65
- c.to_i # => 4
66
- c.zero? # => false
74
+ counter = Ratomic::Counter.new
75
+
76
+ counter.read # => 0
77
+ counter.increment(1)
78
+ counter.increment(5)
79
+ counter.decrement(1)
80
+ counter.decrement(1)
81
+
82
+ counter.read # => 4
83
+ counter.to_i # => 4
84
+ counter.zero? # => false
67
85
  ```
68
86
 
69
- ### `Ratomic::Pool`
87
+ ### `Ratomic::Map`
70
88
 
71
- A Ractor-safe object pool:
89
+ `Ratomic::Map` is a Ractor-safe concurrent Hash backed by Rust's DashMap. It is
90
+ not a full `Hash` replacement; iteration and arbitrary mutable object borrowing
91
+ are intentionally absent.
72
92
 
73
93
  ```ruby
74
- POOL = Ratomic::Pool.new(5, 1.0) { [] }
75
- POOL.with do |obj|
76
- # do something with obj
77
- obj << "work"
78
- end
94
+ OFFSETS = Ratomic::Map.new
95
+
96
+ OFFSETS["mike"] = 123
97
+ OFFSETS["mike"] # => 123
98
+ OFFSETS.key?("mike") # => true
99
+ OFFSETS.fetch("missing", 0) # => 0
100
+
101
+ OFFSETS.fetch_or_store("count") { 0 } # => 0
102
+ OFFSETS.compute("mike") { |value| value + 1 } # => 124
103
+ OFFSETS.upsert("mike", 1) { |value| value + 1 } # => 125
104
+ OFFSETS.fetch_and_modify("mike") { |value| value + 1 }
105
+
106
+ OFFSETS.delete("mike") # => 126
107
+ OFFSETS.length
108
+ OFFSETS.empty?
109
+ OFFSETS.clear
110
+ ```
111
+
112
+ `Map` also includes atomic convenience methods for common bucket patterns:
113
+
114
+ ```ruby
115
+ counts = Ratomic::Map.new
116
+ counts.increment("jobs") # => 1
117
+ counts.decrement("jobs") # => 0
118
+
119
+ groups = Ratomic::Map.new
120
+ groups.append("jobs", "import") # => ["import"]
121
+ groups.add_to_set("workers", "alpha") # => #<Set: {"alpha"}>
79
122
  ```
80
123
 
81
- `Pool` is an ownership-transfer pool for mutable Ruby objects. It uses Ruby 4's `Ractor::Port` and `move: true` semantics so only one Ractor owns a checked-out object at a time.
124
+ ### `Ratomic::Queue`
125
+
126
+ `Ratomic::Queue` is a Ractor-shareable multi-producer, multi-consumer queue.
82
127
 
83
- This design addresses [issue #5](https://github.com/mperham/ratomic/issues/5), where using a pooled object after `with` could lead to memory corruption or a process crash. The fix is Rust-inspired ownership transfer, not Rust's full borrow checker: Ruby enforces the boundary dynamically at runtime through Ractor move semantics, while Rust enforces ownership and borrowing statically at compile time.
128
+ ```ruby
129
+ queue = Ratomic::Queue.new(128)
84
130
 
85
- In that model:
131
+ queue.push("hello")
132
+ queue << "world"
86
133
 
87
- * the Rust owner maps to the Ractor that currently checked out the pooled object
88
- * the Rust move maps to `Ractor::Port#send(..., move: true)`
89
- * Rust's "cannot use after move" rule maps to Ruby raising `Ractor::MovedError`
90
- * borrowing is not modeled; `Pool` transfers ownership instead of lending references
134
+ queue.size # => 2
135
+ queue.empty? # => false
136
+ queue.peek # => "hello"
137
+ queue.pop # => "hello"
138
+ queue.pop # => "world"
139
+ queue.empty? # => true
140
+ ```
141
+
142
+ The `.new(capacity)` method initializes the queue with a fixed-size buffer.
143
+ Capacity must be at least `1` and at most `2**20`. Non-power-of-two capacities
144
+ are supported exactly.
145
+
146
+ Since `Ratomic::Queue` is concurrent, `size`, `empty?`, and `peek` are
147
+ moment-in-time observations. Their results may already be stale by the time your
148
+ code uses them.
149
+
150
+ ### `Ratomic::Pool`
151
+
152
+ `Ratomic::Pool` is a Ractor-safe ownership-transfer pool for mutable Ruby objects.
153
+
154
+ ```ruby
155
+ BUFFERS = Ratomic::Pool.new(5, 1.0) { [] }
156
+
157
+ BUFFERS.with do |buffer|
158
+ buffer.clear
159
+ buffer << "work"
160
+ end
161
+ ```
162
+
163
+ `Pool` uses Ruby 4's `Ractor::Port` and `move: true` semantics so only one
164
+ Ractor owns a checked-out object at a time.
91
165
 
92
166
  When an object is checked out:
93
167
 
94
- * the pool moves the object to the caller
95
- * the caller can mutate the object while it owns it
96
- * the pool cannot hand that object to another Ractor until it is checked in
168
+ - the pool moves the object to the caller
169
+ - the caller can mutate the object while it owns it
170
+ - the pool cannot hand that object to another Ractor until it is checked in
97
171
 
98
172
  When an object is checked in:
99
173
 
100
- * ownership moves back to the pool
101
- * stale references held by the caller become moved objects
102
- * using those stale references raises `Ractor::MovedError`
174
+ - ownership moves back to the pool
175
+ - stale references held by the caller become moved objects
176
+ - using those stale references raises `Ractor::MovedError`
103
177
 
104
- This means incorrect usage fails at the Ruby object-ownership boundary rather than allowing two Ractors to mutate the same object concurrently.
178
+ This means incorrect usage fails at the Ruby object-ownership boundary rather
179
+ than allowing two Ractors to mutate the same object concurrently.
105
180
 
106
181
  ```ruby
107
182
  outside = nil
108
183
 
109
- POOL.with do |obj|
110
- outside = obj
111
- obj << "inside"
184
+ BUFFERS.with do |buffer|
185
+ outside = buffer
186
+ buffer << "inside"
112
187
  end
113
188
 
114
189
  outside << "outside"
115
190
  # raises Ractor::MovedError
116
191
  ```
117
192
 
118
- The lower-level `Ratomic::FixedSizeObjectPool` native class may still exist, but `Ratomic::Pool` does not inherit from it. The public `Pool` API is implemented in Ruby so it can use Ruby's Ractor ownership primitives directly.
119
-
120
- Manual checkout/checkin is also supported:
193
+ Manual checkout and checkin are also supported:
121
194
 
122
195
  ```ruby
123
- obj = POOL.checkout
124
- raise "pool checkout timeout" if obj.nil?
196
+ buffer = BUFFERS.checkout
197
+ raise "pool checkout timeout" if buffer.nil?
125
198
 
126
199
  begin
127
- obj << "manual work"
200
+ buffer << "manual work"
128
201
  ensure
129
- POOL.checkin(obj) if obj
202
+ BUFFERS.checkin(buffer) if buffer
130
203
  end
131
204
  ```
132
205
 
133
- `checkout` returns `nil` if no pooled object becomes available before the configured timeout. `with` raises `Ratomic::Error` in that case.
206
+ `checkout` returns `nil` if no pooled object becomes available before the
207
+ configured timeout. `with` raises `Ratomic::Error` in that case.
134
208
 
135
- ### `Ratomic::Map`
209
+ `Pool` uses ownership transfer, not Rust's full borrow checker:
136
210
 
137
- A Ractor-safe map/hash structure:
211
+ - the Rust owner maps to the Ractor that currently checked out the pooled object
212
+ - the Rust move maps to `Ractor::Port#send(..., move: true)`
213
+ - Rust's "cannot use after move" rule maps to Ruby raising `Ractor::MovedError`
214
+ - borrowing is not modeled; `Pool` transfers ownership instead of lending references
138
215
 
139
- ```ruby
140
- HASH = Ratomic::Map.new
141
- HASH["mike"] = 123
142
- HASH["mike"] # => 123
143
- HASH.fetch_and_modify("mike") { |value| value + 1 }
144
- HASH.length
145
- HASH.empty?
146
- HASH.clear
147
- ```
216
+ This design addresses [issue #5](https://github.com/mperham/ratomic/issues/5),
217
+ where using a pooled object after `with` could lead to memory corruption or a
218
+ process crash.
148
219
 
149
- ### `Ratomic::Queue`
220
+ The lower-level `Ratomic::FixedSizeObjectPool` native class may still exist, but
221
+ `Ratomic::Pool` does not inherit from it. The public `Pool` API is implemented
222
+ in Ruby so it can use Ruby's Ractor ownership primitives directly.
150
223
 
151
- A multi-producer, multi-consumer queue.
224
+ ## Contributing
152
225
 
153
- ```ruby
154
- q = Ratomic::Queue.new(128)
226
+ Please read the [Code of Conduct](./CODE_OF_CONDUCT.md) before contributing.
155
227
 
156
- q.push("hello")
157
- q << "world"
228
+ After changing code, run:
158
229
 
159
- q.size # => 2
160
- q.empty? # => false
161
- q.peek # => "hello"
162
- item = q.pop # => "hello"
163
- item = q.pop # => "world"
164
- q.empty? # => true
230
+ ```bash
231
+ rake
165
232
  ```
166
- The `.new(capacity)` method initializes the queue with a fixed-size buffer. The capacity must be greater than or equal to 1 and less than or equal to 2<sup>20</sup>.
167
-
168
- Values that are not a power of two are rounded up to the nearest greater power of two, which enables efficient indexing and wrap-around calculations in the underlying buffer.
169
233
 
170
- Since `Ratomic::Queue` is a concurrent queue, the `size`, `empty?`, and `peek` methods provide only a best-effort guess — the values they return might be stale or incorrect.
234
+ This compiles the Rust code and runs the test suite. The test suite writes a
235
+ SimpleCov report to `coverage/index.html` for the Ruby wrapper paths.
171
236
 
172
237
  ## Thanks
173
238
 
174
- [Ilya Bylich](https://github.com/iliabylich) wrote and documented his original research at [Ruby, Ractors, and Lock-free Data Structures/](https://iliabylich.github.io/ruby-ractors-and-lock-free-data-structures/).
175
- Thank you for your impressive work, Ilya!
239
+ [Ilya Bylich](https://github.com/iliabylich) wrote and documented his original
240
+ research at [Ruby, Ractors, and Lock-free Data Structures][ractor-research].
176
241
 
177
- This repo is further research into the usability and limitations of Ractor-friendly structures in Ruby code and gems.
242
+ This repo continues that research into the usability and limitations of
243
+ Ractor-friendly structures in Ruby code and gems.
178
244
 
179
245
  ## License
180
246
 
181
247
  [MIT License](https://opensource.org/licenses/MIT).
248
+
249
+ [cdc-parallel-ratomic]: https://github.com/kanutocd/cdc-parallel/blob/main/benchmark/RATOMIC.md
250
+ [ractor-research]: https://iliabylich.github.io/ruby-ractors-and-lock-free-data-structures/
@@ -1,3 +1,4 @@
1
+ use dashmap::mapref::entry::Entry;
1
2
  use rb_sys::{rb_eql, rb_hash, VALUE};
2
3
 
3
4
  #[derive(Debug)]
@@ -18,11 +19,11 @@ impl std::hash::Hash for RubyHashEql {
18
19
  }
19
20
  }
20
21
 
21
- pub struct ConcurrentHashMap {
22
+ pub struct MapStore {
22
23
  map: dashmap::DashMap<RubyHashEql, VALUE>,
23
24
  }
24
25
 
25
- impl ConcurrentHashMap {
26
+ impl MapStore {
26
27
  pub fn new() -> Self {
27
28
  Self {
28
29
  map: dashmap::DashMap::new(),
@@ -33,10 +34,18 @@ impl ConcurrentHashMap {
33
34
  self.map.get(&RubyHashEql(key)).map(|v| *v)
34
35
  }
35
36
 
37
+ pub fn contains_key(&self, key: VALUE) -> bool {
38
+ self.map.contains_key(&RubyHashEql(key))
39
+ }
40
+
36
41
  pub fn set(&self, key: VALUE, value: VALUE) {
37
42
  self.map.insert(RubyHashEql(key), value);
38
43
  }
39
44
 
45
+ pub fn delete(&self, key: VALUE) -> Option<VALUE> {
46
+ self.map.remove(&RubyHashEql(key)).map(|(_, value)| value)
47
+ }
48
+
40
49
  pub fn clear(&self) {
41
50
  self.map.clear()
42
51
  }
@@ -52,6 +61,73 @@ impl ConcurrentHashMap {
52
61
  self.map.alter(&RubyHashEql(key), |_, value| f(value));
53
62
  }
54
63
 
64
+ pub fn compute<F, E>(&self, key: VALUE, missing: VALUE, f: F) -> Result<VALUE, E>
65
+ where
66
+ F: FnOnce(VALUE) -> Result<VALUE, E>,
67
+ {
68
+ match self.map.entry(RubyHashEql(key)) {
69
+ Entry::Occupied(mut entry) => {
70
+ let new_value = f(*entry.get())?;
71
+ entry.insert(new_value);
72
+ Ok(new_value)
73
+ }
74
+ Entry::Vacant(entry) => {
75
+ let new_value = f(missing)?;
76
+ entry.insert(new_value);
77
+ Ok(new_value)
78
+ }
79
+ }
80
+ }
81
+
82
+ pub fn update<F, E>(&self, key: VALUE, f: F) -> Result<VALUE, E>
83
+ where
84
+ F: FnOnce(Option<VALUE>) -> Result<VALUE, E>,
85
+ {
86
+ match self.map.entry(RubyHashEql(key)) {
87
+ Entry::Occupied(mut entry) => {
88
+ let new_value = f(Some(*entry.get()))?;
89
+ entry.insert(new_value);
90
+ Ok(new_value)
91
+ }
92
+ Entry::Vacant(entry) => {
93
+ let new_value = f(None)?;
94
+ entry.insert(new_value);
95
+ Ok(new_value)
96
+ }
97
+ }
98
+ }
99
+
100
+ pub fn fetch_or_store<F, E>(&self, key: VALUE, f: F) -> Result<VALUE, E>
101
+ where
102
+ F: FnOnce() -> Result<VALUE, E>,
103
+ {
104
+ match self.map.entry(RubyHashEql(key)) {
105
+ Entry::Occupied(entry) => Ok(*entry.get()),
106
+ Entry::Vacant(entry) => {
107
+ let value = f()?;
108
+ entry.insert(value);
109
+ Ok(value)
110
+ }
111
+ }
112
+ }
113
+
114
+ pub fn upsert<F, E>(&self, key: VALUE, initial: VALUE, f: F) -> Result<VALUE, E>
115
+ where
116
+ F: FnOnce(VALUE) -> Result<VALUE, E>,
117
+ {
118
+ match self.map.entry(RubyHashEql(key)) {
119
+ Entry::Occupied(mut entry) => {
120
+ let new_value = f(*entry.get())?;
121
+ entry.insert(new_value);
122
+ Ok(new_value)
123
+ }
124
+ Entry::Vacant(entry) => {
125
+ entry.insert(initial);
126
+ Ok(initial)
127
+ }
128
+ }
129
+ }
130
+
55
131
  pub fn mark<F>(&self, f: F)
56
132
  where
57
133
  F: Fn(VALUE),
@@ -63,7 +139,7 @@ impl ConcurrentHashMap {
63
139
  }
64
140
  }
65
141
 
66
- impl Default for ConcurrentHashMap {
142
+ impl Default for MapStore {
67
143
  fn default() -> Self {
68
144
  Self::new()
69
145
  }
@@ -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 {
@@ -64,7 +64,8 @@ impl DataTypeFunctions for Counter {}
64
64
  unsafe impl TypedData for Counter {
65
65
  fn class(ruby: &Ruby) -> RClass {
66
66
  static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
67
- let class = ruby.define_module("Ratomic")
67
+ let class = ruby
68
+ .define_module("Ratomic")
68
69
  .unwrap()
69
70
  .define_class("Counter", ruby.class_object())
70
71
  .unwrap();
@@ -75,17 +76,18 @@ unsafe impl TypedData for Counter {
75
76
  }
76
77
 
77
78
  fn data_type() -> &'static DataType {
78
- static DATA_TYPE: DataType =
79
- data_type_builder!(Counter, "ratomic/counter").frozen_shareable().build();
79
+ static DATA_TYPE: DataType = data_type_builder!(Counter, "ratomic/counter")
80
+ .frozen_shareable()
81
+ .build();
80
82
  &DATA_TYPE
81
83
  }
82
84
  }
83
85
 
84
- struct HashMap(ConcurrentHashMap);
86
+ struct HashMap(MapStore);
85
87
 
86
88
  impl HashMap {
87
89
  fn new(ruby: &Ruby, class: RClass) -> Result<Value, Error> {
88
- let value = ruby.wrap_as(Self(ConcurrentHashMap::new()), class).as_value();
90
+ let value = ruby.wrap_as(Self(MapStore::new()), class).as_value();
89
91
  make_shareable(ruby, value)
90
92
  }
91
93
 
@@ -94,10 +96,19 @@ impl HashMap {
94
96
  unsafe { value_from_raw(raw) }.into_value_with(ruby)
95
97
  }
96
98
 
99
+ fn contains_key(&self, key: Value) -> bool {
100
+ self.0.contains_key(value_to_raw(key))
101
+ }
102
+
97
103
  fn set(&self, key: Value, value: Value) {
98
104
  self.0.set(value_to_raw(key), value_to_raw(value));
99
105
  }
100
106
 
107
+ fn delete(ruby: &Ruby, rb_self: &Self, key: Value) -> Value {
108
+ let raw = rb_self.0.delete(value_to_raw(key)).unwrap_or_else(qnil_raw);
109
+ unsafe { value_from_raw(raw) }.into_value_with(ruby)
110
+ }
111
+
101
112
  fn clear(&self) {
102
113
  self.0.clear();
103
114
  }
@@ -132,20 +143,113 @@ impl HashMap {
132
143
  Ok(())
133
144
  }
134
145
  }
146
+
147
+ fn compute(ruby: &Ruby, rb_self: &Self, key: Value) -> Result<Value, Error> {
148
+ if !ruby.block_given() {
149
+ return Err(Error::new(
150
+ ruby.exception_local_jump_error(),
151
+ "no block given",
152
+ ));
153
+ }
154
+
155
+ let proc = ruby.block_proc()?;
156
+ let raw = rb_self.0.compute(value_to_raw(key), qnil_raw(), |value| {
157
+ proc.call::<_, Value>((unsafe { value_from_raw(value) },))
158
+ .map(value_to_raw)
159
+ })?;
160
+
161
+ Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
162
+ }
163
+
164
+ fn fetch_or_store(ruby: &Ruby, rb_self: &Self, key: Value) -> Result<Value, Error> {
165
+ if !ruby.block_given() {
166
+ return Err(Error::new(
167
+ ruby.exception_local_jump_error(),
168
+ "no block given",
169
+ ));
170
+ }
171
+
172
+ let proc = ruby.block_proc()?;
173
+ let raw = rb_self.0.fetch_or_store(value_to_raw(key), || {
174
+ proc.call::<_, Value>(()).map(value_to_raw)
175
+ })?;
176
+
177
+ Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
178
+ }
179
+
180
+ fn upsert(ruby: &Ruby, rb_self: &Self, key: Value, initial: Value) -> Result<Value, Error> {
181
+ if !ruby.block_given() {
182
+ return Err(Error::new(
183
+ ruby.exception_local_jump_error(),
184
+ "no block given",
185
+ ));
186
+ }
187
+
188
+ let proc = ruby.block_proc()?;
189
+ let raw = rb_self
190
+ .0
191
+ .upsert(value_to_raw(key), value_to_raw(initial), |value| {
192
+ proc.call::<_, Value>((unsafe { value_from_raw(value) },))
193
+ .map(value_to_raw)
194
+ })?;
195
+
196
+ Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
197
+ }
198
+
199
+ fn increment_numeric(
200
+ ruby: &Ruby,
201
+ rb_self: &Self,
202
+ key: Value,
203
+ by: Value,
204
+ ) -> Result<Value, Error> {
205
+ let key_inspect = key.funcall::<_, _, String>("inspect", ())?;
206
+ let numeric_class: RClass = ruby.class_object().const_get("Numeric")?;
207
+ let raw = rb_self.0.update(value_to_raw(key), |current| {
208
+ let next = match current {
209
+ Some(value) if value == qnil_raw() => {
210
+ return Err(Error::new(
211
+ ruby.exception_type_error(),
212
+ format!("existing value for {key_inspect} must be numeric: nil"),
213
+ ));
214
+ }
215
+ Some(value) => {
216
+ let old_value = unsafe { value_from_raw(value) };
217
+ if !old_value.funcall::<_, _, bool>("is_a?", (numeric_class,))? {
218
+ let old_value_inspect = old_value.funcall::<_, _, String>("inspect", ())?;
219
+ return Err(Error::new(
220
+ ruby.exception_type_error(),
221
+ format!(
222
+ "existing value for {key_inspect} must be numeric: {old_value_inspect}"
223
+ ),
224
+ ));
225
+ }
226
+
227
+ old_value.funcall::<_, _, Value>("+", (by,))?
228
+ }
229
+ None => by,
230
+ };
231
+
232
+ Ok(value_to_raw(next))
233
+ })?;
234
+
235
+ Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
236
+ }
135
237
  }
136
238
 
137
239
  impl DataTypeFunctions for HashMap {
138
240
  fn mark(&self, marker: &magnus::gc::Marker) {
139
- self.0.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
241
+ self.0
242
+ .mark(|value| marker.mark(unsafe { value_from_raw(value) }));
140
243
  }
141
244
  }
142
245
 
143
246
  unsafe impl TypedData for HashMap {
144
247
  fn class(ruby: &Ruby) -> RClass {
145
248
  static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
146
- let class = ruby.define_module("Ratomic")
249
+ let class = ruby
250
+ .define_module("Ratomic")
147
251
  .unwrap()
148
- .define_class("ConcurrentHashMap", ruby.class_object())
252
+ .define_class("Map", ruby.class_object())
149
253
  .unwrap();
150
254
  class.undef_default_alloc_func();
151
255
  class
@@ -245,14 +349,16 @@ impl Queue {
245
349
 
246
350
  impl DataTypeFunctions for Queue {
247
351
  fn mark(&self, marker: &magnus::gc::Marker) {
248
- self.0.mark(|value| marker.mark(unsafe { value_from_raw(value) }));
352
+ self.0
353
+ .mark(|value| marker.mark(unsafe { value_from_raw(value) }));
249
354
  }
250
355
  }
251
356
 
252
357
  unsafe impl TypedData for Queue {
253
358
  fn class(ruby: &Ruby) -> RClass {
254
359
  static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
255
- let class = ruby.define_module("Ratomic")
360
+ let class = ruby
361
+ .define_module("Ratomic")
256
362
  .unwrap()
257
363
  .define_class("Queue", ruby.class_object())
258
364
  .unwrap();
@@ -278,7 +384,10 @@ impl Pool {
278
384
  if args.len() > 2 {
279
385
  return Err(Error::new(
280
386
  ruby.exception_arg_error(),
281
- format!("wrong number of arguments (given {}, expected 0..2)", args.len()),
387
+ format!(
388
+ "wrong number of arguments (given {}, expected 0..2)",
389
+ args.len()
390
+ ),
282
391
  ));
283
392
  }
284
393
  let size = args
@@ -296,7 +405,10 @@ impl Pool {
296
405
  .unwrap_or(1000);
297
406
 
298
407
  if size == 0 {
299
- return Err(Error::new(ruby.exception_arg_error(), "pool size must be positive"));
408
+ return Err(Error::new(
409
+ ruby.exception_arg_error(),
410
+ "pool size must be positive",
411
+ ));
300
412
  }
301
413
  if !ruby.block_given() {
302
414
  return Err(Error::new(
@@ -351,7 +463,8 @@ impl DataTypeFunctions for Pool {
351
463
  unsafe impl TypedData for Pool {
352
464
  fn class(ruby: &Ruby) -> RClass {
353
465
  static CLASS: Lazy<RClass> = Lazy::new(|ruby| {
354
- let class = ruby.define_module("Ratomic")
466
+ let class = ruby
467
+ .define_module("Ratomic")
355
468
  .unwrap()
356
469
  .define_class("FixedSizeObjectPool", ruby.class_object())
357
470
  .unwrap();
@@ -383,14 +496,23 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
383
496
  counter.define_method("decrement", method!(Counter::decrement, 1))?;
384
497
  counter.define_method("read", method!(Counter::read, 0))?;
385
498
 
386
- let hashmap = root.define_class("ConcurrentHashMap", ruby.class_object())?;
499
+ let hashmap = root.define_class("Map", ruby.class_object())?;
387
500
  hashmap.undef_default_alloc_func();
388
501
  hashmap.define_singleton_method("new", method!(HashMap::new, 0))?;
389
502
  hashmap.define_method("get", method!(HashMap::get, 1))?;
503
+ hashmap.define_method("key?", method!(HashMap::contains_key, 1))?;
390
504
  hashmap.define_method("set", method!(HashMap::set, 2))?;
505
+ hashmap.define_method("delete", method!(HashMap::delete, 1))?;
391
506
  hashmap.define_method("clear", method!(HashMap::clear, 0))?;
392
507
  hashmap.define_method("size", method!(HashMap::size, 0))?;
393
508
  hashmap.define_method("fetch_and_modify", method!(HashMap::fetch_and_modify, 1))?;
509
+ hashmap.define_method("compute", method!(HashMap::compute, 1))?;
510
+ hashmap.define_method("fetch_or_store", method!(HashMap::fetch_or_store, 1))?;
511
+ hashmap.define_method("upsert", method!(HashMap::upsert, 2))?;
512
+ hashmap.define_private_method(
513
+ "__increment_numeric",
514
+ method!(HashMap::increment_numeric, 2),
515
+ )?;
394
516
 
395
517
  let queue = root.define_class("Queue", ruby.class_object())?;
396
518
  queue.undef_default_alloc_func();
@@ -1,8 +1,38 @@
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)
12
+ # counter.read # => 1
13
+ #
14
+ # @!method self.new
15
+ # Create a counter initialized to zero.
16
+ #
17
+ # @return [Ratomic::Counter]
18
+ #
19
+ # @!method read
20
+ # Read the current counter value.
21
+ #
22
+ # @return [Integer]
23
+ #
24
+ # @!method increment(amt)
25
+ # Increment the counter by +amt+.
26
+ #
27
+ # @param amt [Integer]
28
+ # @return [void]
29
+ #
30
+ # @!method decrement(amt)
31
+ # Decrement the counter by +amt+.
32
+ #
33
+ # @param amt [Integer]
34
+ # @return [void]
35
+ class Counter
6
36
  # Read the current counter value.
7
37
  #
8
38
  # @return [Integer]
@@ -23,29 +53,5 @@ module Ratomic
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
data/lib/ratomic/map.rb CHANGED
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratomic
4
- # A Ractor-shareable concurrent map.
4
+ # A Ractor-shareable concurrent Hash backed by Rust's DashMap.
5
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.
6
+ # Map gives Ruby code a small, Ruby-shaped API over DashMap's concurrent
7
+ # storage. It is suitable for runtime state with shareable keys and values
8
+ # that are safe to access from multiple Ractors, such as integer counters or
9
+ # immutable offsets.
9
10
  #
10
11
  # This is not a full Hash replacement. Iteration and arbitrary mutable object
11
12
  # borrowing are intentionally absent.
@@ -14,10 +15,171 @@ module Ratomic
14
15
  # OFFSETS = Ratomic::Map.new
15
16
  # OFFSETS[:source_a] = 42
16
17
  # OFFSETS[:source_a] # => 42
17
- Map = ConcurrentHashMap
18
-
19
- # Ruby convenience methods for {Map}.
20
- module MapMethods
18
+ #
19
+ # @!method get(key)
20
+ # Read a value by +key+.
21
+ #
22
+ # Missing keys return nil, so use #key? or #fetch when stored nil values
23
+ # need to be distinguished from missing entries.
24
+ #
25
+ # @param key [Object]
26
+ # @return [Object, nil]
27
+ #
28
+ # @!method set(key, value)
29
+ # Set a value for +key+.
30
+ #
31
+ # @param key [Object]
32
+ # @param value [Object]
33
+ # @return [void]
34
+ #
35
+ # @!method key?(key)
36
+ # Check whether +key+ currently exists in the map.
37
+ #
38
+ # Unlike #get and #[], this distinguishes missing keys from stored nil
39
+ # values.
40
+ #
41
+ # @param key [Object]
42
+ # @return [Boolean]
43
+ #
44
+ # @!method delete(key)
45
+ # Remove +key+ and return its previous value.
46
+ #
47
+ # Missing keys return nil. Stored nil values also return nil; use #key?
48
+ # before deleting if that distinction matters.
49
+ #
50
+ # @param key [Object]
51
+ # @return [Object, nil]
52
+ #
53
+ # @!method clear
54
+ # Remove all entries from the map.
55
+ #
56
+ # @return [void]
57
+ #
58
+ # @!method size
59
+ # Return the current number of entries.
60
+ #
61
+ # Since this is a concurrent map, the value is a moment-in-time observation.
62
+ #
63
+ # @return [Integer]
64
+ #
65
+ # @!method fetch_and_modify(key)
66
+ # Replace the existing value for +key+ with the block return value.
67
+ #
68
+ # TODO: Revisit this name once the Map API settles. Prefer public method
69
+ # names that stay as close as possible to Ruby Hash semantics.
70
+ #
71
+ # The key must already exist. The operation is atomic for the key. The block
72
+ # runs while the map entry is locked, so avoid using this method for
73
+ # Ractor-hot loops or calling back into the same map from inside the block.
74
+ # If the block raises, the previous value is preserved.
75
+ #
76
+ # @param key [Object]
77
+ # @yieldparam value [Object] current value
78
+ # @return [void]
79
+ # @raise [LocalJumpError] if no block is given
80
+ # @raise [Exception] any exception raised by the block
81
+ #
82
+ # @!method compute(key)
83
+ # Atomically compute and store a value for +key+.
84
+ #
85
+ # If +key+ exists, yields the current value. If +key+ is missing, yields
86
+ # nil. The block return value is stored and returned.
87
+ #
88
+ # The operation is atomic for the key. The block runs while the map entry is
89
+ # locked, so avoid using this method for Ractor-hot loops or calling back
90
+ # into the same map from inside the block. Prefer native update helpers such
91
+ # as #increment when they fit the workflow.
92
+ #
93
+ # If the block raises, the previous value is preserved. If the key was
94
+ # missing, no entry is inserted.
95
+ #
96
+ # @param key [Object]
97
+ # @yieldparam value [Object, nil] current value, or nil if missing
98
+ # @return [Object] the newly stored value
99
+ # @raise [LocalJumpError] if no block is given
100
+ # @raise [Exception] any exception raised by the block
101
+ #
102
+ # @!method fetch_or_store(key)
103
+ # Return the existing value for +key+, or atomically store the block result.
104
+ #
105
+ # If +key+ exists, returns the current value and does not yield. If +key+
106
+ # is missing, yields once, stores the block return value, and returns it.
107
+ #
108
+ # The operation is atomic for the key. Under contention, only one stored
109
+ # value wins for a missing key. The block runs while the map entry is locked,
110
+ # so avoid using this method for Ractor-hot loops or calling back into the
111
+ # same map from inside the block.
112
+ #
113
+ # If the block raises, no entry is inserted.
114
+ #
115
+ # @param key [Object]
116
+ # @return [Object] the existing or newly stored value
117
+ # @raise [LocalJumpError] if no block is given
118
+ # @raise [Exception] any exception raised by the block
119
+ #
120
+ # @!method upsert(key, initial)
121
+ # Atomically insert +initial+ for a missing key, or update an existing value.
122
+ #
123
+ # If +key+ is missing, stores and returns +initial+ without yielding. If
124
+ # +key+ exists, yields the current value, stores the block return value, and
125
+ # returns it.
126
+ #
127
+ # The operation is atomic for the key. The block runs while the map entry is
128
+ # locked, so avoid using this method for Ractor-hot loops or calling back
129
+ # into the same map from inside the block. Prefer native update helpers such
130
+ # as #increment when they fit the workflow.
131
+ #
132
+ # If the block raises, the previous value is preserved.
133
+ #
134
+ # @param key [Object]
135
+ # @param initial [Object]
136
+ # @yieldparam value [Object, nil] current value
137
+ # @return [Object] the inserted or newly stored value
138
+ # @raise [LocalJumpError] if no block is given
139
+ # @raise [Exception] any exception raised by the block
140
+ #
141
+ # @!method increment(key, by = 1)
142
+ # Atomically increment the numeric value for +key+.
143
+ #
144
+ # Missing keys start at zero. Existing non-numeric values raise TypeError
145
+ # and are left unchanged. This uses a native update path and is the preferred
146
+ # counter primitive for Ractor-heavy workloads.
147
+ #
148
+ # @param key [Object]
149
+ # @param by [Numeric]
150
+ # @return [Numeric] the newly stored value
151
+ # @raise [TypeError] if +by+ or the existing value is not numeric
152
+ #
153
+ # @!method decrement(key, by = 1)
154
+ # Atomically decrement the numeric value for +key+.
155
+ #
156
+ # Missing keys start at zero.
157
+ #
158
+ # @param key [Object]
159
+ # @param by [Numeric]
160
+ # @return [Numeric] the newly stored value
161
+ # @raise [TypeError] if +by+ or the existing value is not numeric
162
+ #
163
+ # @!method append(key, value)
164
+ # Atomically append +value+ to an Array bucket for +key+.
165
+ #
166
+ # The stored Array is replaced rather than mutated in place.
167
+ #
168
+ # @param key [Object]
169
+ # @param value [Object]
170
+ # @return [Array] the newly stored frozen Array
171
+ # @raise [TypeError] if the existing value is not an Array
172
+ #
173
+ # @!method add_to_set(key, value)
174
+ # Atomically add +value+ to a Set bucket for +key+.
175
+ #
176
+ # The stored Set is replaced rather than mutated in place.
177
+ #
178
+ # @param key [Object]
179
+ # @param value [Object]
180
+ # @return [Set] the newly stored frozen Set
181
+ # @raise [TypeError] if the existing value is not a Set
182
+ class Map
21
183
  # Set a value for +key+.
22
184
  #
23
185
  # @param key [Object]
@@ -37,6 +199,101 @@ module Ratomic
37
199
  get(key)
38
200
  end
39
201
 
202
+ # Fetch a value by +key+.
203
+ #
204
+ # Unlike #[], this distinguishes missing keys from explicit nil values.
205
+ #
206
+ # @param key [Object]
207
+ # @param default [Object]
208
+ # @yieldparam key [Object]
209
+ # @return [Object]
210
+ # @raise [KeyError] if +key+ is missing and no default or block is provided
211
+ def fetch(key, default = UNDEFINED)
212
+ return get(key) if key?(key)
213
+ return yield key if block_given?
214
+ return default unless default.equal?(UNDEFINED)
215
+
216
+ raise KeyError, "key not found: #{key.inspect}"
217
+ end
218
+
219
+ # Atomically increment the numeric value for +key+.
220
+ #
221
+ # Missing keys start at zero. Existing non-numeric values raise TypeError
222
+ # and are left unchanged. This uses a native update path and is the preferred
223
+ # counter primitive for Ractor-heavy workloads.
224
+ #
225
+ # @param key [Object]
226
+ # @param by [Numeric]
227
+ # @return [Numeric] the newly stored value
228
+ # @raise [TypeError] if +by+ or the existing value is not numeric
229
+ def increment(key, by = 1)
230
+ raise TypeError, "amount must be numeric: #{by.inspect}" unless by.is_a?(Numeric)
231
+
232
+ __increment_numeric(key, by)
233
+ end
234
+
235
+ # Atomically decrement the numeric value for +key+.
236
+ #
237
+ # Missing keys start at zero.
238
+ #
239
+ # @param key [Object]
240
+ # @param by [Numeric]
241
+ # @return [Numeric] the newly stored value
242
+ # @raise [TypeError] if +by+ or the existing value is not numeric
243
+ def decrement(key, by = 1)
244
+ raise TypeError, "amount must be numeric: #{by.inspect}" unless by.is_a?(Numeric)
245
+
246
+ increment(key, -by)
247
+ end
248
+
249
+ # Atomically append +value+ to an Array bucket for +key+.
250
+ #
251
+ # The stored Array is replaced rather than mutated in place.
252
+ #
253
+ # @param key [Object]
254
+ # @param value [Object]
255
+ # @return [Array] the newly stored frozen Array
256
+ # @raise [TypeError] if the existing value is not an Array
257
+ def append(key, value)
258
+ missing = !key?(key)
259
+ compute(key) do |old_value|
260
+ if old_value.nil? && missing
261
+ [value].freeze
262
+ else
263
+ unless old_value.is_a?(Array)
264
+ raise TypeError,
265
+ "existing value for #{key.inspect} must be an Array: #{old_value.inspect}"
266
+ end
267
+
268
+ (old_value + [value]).freeze
269
+ end
270
+ end
271
+ end
272
+
273
+ # Atomically add +value+ to a Set bucket for +key+.
274
+ #
275
+ # The stored Set is replaced rather than mutated in place.
276
+ #
277
+ # @param key [Object]
278
+ # @param value [Object]
279
+ # @return [Set] the newly stored frozen Set
280
+ # @raise [TypeError] if the existing value is not a Set
281
+ def add_to_set(key, value)
282
+ missing = !key?(key)
283
+ compute(key) do |old_value|
284
+ if old_value.nil? && missing
285
+ Set[value].freeze
286
+ else
287
+ unless old_value.is_a?(Set)
288
+ raise TypeError,
289
+ "existing value for #{key.inspect} must be a Set: #{old_value.inspect}"
290
+ end
291
+
292
+ (old_value | [value]).freeze
293
+ end
294
+ end
295
+ end
296
+
40
297
  # Alias for #size.
41
298
  #
42
299
  # @return [Integer]
@@ -50,7 +307,14 @@ module Ratomic
50
307
  def empty?
51
308
  size.zero?
52
309
  end
53
- end
54
310
 
55
- ConcurrentHashMap.prepend(MapMethods)
311
+ # Alias for #key?.
312
+ #
313
+ # @param key [Object]
314
+ # @return [Boolean]
315
+ def include?(key)
316
+ key?(key)
317
+ end
318
+ alias member? include?
319
+ end
56
320
  end
data/lib/ratomic/pool.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "timeout"
4
+
3
5
  module Ratomic
4
6
  # A Ractor-safe ownership-transfer pool for mutable Ruby objects.
5
7
  #
data/lib/ratomic/queue.rb CHANGED
@@ -1,8 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratomic
4
- # Ruby convenience methods for {Queue}.
5
- module QueueMethods
4
+ # A Ractor-shareable multi-producer, multi-consumer queue.
5
+ #
6
+ # Queue stores Ruby objects in a fixed-size native ring buffer. Push blocks
7
+ # when the queue is full; pop blocks when the queue is empty.
8
+ #
9
+ # @example Push and pop work
10
+ # queue = Ratomic::Queue.new(128)
11
+ # queue << "job"
12
+ # queue.pop # => "job"
13
+ #
14
+ # @!method self.new(capacity)
15
+ # Create a queue with a fixed capacity.
16
+ #
17
+ # @param capacity [Integer]
18
+ # @return [Ratomic::Queue]
19
+ # @raise [ArgumentError] if capacity is outside the supported range
20
+ # @raise [TypeError] if capacity is not an Integer
21
+ #
22
+ # @!method push(item)
23
+ # Push an item, blocking until space is available.
24
+ #
25
+ # @param item [Object]
26
+ # @return [void]
27
+ #
28
+ # @!method pop
29
+ # Pop an item, blocking until one is available.
30
+ #
31
+ # @return [Object]
32
+ #
33
+ # @!method peek
34
+ # Return the next item without removing it.
35
+ #
36
+ # Since this is a concurrent queue, the value is a moment-in-time
37
+ # observation.
38
+ #
39
+ # @return [Object, nil]
40
+ #
41
+ # @!method empty?
42
+ # Check whether the queue currently appears empty.
43
+ #
44
+ # Since this is a concurrent queue, the value is a moment-in-time
45
+ # observation.
46
+ #
47
+ # @return [Boolean]
48
+ #
49
+ # @!method size
50
+ # Return the current queue size.
51
+ #
52
+ # Since this is a concurrent queue, the value is a moment-in-time
53
+ # observation.
54
+ #
55
+ # @return [Integer]
56
+ #
57
+ # @!method length
58
+ # Alias for #size.
59
+ #
60
+ # @return [Integer]
61
+ class Queue
6
62
  # Push an item and return the queue for chaining.
7
63
  #
8
64
  # @param item [Object]
@@ -12,6 +68,4 @@ module Ratomic
12
68
  self
13
69
  end
14
70
  end
15
-
16
- Queue.prepend(QueueMethods)
17
71
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Ratomic provides Ractor-friendly mutable data structures backed by native
4
+ # Rust concurrency primitives.
5
+ #
6
+ # The public API currently includes {Counter}, {Map}, {Queue}, and {Pool}.
3
7
  module Ratomic
4
- VERSION = "0.2.1"
8
+ # Current gem version.
9
+ VERSION = "0.3.0"
5
10
  end
data/lib/ratomic.rb CHANGED
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "timeout"
4
-
5
- require_relative "ratomic/version"
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"
3
+ require "ratomic/ratomic"
4
+ require "ratomic/version"
12
5
 
13
6
  module Ratomic
14
- # Base error for Ratomic-specific runtime failures.
15
7
  class Error < StandardError; end
16
8
  end
9
+
10
+ require "ratomic/undefined"
11
+ require "ratomic/counter"
12
+ require "ratomic/map"
13
+ require "ratomic/queue"
14
+ require "ratomic/pool"
data/ratomic.gemspec CHANGED
@@ -9,15 +9,16 @@ Gem::Specification.new do |spec|
9
9
  spec.email = ["mike@perham.net"]
10
10
  spec.metadata["maintainers"] = "Ken C. Demanawa"
11
11
 
12
- spec.summary = "Mutable data structures for Ractors"
13
- spec.description = spec.summary
14
- spec.homepage = "https://github.com/mperham/ratomic"
12
+ spec.summary = "Ractor-safe concurrent data structures for Ruby"
13
+ spec.description = "Ractor-safe counters, maps, queues, and ownership-transfer pools " \
14
+ "backed by native Rust concurrency primitives."
15
+ spec.homepage = "https://mperham.github.io/ratomic"
15
16
  spec.license = "MIT"
16
- spec.required_ruby_version = ">= 4.0.0"
17
+ spec.required_ruby_version = [">= 4.0", "< 4.1.dev"]
17
18
 
18
19
  spec.metadata["homepage_uri"] = spec.homepage
19
20
  spec.metadata["source_code_uri"] = "https://github.com/mperham/ratomic"
20
- spec.metadata["changelog_uri"] = "https://github.com/mperham/ratomic"
21
+ spec.metadata["changelog_uri"] = "https://github.com/mperham/ratomic/blob/trunk/CHANGELOG.md"
21
22
  spec.metadata["rubygems_mfa_required"] = "true"
22
23
 
23
24
  # Specify which files should be added to the gem when it is released.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratomic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
@@ -24,7 +24,8 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.9.128
27
- description: Mutable data structures for Ractors
27
+ description: Ractor-safe counters, maps, queues, and ownership-transfer pools backed
28
+ by native Rust concurrency primitives.
28
29
  email:
29
30
  - mike@perham.net
30
31
  executables: []
@@ -55,14 +56,14 @@ files:
55
56
  - lib/ratomic/undefined.rb
56
57
  - lib/ratomic/version.rb
57
58
  - ratomic.gemspec
58
- homepage: https://github.com/mperham/ratomic
59
+ homepage: https://mperham.github.io/ratomic
59
60
  licenses:
60
61
  - MIT
61
62
  metadata:
62
63
  maintainers: Ken C. Demanawa
63
- homepage_uri: https://github.com/mperham/ratomic
64
+ homepage_uri: https://mperham.github.io/ratomic
64
65
  source_code_uri: https://github.com/mperham/ratomic
65
- changelog_uri: https://github.com/mperham/ratomic
66
+ changelog_uri: https://github.com/mperham/ratomic/blob/trunk/CHANGELOG.md
66
67
  rubygems_mfa_required: 'true'
67
68
  rdoc_options: []
68
69
  require_paths:
@@ -71,7 +72,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
71
72
  requirements:
72
73
  - - ">="
73
74
  - !ruby/object:Gem::Version
74
- version: 4.0.0
75
+ version: '4.0'
76
+ - - "<"
77
+ - !ruby/object:Gem::Version
78
+ version: 4.1.dev
75
79
  required_rubygems_version: !ruby/object:Gem::Requirement
76
80
  requirements:
77
81
  - - ">="
@@ -80,5 +84,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
80
84
  requirements: []
81
85
  rubygems_version: 4.0.10
82
86
  specification_version: 4
83
- summary: Mutable data structures for Ractors
87
+ summary: Ractor-safe concurrent data structures for Ruby
84
88
  test_files: []