ratomic 0.2.1-aarch64-linux → 0.3.1-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: 5e1ecee0ea057050602abefbb051ed2cf5daa9ac44f7ab5de507645a1f4496fa
4
- data.tar.gz: '0940a36aa2745313acd26b7d924d94c233c6a240d866d50eaa9ac9fd0549f3b0'
3
+ metadata.gz: e1c48d0519af48541df300ad0300a47bf39fb12a25a600b559e703e97643d47b
4
+ data.tar.gz: 1746bf99c58d3d59692ef5179a0e00522b52e8512ad85cd97953a0be87fc1ef8
5
5
  SHA512:
6
- metadata.gz: 51fb89af866c1b1f4ebc97bcab4e65108e588b5b8acd508daa98276e84617bc3a41b225db8ea242e645d2d2538de817f6a0d321655fabc280b7f6cd71691fcbf
7
- data.tar.gz: dcd37d8ee7b9631709246e5d9ba0fcfd34285d9eb6822787b914d981b68c9df4f8caca95aa4b278f6b425868fe83e0ef274f4a6411ea1a7c1903f1494c5ddb67
6
+ metadata.gz: 164b3bccd2d5d02fb7443d944fecec7021f40288011f4b9182ba830bbef7baf4834f31d6c5e9d109708aa9460acb52f60b6e73cf5c7fce728f2a2f8950815436
7
+ data.tar.gz: 1185884018e3d130297ce7e0224aa80ee3b2d14a2ad7c8ea3699c9b5a948cf9f5f7a4ec86d8517c3846b4e9c260b73e2119ffd11217f0eda5bd13a6a86fbb95d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.3.1] - 2026-06-05
4
+
5
+ - Realign `Counter`, `Map`, and `Queue` return semantics with Ruby conventions.
6
+ - Expand YARD comments across the public API to keep full documentation coverage.
7
+ - Move the top-level module docs to the gem entrypoint for a cleaner load path.
8
+
9
+ ## [0.3.0] - 2026-06-05
10
+
11
+ - Promote the DashMap-backed `Ratomic::Map` API as the primary concurrent Hash primitive.
12
+ - Add `Ratomic::Map#key?`, `#include?`, `#member?`, and `#delete`.
13
+ - Add `Ratomic::Map#fetch`, `#compute`, `#fetch_or_store`, and `#upsert` for atomic per-key workflows.
14
+ - Add `Ratomic::Map#increment`, `#decrement`, `#append`, and `#add_to_set` convenience methods for shared counters and bucket-style values.
15
+ - Improve API documentation for `Counter`, `Map`, `Queue`, and `Pool`.
16
+ - Add GitHub Pages deployment for generated YARD API documentation.
17
+ - Add Redis POC scripts for exercising `Ratomic::Map`, `Ratomic::Counter`, and `Ratomic::Pool` under Thread and Ractor workloads.
18
+ - Remove `Counter#inc` and `Counter#dec` in favor of the native `#increment` and `#decrement` methods.
19
+
3
20
  ## [0.2.1] - 2026-06-04
4
21
 
5
22
  - Fix `Ratomic::Queue` slot indexing for non-power-of-two capacities.
data/README.md CHANGED
@@ -2,180 +2,247 @@
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 native Rust concurrency libraries so Ruby code can share useful state across Ractors without falling back to one global lock. `Pool` uses Ruby Ractor ownership-transfer primitives instead of the native Rust path.
9
9
 
10
- Ratomic provides mutable data structures for use with Ruby's Ractors.
11
- This allows Ruby code to scale beyond the infamous GVL.
10
+ ## Project Direction
12
11
 
13
- # HELP WANTED!
12
+ Ratomic focuses on practical Ractor-safe primitives with a small API surface, clear
13
+ ownership semantics, and honest documentation about sharp edges.
14
14
 
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.
15
+ `Ratomic::Map` is the current priority: a Ruby-facing concurrent Hash powered by
16
+ DashMap, with atomic per-key operations designed for real Ractor workloads.
19
17
 
20
- ## How to contribute
18
+ ## Requirements
21
19
 
22
- Please make sure to understand our [Code of Conduct](./CODE_OF_CONDUCT.md).
20
+ - Ruby 4.0 or newer
21
+ - Bundler
22
+ - Rust toolchain when building the native extension from source
23
23
 
24
- After changing code, you can give it a spin with:
24
+ ## Installation
25
+
26
+ Add Ratomic to your application's Gemfile:
25
27
 
26
28
  ```bash
27
- rake
29
+ bundle add ratomic
28
30
  ```
