ratomic 0.2.0-aarch64-linux → 0.3.0-aarch64-linux

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: 4daad186ea78dee734a51a45058796088a136dc4581b2aca8069c1b537d18700
4
- data.tar.gz: 16eb668ab1e0a204fdf9d88d8e947d401782b1aa6662e0af6677a34d98e4e98a
3
+ metadata.gz: 2a9a21c9f48aee3bcf45cf6ff9b4fdb5c387162c09d320498dfc572279ebcefe
4
+ data.tar.gz: 8f24205fefa107afedd669aaa2772012339195669cf8c6c95fdea6c2e431cc84
5
5
  SHA512:
6
- metadata.gz: 7d2f7ea9508fe6990a5b4da2f5309754da78b8a46193039155e9a9326a5411c32285c15dc0440bd159cbc1a50629e4765d3ad394fd25a497be68557e4f1fc827
7
- data.tar.gz: 26938fc5cd19ce5a71fdf70b156e6a945eaf151e091c70bca8ee95cd543bccd89111eea221992b00383113dc1829ffe7a7e988480267fe36250ac8143e9206bf
6
+ metadata.gz: 15180c7c0f215d43c0eedfa12187d2e3b8c868bb1a24c92cf38f163864ea7a0ec3c5e1589e0fe9fe7d23a48ccfb40a039f6bd687e5ddce2d37d25d1c559efae9
7
+ data.tar.gz: 1e5aa4f5be6fb3af99a838e12cc6bb787787f5e22194cc72b27e4602eacc02cbbfd97740bed484c31978957c23e94fd78355773428a655a7f16474c1d5bb619c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
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
+
14
+ ## [0.2.1] - 2026-06-04
15
+
16
+ - Fix `Ratomic::Queue` slot indexing for non-power-of-two capacities.
17
+ - Fix `Ratomic::Map#fetch_and_modify` to propagate block exceptions instead of panicking.
18
+
3
19
  ## [0.2.0] - 2026-06-04
4
20
 
5
21
  - Drop Ruby 3.x support and require Ruby 4.
data/README.md CHANGED
@@ -1,176 +1,250 @@
1
1
  # Ratomic
2
2
 