29
31
 
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.
32
+ Then require it from Ruby:
32
33
 
33
- ## Installation
34
+ ```ruby
35
+ require "ratomic"
36
+ ```
34
37
 
35
- Install the gem and add to the application's Gemfile by executing:
38
+ ## Documentation
36
39
 
37
- ```bash
38
- bundle add ratomic
39
- ```
40
+ API documentation is published to GitHub Pages:
40
41
 
41
- ## Usage
42
+ - [mperham.github.io/ratomic](https://mperham.github.io/ratomic/)
43
+
44
+ ## Examples And Benchmarks
42
45
 
43
- Ratomic provides several useful Ractor-safe structures.
44
- Note the APIs available are frequently very limited compared to Ruby's broad API.
46
+ - [`redis_poc`](./redis_poc) contains local Redis scripts that exercise
47
+ `Ratomic::Map`, `Ratomic::Counter`, and `Ratomic::Pool` under Thread and
48
+ Ractor workloads.
49
+ - The [`cdc-parallel` Ratomic benchmark][cdc-parallel-ratomic] demonstrates
50
+ Ractor workers updating shared CDC processing metrics through `Ratomic::Map`
51
+ and `Ratomic::Counter`.
45
52
 
46
- These structures are designed for use as class-level constants so they can be shared by numerous Ractors.
53
+ ## Usage
47
54
 
48
- Ratomic has two different safety models:
55
+ Ratomic provides two safety models:
49
56
 
50
- * `Counter`, `Map`, and `Queue` are shared concurrent structures.
51
- * `Pool` transfers ownership of mutable objects between Ractors.
57
+ - `Counter`, `Map`, and `Queue` are shared concurrent structures.
58
+ - `Pool` transfers ownership of mutable objects between Ractors.
52
59
 
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.
60
+ That distinction matters. A mutable pooled object is not shared by multiple Ractors
61
+ at the same time. It is moved to the caller on checkout and moved back to the pool
62
+ on checkin.
63
+
64
+ These structures are designed for use as class-level constants so they can be
65
+ shared by many Ractors.
54
66
 
55
67
  ### `Ratomic::Counter`
56
68
 
69
+ `Ratomic::Counter` is a Ractor-shareable atomic counter.
70
+
57
71
  ```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
72
+ counter = Ratomic::Counter.new
73
+
74
+ counter.read # => 0
75
+ counter.increment(1)
76
+ counter.increment(5)
77
+ counter.decrement(1)
78
+ counter.decrement(1)
79
+
80
+ counter.read # => 4
81
+ counter.to_i # => 4
82
+ counter.zero? # => false
67
83
  ```
68
84
 
69
- ### `Ratomic::Pool`
85
+ ### `Ratomic::Map`
70
86
 
71
- A Ractor-safe object pool:
87
+ `Ratomic::Map` is a Ractor-safe concurrent Hash backed by Rust's DashMap. It is
88
+ not a full `Hash` replacement; iteration and arbitrary mutable object borrowing
89
+ are intentionally absent.
72
90
 
73
91
  ```ruby
74
- POOL = Ratomic::Pool.new(5, 1.0) { [] }
75
- POOL.with do |obj|
76
- # do something with obj
77
- obj << "work"
78
- end
92
+ OFFSETS = Ratomic::Map.new
93
+
94
+ OFFSETS["mike"] = 123
95
+ OFFSETS["mike"] # => 123
96
+ OFFSETS.key?("mike") # => true
97
+ OFFSETS.fetch("missing", 0) # => 0
98
+
99
+ OFFSETS.fetch_or_store("count") { 0 } # => 0
100
+ OFFSETS.compute("mike") { |value| value + 1 } # => 124
101
+ OFFSETS.upsert("mike", 1) { |value| value + 1 } # => 125
102
+ OFFSETS.fetch_and_modify("mike") { |value| value + 1 }
103
+
104
+ OFFSETS.delete("mike") # => 126
105
+ OFFSETS.length
106
+ OFFSETS.empty?
107
+ OFFSETS.clear
108
+ ```
109
+
110
+ `Map` also includes atomic convenience methods for common bucket patterns:
111
+
112
+ ```ruby
113
+ counts = Ratomic::Map.new
114
+ counts.increment("jobs") # => 1
115
+ counts.decrement("jobs") # => 0
116
+
117
+ groups = Ratomic::Map.new
118
+ groups.append("jobs", "import") # => ["import"]
119
+ groups.add_to_set("workers", "alpha") # => #<Set: {"alpha"}>
79
120
  ```
80
121
 
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.
122
+ ### `Ratomic::Queue`
123
+
124
+ `Ratomic::Queue` is a Ractor-shareable multi-producer, multi-consumer queue.
82
125
 
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.
126
+ ```ruby
127
+ queue = Ratomic::Queue.new(128)
84
128
 
85
- In that model:
129
+ queue.push("hello")
130
+ queue << "world"
86
131
 
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
132
+ queue.size # => 2
133
+ queue.empty? # => false
134
+ queue.peek # => "hello"
135
+ queue.pop # => "hello"
136
+ queue.pop # => "world"
137
+ queue.empty? # => true
138
+ ```
139
+
140
+ The `.new(capacity)` method initializes the queue with a fixed-size buffer.
141
+ Capacity must be at least `1` and at most `2**20`. Non-power-of-two capacities
142
+ are supported exactly.
143
+
144
+ Since `Ratomic::Queue` is concurrent, `size`, `empty?`, and `peek` are
145
+ moment-in-time observations. Their results may already be stale by the time your
146
+ code uses them.
147
+
148
+ ### `Ratomic::Pool`
149
+
150
+ `Ratomic::Pool` is a Ractor-safe ownership-transfer pool for mutable Ruby objects.
151
+
152
+ ```ruby
153
+ BUFFERS = Ratomic::Pool.new(5, 1.0) { [] }
154
+
155
+ BUFFERS.with do |buffer|
156
+ buffer.clear
157
+ buffer << "work"
158
+ end
159
+ ```
160
+
161
+ `Pool` uses Ruby 4's `Ractor::Port` and `move: true` semantics so only one
162
+ Ractor owns a checked-out object at a time.
91
163
 
92
164
  When an object is checked out:
93
165
 
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
166
+ - the pool moves the object to the caller
167
+ - the caller can mutate the object while it owns it
168
+ - the pool cannot hand that object to another Ractor until it is checked in
97
169
 
98
170
  When an object is checked in:
99
171
 
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`
172
+ - ownership moves back to the pool
173
+ - stale references held by the caller become moved objects
174
+ - using those stale references raises `Ractor::MovedError`
103
175
 
104
- This means incorrect usage fails at the Ruby object-ownership boundary rather than allowing two Ractors to mutate the same object concurrently.
176
+ This means incorrect usage fails at the Ruby object-ownership boundary rather
177
+ than allowing two Ractors to mutate the same object concurrently.
105
178
 
106
179
  ```ruby
107
180
  outside = nil
108
181
 
109
- POOL.with do |obj|
110
- outside = obj
111
- obj << "inside"
182
+ BUFFERS.with do |buffer|
183
+ outside = buffer
184
+ buffer << "inside"
112
185
  end
113
186
 
114
187
  outside << "outside"
115
188
  # raises Ractor::MovedError
116
189
  ```
117
190
 
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:
191
+ Manual checkout and checkin are also supported:
121
192
 
122
193
  ```ruby
123
- obj = POOL.checkout
124
- raise "pool checkout timeout" if obj.nil?
194
+ buffer = BUFFERS.checkout
195
+ raise "pool checkout timeout" if buffer.nil?
125
196
 
126
197
  begin
127
- obj << "manual work"
198
+ buffer << "manual work"
128
199
  ensure
129
- POOL.checkin(obj) if obj
200
+ BUFFERS.checkin(buffer) if buffer
130
201
  end
131
202
  ```
132
203
 
133
- `checkout` returns `nil` if no pooled object becomes available before the configured timeout. `with` raises `Ratomic::Error` in that case.
204
+ `checkout` returns `nil` if no pooled object becomes available before the
205
+ configured timeout. `with` raises `Ratomic::Error` in that case.
134
206
 
135
- ### `Ratomic::Map`
207
+ `Pool` uses ownership transfer, not Rust's full borrow checker:
136
208
 
137
- A Ractor-safe map/hash structure:
209
+ - the Rust owner maps to the Ractor that currently checked out the pooled object
210
+ - the Rust move maps to `Ractor::Port#send(..., move: true)`
211
+ - Rust's "cannot use after move" rule maps to Ruby raising `Ractor::MovedError`
212
+ - borrowing is not modeled; `Pool` transfers ownership instead of lending references
138
213
 
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
- ```
214
+ This design addresses [issue #5](https://github.com/mperham/ratomic/issues/5),
215
+ where using a pooled object after `with` could lead to memory corruption or a
216
+ process crash.
148
217
 
149
- ### `Ratomic::Queue`
218
+ The lower-level `Ratomic::FixedSizeObjectPool` native class may still exist, but
219
+ `Ratomic::Pool` does not inherit from it. The public `Pool` API is implemented
220
+ in Ruby so it can use Ruby's Ractor ownership primitives directly.
150
221
 
151
- A multi-producer, multi-consumer queue.
222
+ ## Contributing
152
223
 
153
- ```ruby
154
- q = Ratomic::Queue.new(128)
224
+ Please read the [Code of Conduct](./CODE_OF_CONDUCT.md) before contributing.
155
225
 
156
- q.push("hello")
157
- q << "world"
226
+ After changing code, run:
158
227
 
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
228
+ ```bash
229
+ rake
165
230
  ```
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
231
 
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.
232
+ This compiles the Rust code and runs the test suite. The test suite writes a
233
+ SimpleCov report to `coverage/index.html` for the Ruby wrapper paths.
171
234
 
172
235
  ## Thanks
173
236
 
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!
237
+ [Ilya Bylich](https://github.com/iliabylich) wrote and documented his original
238
+ research at [Ruby, Ractors, and Lock-free Data Structures][ractor-research].
176
239
 
177
- This repo is further research into the usability and limitations of Ractor-friendly structures in Ruby code and gems.
240
+ This repo continues that research into the usability and limitations of
241
+ Ractor-friendly structures in Ruby code and gems.
178
242
 
179
243
  ## License
180
244
 
181
245
  [MIT License](https://opensource.org/licenses/MIT).
246
+
247
+ [cdc-parallel-ratomic]: https://github.com/kanutocd/cdc-parallel/blob/main/benchmark/RATOMIC.md
248
+ [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,51 +1,57 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratomic
4
- # Ruby convenience methods for {Counter}.
5
- module CounterMethods
4
+ # A Ractor-shareable atomic counter.
5
+ #
6
+ # Counter stores an unsigned integer in native Rust atomics and can be shared
7
+ # safely across Ractors.
8
+ #
9
+ # @example Count work across Ractors
10
+ # counter = Ratomic::Counter.new
11
+ # counter.increment(1) # => 1
12
+ # counter.read # => 1
13
+ #
14
+ # @!method self.new
15
+ # Create a counter initialized to zero.
16
+ #
17
+ # @return [Ratomic::Counter] a new shareable counter
18
+ #
19
+ # @!method read
20
+ # Read the current counter value.
21
+ #
22
+ # @return [Integer] the current counter value
23
+ #
24
+ # @!method increment(amt)
25
+ # Increment the counter by +amt+.
26
+ #
27
+ # @param amt [Integer] amount to add to the counter
28
+ # @return [Integer] the updated counter value
29
+ #
30
+ # @!method decrement(amt)
31
+ # Decrement the counter by +amt+.
32
+ #
33
+ # @param amt [Integer] amount to subtract from the counter
34
+ # @return [Integer] the updated counter value
35
+ class Counter
6
36
  # Read the current counter value.
7
37
  #
8
- # @return [Integer]
38
+ # @return [Integer] the current counter value
9
39
  def value
10
40
  read
11
41
  end
12
42
 
13
43
  # Coerce the counter to an Integer snapshot.
14
44
  #
15
- # @return [Integer]
45
+ # @return [Integer] the current counter value
16
46
  def to_i
17
47
  read
18
48
  end
19
49
 
20
50
  # Check whether the current counter value is zero.
21
51
  #
22
- # @return [Boolean]
52
+ # @return [Boolean] true when the counter currently reads zero
23
53
  def zero?
24
54
  read.zero?
25
55
  end
26
-
27
- # Increment the counter.
28
- #
29
- # @param amt [Integer] amount to add
30
- # @raise [ArgumentError] if +amt+ is negative
31
- # @return [void]
32
- def inc(amt = 1)
33
- raise ArgumentError, "amount must be positive: #{amt}" if amt.negative?
34
-
35
- increment(amt)
36
- end
37
-
38
- # Decrement the counter.
39
- #
40
- # @param amt [Integer] amount to subtract
41
- # @raise [ArgumentError] if +amt+ is negative
42
- # @return [void]
43
- def dec(amt = 1)
44
- raise ArgumentError, "amount must be positive: #{amt}" if amt.negative?
45
-
46
- decrement(amt)
47
- end
48
56
  end
49
-
50
- Counter.prepend(CounterMethods)
51
57
  end
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,15 +15,190 @@ 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] lookup key
26
+ # @return [Object, nil] the stored value, or nil when the key is missing
27
+ #
28
+ # @!method set(key, value)
29
+ # Set a value for +key+.
30
+ #
31
+ # This is the method behind `#[]=` and follows Ruby setter semantics by
32
+ # returning the assigned value.
33
+ #
34
+ # @param key [Object] key to write
35
+ # @param value [Object] value to store
36
+ # @return [Object] the assigned value
37
+ #
38
+ # @!method []=(key, value)
39
+ # Set a value for +key+.
40
+ #
41
+ # In assignment form, Ruby returns the value assigned to the expression.
42
+ #
43
+ # @param key [Object] key to write
44
+ # @param value [Object] value to store
45
+ # @return [Object] the assigned value
46
+ #
47
+ # @!method key?(key)
48
+ # Check whether +key+ currently exists in the map.
49
+ #
50
+ # Unlike #get and #[], this distinguishes missing keys from stored nil
51
+ # values.
52
+ #
53
+ # @param key [Object] lookup key
54
+ # @return [Boolean] true when the key currently exists
55
+ #
56
+ # @!method delete(key)
57
+ # Remove +key+ and return its previous value.
58
+ #
59
+ # Missing keys return nil. Stored nil values also return nil; use #key?
60
+ # before deleting if that distinction matters.
61
+ #
62
+ # @param key [Object] key to remove
63
+ # @return [Object, nil] the previous value, or nil when the key was missing
64
+ #
65
+ # @!method clear
66
+ # Remove all entries from the map.
67
+ #
68
+ # @return [Ratomic::Map] self
69
+ #
70
+ # @!method size
71
+ # Return the current number of entries.
72
+ #
73
+ # Since this is a concurrent map, the value is a moment-in-time observation.
74
+ #
75
+ # @return [Integer] the current number of entries
76
+ #
77
+ # @!method fetch_and_modify(key)
78
+ # Replace the existing value for +key+ with the block return value.
79
+ #
80
+ # TODO: Revisit this name once the Map API settles. Prefer public method
81
+ # names that stay as close as possible to Ruby Hash semantics.
82
+ #
83
+ # The key must already exist. The operation is atomic for the key. The block
84
+ # runs while the map entry is locked, so avoid using this method for
85
+ # Ractor-hot loops or calling back into the same map from inside the block.
86
+ # If the block raises, the previous value is preserved.
87
+ #
88
+ # @param key [Object] key to modify in place
89
+ # @yieldparam value [Object] the current stored value
90
+ # @return [void] nothing useful is returned
91
+ # @raise [LocalJumpError] if no block is given
92
+ # @raise [Exception] any exception raised by the block
93
+ #
94
+ # @!method compute(key)
95
+ # Atomically compute and store a value for +key+.
96
+ #
97
+ # If +key+ exists, yields the current value. If +key+ is missing, yields
98
+ # nil. The block return value is stored and returned.
99
+ #
100
+ # The operation is atomic for the key. The block runs while the map entry is
101
+ # locked, so avoid using this method for Ractor-hot loops or calling back
102
+ # into the same map from inside the block. Prefer native update helpers such
103
+ # as #increment when they fit the workflow.
104
+ #
105
+ # If the block raises, the previous value is preserved. If the key was
106
+ # missing, no entry is inserted.
107
+ #
108
+ # @param key [Object] key to compute
109
+ # @yieldparam value [Object, nil] the current stored value, or nil when missing
110
+ # @return [Object] the newly stored value
111
+ # @raise [LocalJumpError] if no block is given
112
+ # @raise [Exception] any exception raised by the block
113
+ #
114
+ # @!method fetch_or_store(key)
115
+ # Return the existing value for +key+, or atomically store the block result.
116
+ #
117
+ # If +key+ exists, returns the current value and does not yield. If +key+
118
+ # is missing, yields once, stores the block return value, and returns it.
119
+ #
120
+ # The operation is atomic for the key. Under contention, only one stored
121
+ # value wins for a missing key. The block runs while the map entry is locked,
122
+ # so avoid using this method for Ractor-hot loops or calling back into the
123
+ # same map from inside the block.
124
+ #
125
+ # If the block raises, no entry is inserted.
126
+ #
127
+ # @param key [Object] key to read or initialize
128
+ # @return [Object] the existing or newly stored value
129
+ # @raise [LocalJumpError] if no block is given
130
+ # @raise [Exception] any exception raised by the block
131
+ #
132
+ # @!method upsert(key, initial)
133
+ # Atomically insert +initial+ for a missing key, or update an existing value.
134
+ #
135
+ # If +key+ is missing, stores and returns +initial+ without yielding. If
136
+ # +key+ exists, yields the current value, stores the block return value, and
137
+ # returns it.
138
+ #
139
+ # The operation is atomic for the key. The block runs while the map entry is
140
+ # locked, so avoid using this method for Ractor-hot loops or calling back
141
+ # into the same map from inside the block. Prefer native update helpers such
142
+ # as #increment when they fit the workflow.
143
+ #
144
+ # If the block raises, the previous value is preserved.
145
+ #
146
+ # @param key [Object] key to update
147
+ # @param initial [Object] value to use when the key is missing
148
+ # @yieldparam value [Object, nil] the current stored value, or nil when missing
149
+ # @return [Object] the inserted or newly stored value
150
+ # @raise [LocalJumpError] if no block is given
151
+ # @raise [Exception] any exception raised by the block
152
+ #
153
+ # @!method increment(key, by = 1)
154
+ # Atomically increment the numeric value for +key+.
155
+ #
156
+ # Missing keys start at zero. Existing non-numeric values raise TypeError
157
+ # and are left unchanged. This uses a native update path and is the preferred
158
+ # counter primitive for Ractor-heavy workloads.
159
+ #
160
+ # @param key [Object] counter key to increment
161
+ # @param by [Numeric] amount to add
162
+ # @return [Numeric] the newly stored value
163
+ # @raise [TypeError] if +by+ or the existing value is not numeric
164
+ #
165
+ # @!method decrement(key, by = 1)
166
+ # Atomically decrement the numeric value for +key+.
167
+ #
168
+ # Missing keys start at zero.
169
+ #
170
+ # @param key [Object] counter key to decrement
171
+ # @param by [Numeric] amount to subtract
172
+ # @return [Numeric] the newly stored value
173
+ # @raise [TypeError] if +by+ or the existing value is not numeric
174
+ #
175
+ # @!method append(key, value)
176
+ # Atomically append +value+ to an Array bucket for +key+.
177
+ #
178
+ # The stored Array is replaced rather than mutated in place.
179
+ #
180
+ # @param key [Object] bucket key to append into
181
+ # @param value [Object] value to append
182
+ # @return [Array] the newly stored frozen Array
183
+ # @raise [TypeError] if the existing value is not an Array
184
+ #
185
+ # @!method add_to_set(key, value)
186
+ # Atomically add +value+ to a Set bucket for +key+.
187
+ #
188
+ # The stored Set is replaced rather than mutated in place.
189
+ #
190
+ # @param key [Object] bucket key to update
191
+ # @param value [Object] value to add to the set
192
+ # @return [Set] the newly stored frozen Set
193
+ # @raise [TypeError] if the existing value is not a Set
194
+ class Map
21
195
  # Set a value for +key+.
22
196
  #
23
- # @param key [Object]
24
- # @param value [Object]
25
- # @return [void]
197
+ # In assignment form, Ruby returns the assigned value.
198
+ #
199
+ # @param key [Object] key to write
200
+ # @param value [Object] value to store
201
+ # @return [Object] the assigned value
26
202
  def []=(key, value)
27
203
  set(key, value)
28
204
  end
@@ -31,26 +207,128 @@ module Ratomic
31
207
  #
32
208
  # Missing keys currently return nil, so storing nil is ambiguous.
33
209
  #
34
- # @param key [Object]
35
- # @return [Object, nil]
210
+ # @param key [Object] lookup key
211
+ # @return [Object, nil] the stored value, or nil when the key is missing
36
212
  def [](key)
37
213
  get(key)
38
214
  end
39
215
 
216
+ # Fetch a value by +key+.
217
+ #
218
+ # Unlike #[], this distinguishes missing keys from explicit nil values.
219
+ #
220
+ # @param key [Object] lookup key
221
+ # @param default [Object] fallback value to return when the key is missing
222
+ # @yieldparam key [Object] the missing key
223
+ # @return [Object] the found value, default, or block result
224
+ # @raise [KeyError] if +key+ is missing and no default or block is provided
225
+ def fetch(key, default = UNDEFINED)
226
+ return get(key) if key?(key)
227
+ return yield key if block_given?
228
+ return default unless default.equal?(UNDEFINED)
229
+
230
+ raise KeyError, "key not found: #{key.inspect}"
231
+ end
232
+
233
+ # Atomically increment the numeric value for +key+.
234
+ #
235
+ # Missing keys start at zero. Existing non-numeric values raise TypeError
236
+ # and are left unchanged. This uses a native update path and is the preferred
237
+ # counter primitive for Ractor-heavy workloads.
238
+ #
239
+ # @param key [Object] counter key to increment
240
+ # @param by [Numeric] amount to add
241
+ # @return [Numeric] the newly stored value
242
+ # @raise [TypeError] if +by+ or the existing value is not numeric
243
+ def increment(key, by = 1)
244
+ raise TypeError, "amount must be numeric: #{by.inspect}" unless by.is_a?(Numeric)
245
+
246
+ __increment_numeric(key, by)
247
+ end
248
+
249
+ # Atomically decrement the numeric value for +key+.
250
+ #
251
+ # Missing keys start at zero.
252
+ #
253
+ # @param key [Object] counter key to decrement
254
+ # @param by [Numeric] amount to subtract
255
+ # @return [Numeric] the newly stored value
256
+ # @raise [TypeError] if +by+ or the existing value is not numeric
257
+ def decrement(key, by = 1)
258
+ raise TypeError, "amount must be numeric: #{by.inspect}" unless by.is_a?(Numeric)
259
+
260
+ increment(key, -by)
261
+ end
262
+
263
+ # Atomically append +value+ to an Array bucket for +key+.
264
+ #
265
+ # The stored Array is replaced rather than mutated in place.
266
+ #
267
+ # @param key [Object] bucket key to append into
268
+ # @param value [Object] value to append
269
+ # @return [Array] the newly stored frozen Array
270
+ # @raise [TypeError] if the existing value is not an Array
271
+ def append(key, value)
272
+ missing = !key?(key)
273
+ compute(key) do |old_value|
274
+ if old_value.nil? && missing
275
+ [value].freeze
276
+ else
277
+ unless old_value.is_a?(Array)
278
+ raise TypeError,
279
+ "existing value for #{key.inspect} must be an Array: #{old_value.inspect}"
280
+ end
281
+
282
+ (old_value + [value]).freeze
283
+ end
284
+ end
285
+ end
286
+
287
+ # Atomically add +value+ to a Set bucket for +key+.
288
+ #
289
+ # The stored Set is replaced rather than mutated in place.
290
+ #
291
+ # @param key [Object] bucket key to update
292
+ # @param value [Object] value to add to the set
293
+ # @return [Set] the newly stored frozen Set
294
+ # @raise [TypeError] if the existing value is not a Set
295
+ def add_to_set(key, value)
296
+ missing = !key?(key)
297
+ compute(key) do |old_value|
298
+ if old_value.nil? && missing
299
+ Set[value].freeze
300
+ else
301
+ unless old_value.is_a?(Set)
302
+ raise TypeError,
303
+ "existing value for #{key.inspect} must be a Set: #{old_value.inspect}"
304
+ end
305
+
306
+ (old_value | [value]).freeze
307
+ end
308
+ end
309
+ end
310
+
40
311
  # Alias for #size.
41
312
  #
42
- # @return [Integer]
313
+ # @return [Integer] the current number of entries
43
314
  def length
44
315
  size
45
316
  end
46
317
 
47
318
  # Check whether the map currently has no entries.
48
319
  #
49
- # @return [Boolean]
320
+ # @return [Boolean] true when the map currently has no entries
50
321
  def empty?
51
322
  size.zero?
52
323
  end
53
- end
54
324
 
55
- ConcurrentHashMap.prepend(MapMethods)
325
+ # Alias for #key?.
326
+ #
327
+ # @param key [Object] lookup key
328
+ # @return [Boolean] true when the key currently exists
329
+ def include?(key)
330
+ key?(key)
331
+ end
332
+ alias member? include?
333
+ end
56
334
  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
  #
@@ -26,9 +28,9 @@ module Ratomic
26
28
  class Pool
27
29
  # Create a pool and seed it with +size+ objects from the factory block.
28
30
  #
29
- # @param size [Integer] number of pooled objects
31
+ # @param size [Integer] number of pooled objects to create up front
30
32
  # @param timeout [Numeric, nil] checkout timeout in seconds, or nil to wait indefinitely
31
- # @yieldreturn [Object] mutable object to store in the pool
33
+ # @yieldreturn [Object] a mutable object to place into the pool
32
34
  # @raise [ArgumentError] if +size+ is not positive
33
35
  # @raise [LocalJumpError] if no factory block is given
34
36
  def initialize(size = 5, timeout = 1.0)
@@ -47,7 +49,7 @@ module Ratomic
47
49
  # The returned object has been moved from the pool to the caller. The caller
48
50
  # owns it until it is passed to #checkin.
49
51
  #
50
- # @return [Object, nil] pooled object, or nil after timeout
52
+ # @return [Object, nil] the checked-out object, or nil if the timeout expires
51
53
  def checkout
52
54
  reply = Ractor::Port.new
53
55
  request_id = reply.object_id
@@ -66,8 +68,8 @@ module Ratomic
66
68
  # use the object after calling this method; Ruby raises Ractor::MovedError
67
69
  # for stale references.
68
70
  #
69
- # @param object [Object] previously checked-out pooled object
70
- # @return [nil]
71
+ # @param object [Object] the object previously checked out from the pool
72
+ # @return [nil] nothing useful is returned
71
73
  def checkin(object)
72
74
  @control.send([:checkin, object], move: true)
73
75
  nil
@@ -78,7 +80,7 @@ module Ratomic
78
80
  # This is primarily useful for tests and short-lived scripts. A closed pool
79
81
  # should not be used for further checkout/checkin operations.
80
82
  #
81
- # @return [nil]
83
+ # @return [nil] nothing useful is returned
82
84
  def close
83
85
  @control << [:shutdown]
84
86
  @control.value
@@ -92,9 +94,9 @@ module Ratomic
92
94
  # This is the preferred API because it guarantees checkin through an ensure
93
95
  # block. If checkout times out, raises Ratomic::Error and does not yield.
94
96
  #
95
- # @yieldparam object [Object] checked-out pooled object
97
+ # @yieldparam object [Object] the object checked out from the pool
96
98
  # @raise [Ratomic::Error] if checkout times out
97
- # @return [Object] block return value
99
+ # @return [Object] the block return value
98
100
  def with
99
101
  object = checkout
100
102
  raise Ratomic::Error, "pool checkout timeout" if object.nil?
data/lib/ratomic/queue.rb CHANGED
@@ -1,17 +1,71 @@
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] maximum number of items the queue can hold
18
+ # @return [Ratomic::Queue] a new shareable queue
19
+ # @raise [ArgumentError] if +capacity+ is outside the supported range
20
+ # @raise [TypeError] if +capacity+ cannot be converted to an Integer
21
+ #
22
+ # @!method push(item)
23
+ # Push an item, blocking until space is available.
24
+ #
25
+ # @param item [Object] the item to append to the queue
26
+ # @return [Ratomic::Queue] self
27
+ #
28
+ # @!method pop
29
+ # Pop an item, blocking until one is available.
30
+ #
31
+ # @return [Object] the next queued item
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] the next item, or nil if the queue is empty
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] true when the queue currently has no items
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] the current number of queued items
56
+ #
57
+ # @!method length
58
+ # Alias for #size.
59
+ #
60
+ # @return [Integer] the current number of queued items
61
+ class Queue
6
62
  # Push an item and return the queue for chaining.