3
- Ratomic provides mutable data structures for use with Ruby's Ractors.
4
- This allows Ruby code to scale beyond the infamous GVL.
3
+ [![Gem Version](https://badge.fury.io/rb/ratomic.svg)](https://badge.fury.io/rb/ratomic)
4
+ [![CI](https://github.com/mperham/ratomic/workflows/CI/badge.svg)](https://github.com/mperham/ratomic/actions)
5
+ [![Ruby Version](https://img.shields.io/badge/ruby-%3E%3D%204.0-ruby.svg)](https://www.ruby-lang.org/en/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
7
 
6
- # HELP WANTED!
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.
7
11
 
8
- > If you know Rust and Ruby C-extensions, we need your help!
9
- > This project is brand new and could use your knowledge!
10
- > If you don't know Rust or C, consider this a challenge to learn and solve.
11
- > Read through the [issues](//github.com/mperham/ratomic/issues) to find work that sounds interesting to you.
12
+ ## Project Direction
12
13
 
13
- ## How to contribute
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
- Please make sure to understand our [Code of Conduct](./CODE_OF_CONDUCT.md).
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.
16
19
 
17
- After changing code, you can give it a spin with:
20
+ ## Requirements
18
21
 
19
- ```bash
20
- rake
21
- ```
22
-
23
- This should compile the Rust code and run all tests.
24
- The test suite writes a SimpleCov report to `coverage/index.html` so you can see which Ruby wrapper paths are covered.
22
+ - Ruby 4.0 or newer
23
+ - Bundler
24
+ - Rust toolchain when building the native extension from source
25
25
 
26
26
  ## Installation
27
27
 
28
- Install the gem and add to the application's Gemfile by executing:
28
+ Add Ratomic to your application's Gemfile:
29
29
 
30
30
  ```bash
31
31
  bundle add ratomic
32
32
  ```
33
33
 
34
- TODO: We have not released a gem yet.
34
+ Then require it from Ruby:
35
35
 
36
- ## Usage
36
+ ```ruby
37
+ require "ratomic"
38
+ ```
37
39
 
38
- Ratomic provides several useful Ractor-safe structures.
39
- Note the APIs available are frequently very limited compared to Ruby's broad API.
40
+ ## Documentation
41
+
42
+ API documentation is published to GitHub Pages:
43
+
44
+ - [mperham.github.io/ratomic](https://mperham.github.io/ratomic/)
45
+
46
+ ## Examples And Benchmarks
47
+
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`.
54
+
55
+ ## Usage
40
56
 
41
- These structures are designed for use as class-level constants so they can be shared by numerous Ractors.
57
+ Ratomic provides two safety models:
42
58
 
43
- Ratomic has two different safety models:
59
+ - `Counter`, `Map`, and `Queue` are shared concurrent structures.
60
+ - `Pool` transfers ownership of mutable objects between Ractors.
44
61
 
45
- * `Counter`, `Map`, and `Queue` are shared concurrent structures.
46
- * `Pool` transfers ownership of mutable objects between Ractors.
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.
47
65
 
48
- 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.
66
+ These structures are designed for use as class-level constants so they can be
67
+ shared by many Ractors.
49
68
 
50
69
  ### `Ratomic::Counter`
51
70
 
71
+ `Ratomic::Counter` is a Ractor-shareable atomic counter.
72
+
52
73
  ```ruby
53
- c = Ratomic::Counter.new
54
- c.read # => 0
55
- c.inc
56
- c.inc(5)
57
- c.dec(1)
58
- c.dec
59
- c.read # => 4
60
- c.to_i # => 4
61
- 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
62
85
  ```
63
86
 
64
- ### `Ratomic::Pool`
87
+ ### `Ratomic::Map`
65
88
 
66
- 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.
67
92
 
68
93
  ```ruby
69
- POOL = Ratomic::Pool.new(5, 1.0) { [] }
70
- POOL.with do |obj|
71
- # do something with obj
72
- obj << "work"
73
- 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
74
110
  ```
75
111
 
76
- `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.
112
+ `Map` also includes atomic convenience methods for common bucket patterns:
77
113
 
78
- 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.
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"}>
122
+ ```
79
123
 
80
- In that model:
124
+ ### `Ratomic::Queue`
125
+
126
+ `Ratomic::Queue` is a Ractor-shareable multi-producer, multi-consumer queue.
81
127
 
82
- * the Rust owner maps to the Ractor that currently checked out the pooled object
83
- * the Rust move maps to `Ractor::Port#send(..., move: true)`
84
- * Rust's "cannot use after move" rule maps to Ruby raising `Ractor::MovedError`
85
- * borrowing is not modeled; `Pool` transfers ownership instead of lending references
128
+ ```ruby
129
+ queue = Ratomic::Queue.new(128)
130
+
131
+ queue.push("hello")
132
+ queue << "world"
133
+
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.
86
165
 
87
166
  When an object is checked out:
88
167
 
89
- * the pool moves the object to the caller
90
- * the caller can mutate the object while it owns it
91
- * 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
92
171
 
93
172
  When an object is checked in:
94
173
 
95
- * ownership moves back to the pool
96
- * stale references held by the caller become moved objects
97
- * 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`
98
177
 
99
- 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.
100
180
 
101
181
  ```ruby
102
182
  outside = nil
103
183
 
104
- POOL.with do |obj|
105
- outside = obj
106
- obj << "inside"
184
+ BUFFERS.with do |buffer|
185
+ outside = buffer
186
+ buffer << "inside"
107
187
  end
108
188
 
109
189
  outside << "outside"
110
190
  # raises Ractor::MovedError
111
191
  ```
112
192
 
113
- 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.
114
-
115
- Manual checkout/checkin is also supported:
193
+ Manual checkout and checkin are also supported:
116
194
 
117
195
  ```ruby
118
- obj = POOL.checkout
119
- raise "pool checkout timeout" if obj.nil?
196
+ buffer = BUFFERS.checkout
197
+ raise "pool checkout timeout" if buffer.nil?
120
198
 
121
199
  begin
122
- obj << "manual work"
200
+ buffer << "manual work"
123
201
  ensure
124
- POOL.checkin(obj) if obj
202
+ BUFFERS.checkin(buffer) if buffer
125
203
  end
126
204
  ```
127
205
 
128
- `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.
129
208
 
130
- ### `Ratomic::Map`
209
+ `Pool` uses ownership transfer, not Rust's full borrow checker:
131
210
 
132
- 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
133
215
 
134
- ```ruby
135
- HASH = Ratomic::Map.new
136
- HASH["mike"] = 123
137
- HASH["mike"] # => 123
138
- HASH.fetch_and_modify("mike") { |value| value + 1 }
139
- HASH.length
140
- HASH.empty?
141
- HASH.clear
142
- ```
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.
143
219
 
144
- ### `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.
145
223
 
146
- A multi-producer, multi-consumer queue.
224
+ ## Contributing
147
225
 
148
- ```ruby
149
- q = Ratomic::Queue.new(128)
226
+ Please read the [Code of Conduct](./CODE_OF_CONDUCT.md) before contributing.
150
227
 
151
- q.push("hello")
152
- q << "world"
228
+ After changing code, run:
153
229
 
154
- q.size # => 2
155
- q.empty? # => false
156
- q.peek # => "hello"
157
- item = q.pop # => "hello"
158
- item = q.pop # => "world"
159
- q.empty? # => true
230
+ ```bash
231
+ rake
160
232
  ```
161
- 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>.
162
-
163
- 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.
164
233
 
165
- 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.
166
236
 
167
237
  ## Thanks
168
238
 
169
- [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/).
170
- 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].
171
241
 
172
- 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.
173
244
 
174
245
  ## License
175
246
 
176
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/
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -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
  #
@@ -73,6 +75,20 @@ module Ratomic
73
75
  nil
74
76
  end
75
77
 
78
+ # Stop the private coordinator Ractor.
79
+ #
80
+ # This is primarily useful for tests and short-lived scripts. A closed pool
81
+ # should not be used for further checkout/checkin operations.
82
+ #
83
+ # @return [nil]
84
+ def close
85
+ @control << [:shutdown]
86
+ @control.value
87
+ nil
88
+ rescue Ractor::ClosedError, Ractor::Error
89
+ nil
90
+ end
91
+
76
92
  # Checkout an object, yield it, then move it back to the pool.
77
93
  #
78
94
  # This is the preferred API because it guarantees checkin through an ensure
@@ -101,7 +117,7 @@ module Ratomic
101
117
 
102
118
  loop do
103
119
  command, *args = Ractor.receive
104
- handle_command(command, args, available, waiting)
120
+ break if handle_command(command, args, available, waiting) == :shutdown
105
121
  end
106
122
  end
107
123
  private_class_method :run_control_loop
@@ -114,6 +130,8 @@ module Ratomic
114
130
  handle_checkin(args.fetch(0), available, waiting)
115
131
  when :cancel
116
132
  waiting.delete(args.fetch(0))
133
+ when :shutdown
134
+ :shutdown
117
135
  end
118
136
  end
119
137
  private_class_method :handle_command
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.0"
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.0
4
+ version: 0.3.0
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Mike Perham
@@ -9,9 +9,10 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-06-04 00:00:00.000000000 Z
12
+ date: 2026-06-05 00:00:00.000000000 Z
13
13
  dependencies: []
14
- description: Mutable data structures for Ractors
14
+ description: Ractor-safe counters, maps, queues, and ownership-transfer pools backed
15
+ by native Rust concurrency primitives.
15
16
  email:
16
17
  - mike@perham.net
17
18
  executables: []
@@ -35,14 +36,14 @@ files:
35
36
  - lib/ratomic/undefined.rb
36
37
  - lib/ratomic/version.rb
37
38
  - ratomic.gemspec
38
- homepage: https://github.com/mperham/ratomic
39
+ homepage: https://mperham.github.io/ratomic
39
40
  licenses:
40
41
  - MIT
41
42
  metadata:
42
43
  maintainers: Ken C. Demanawa
43
- homepage_uri: https://github.com/mperham/ratomic
44
+ homepage_uri: https://mperham.github.io/ratomic
44
45
  source_code_uri: https://github.com/mperham/ratomic
45
- changelog_uri: https://github.com/mperham/ratomic
46
+ changelog_uri: https://github.com/mperham/ratomic/blob/trunk/CHANGELOG.md
46
47
  rubygems_mfa_required: 'true'
47
48
  post_install_message:
48
49
  rdoc_options: []
@@ -65,5 +66,5 @@ requirements: []
65
66
  rubygems_version: 3.5.23
66
67
  signing_key:
67
68
  specification_version: 4
68
- summary: Mutable data structures for Ractors
69
+ summary: Ractor-safe concurrent data structures for Ruby
69
70
  test_files: []