7
63
  #
8
- # @param item [Object]
9
- # @return [Ratomic::Queue]
64
+ # @param item [Object] the item to append to the queue
65
+ # @return [Ratomic::Queue] self
10
66
  def <<(item)
11
67
  push(item)
12
68
  self
13
69
  end
14
70
  end
15
-
16
- Queue.prepend(QueueMethods)
17
71
  end
@@ -4,7 +4,9 @@ module Ratomic
4
4
  # Internal sentinel object for future Hash-like APIs that need to distinguish
5
5
  # missing keys from explicit nil values.
6
6
  class Undefined
7
- # @return [String]
7
+ # Return the sentinel's stable inspection string.
8
+ #
9
+ # @return [String] a human-readable sentinel marker
8
10
  def inspect
9
11
  "#<Undefined>"
10
12
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ratomic
4
- VERSION = "0.2.1"
4
+ # Current gem version string.
5
+ VERSION = "0.3.1"
5
6
  end
data/lib/ratomic.rb CHANGED
@@ -1,16 +1,21 @@
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"
12
-
3
+ # Ratomic provides mutable data structures for Ruby Ractors. Its primitives
4
+ # are backed by native Rust concurrency libraries so Ruby code can share useful
5
+ # state across Ractors without falling back to one global lock. Pool uses Ruby
6
+ # Ractor ownership-transfer primitives instead of the native Rust path.
7
+ #
8
+ # The public API currently includes {Counter}, {Map}, {Queue}, and {Pool}.
13
9
  module Ratomic
14
- # Base error for Ratomic-specific runtime failures.
10
+ # Base error class for Ratomic-specific failures.
15
11
  class Error < StandardError; end
16
12
  end
13
+
14
+ require "ratomic/ratomic"
15
+ require "ratomic/version"
16
+
17
+ require "ratomic/undefined"
18
+ require "ratomic/counter"
19
+ require "ratomic/map"
20
+ require "ratomic/queue"
21
+ 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.1
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: []