ratomic 0.3.5 → 0.4.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 +4 -4
- data/CHANGELOG.md +30 -0
- data/Cargo.toml +7 -0
- data/README.md +154 -4
- data/lib/ratomic/local_pool.rb +246 -0
- data/lib/ratomic/map.rb +17 -0
- data/lib/ratomic/version.rb +1 -1
- data/lib/ratomic.rb +6 -3
- data/sig/ratomic.rbs +8 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a08c265d5b7a1c6f780a36dc2448b7cb49421c53f15957a242dd1457503b33c1
|
|
4
|
+
data.tar.gz: 5dcbb8b1fd92a0e162dcc4daa9ca6cec31117c918bb984ddf03a5ccb68df8779
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0dccc6d4fcceed16ac0437d2550c25deb6ad83c4de57a5b0465d0de604f4376baa776312607d9bd361073c53eeef14e88c651db5cf3efb09bedcd3a38e9ee93d
|
|
7
|
+
data.tar.gz: b6e9154857321453b8e0b2919ee35551beab7763b80c02358172537e35375628420f10c434d3e97432f87f99e567794de6343855a791d74aa0540d7d11e5ea7c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-06-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
* Introduced `Ratomic::LocalPool`, a new pooling primitive for live resources that should remain local to the Ractor that created them.
|
|
8
|
+
* Added Redis-based smoke tests demonstrating safe operation across both Threads and Ractors.
|
|
9
|
+
* Added queue producer/consumer Redis examples.
|
|
10
|
+
* Added RBS definitions for `LocalPool`.
|
|
11
|
+
* Expanded API documentation and usage examples.
|
|
12
|
+
|
|
13
|
+
### Design
|
|
14
|
+
|
|
15
|
+
`Ratomic::Pool` and `Ratomic::LocalPool` serve different ownership models:
|
|
16
|
+
|
|
17
|
+
* `Pool` — ownership transfer
|
|
18
|
+
* `LocalPool` — ownership preservation
|
|
19
|
+
|
|
20
|
+
`LocalPool` is intended for resources such as:
|
|
21
|
+
|
|
22
|
+
* Redis clients
|
|
23
|
+
* Database connections
|
|
24
|
+
* HTTP clients
|
|
25
|
+
* Kafka producers
|
|
26
|
+
* Other stateful network resources
|
|
27
|
+
|
|
28
|
+
### Notes
|
|
29
|
+
|
|
30
|
+
`LocalPool` is implemented in pure Ruby and is not backed by Ratomic's Rust extension.
|
|
31
|
+
|
|
32
|
+
|
|
3
33
|
## [0.3.5] - 2026-06-06
|
|
4
34
|
|
|
5
35
|
- Fix the native loader so development loads the compiled extension from the
|
data/Cargo.toml
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
members = ["ext/ratomic"]
|
|
3
3
|
resolver = "2"
|
|
4
4
|
|
|
5
|
+
[profile.dev]
|
|
6
|
+
debug = true
|
|
7
|
+
|
|
5
8
|
[profile.release]
|
|
6
9
|
panic = "abort"
|
|
7
10
|
lto = true
|
|
8
11
|
codegen-units = 1
|
|
12
|
+
# By default, debug symbols are stripped from the final binary which makes it
|
|
13
|
+
# harder to debug if something goes wrong. It's recommended to keep debug
|
|
14
|
+
# symbols in the release build so that you can debug the final binary if needed.
|
|
15
|
+
debug = false
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.ruby-lang.org/en/)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
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`
|
|
8
|
+
Ratomic provides mutable data structures for Ruby Ractors. Its core shared 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` and `LocalPool` are pure Ruby primitives that use Ruby Ractor ownership and locality semantics instead of the native Rust path.
|
|
9
9
|
|
|
10
10
|
## Project Direction
|
|
11
11
|
|
|
@@ -43,18 +43,27 @@ RBS signatures are included under `sig/` for downstream type checking.
|
|
|
43
43
|
## Examples And Benchmarks
|
|
44
44
|
|
|
45
45
|
- [`redis_poc`](./redis_poc) contains local Redis scripts that exercise
|
|
46
|
-
`Ratomic::Map`, `Ratomic::Counter`, and `Ratomic::
|
|
46
|
+
`Ratomic::Map`, `Ratomic::Counter`, and `Ratomic::LocalPool` under Thread and
|
|
47
47
|
Ractor workloads.
|
|
48
|
+
- [`pgoutput-parser`](https://github.com/kanutocd/pgoutput-parser#relation-metadata-tracking)
|
|
49
|
+
uses `Ratomic::Map` for relation metadata tracking in a real CDC pipeline
|
|
50
|
+
POC, with a matching benchmark and deeper implementation notes in
|
|
51
|
+
[docs/relation_tracker.md](https://github.com/kanutocd/pgoutput-parser/blob/main/docs/relation_tracker.md).
|
|
52
|
+
- [`sidekiq-tenant-policy-cache`](https://github.com/kanutocd/sidekiq-tenant-policy-cache)
|
|
53
|
+
shows `Ratomic::Map` and `Ratomic::Counter` in Sidekiq middleware for tenant
|
|
54
|
+
policy caching and cache-hit / cache-miss tracking, with a benchmarked
|
|
55
|
+
cache-vs-policy-every-job comparison.
|
|
48
56
|
- The [`cdc-parallel` Ratomic benchmark][cdc-parallel-ratomic] demonstrates
|
|
49
57
|
Ractor workers updating shared CDC processing metrics through `Ratomic::Map`
|
|
50
58
|
and `Ratomic::Counter`.
|
|
51
59
|
|
|
52
60
|
## Usage
|
|
53
61
|
|
|
54
|
-
Ratomic provides
|
|
62
|
+
Ratomic provides three safety models:
|
|
55
63
|
|
|
56
64
|
- `Counter`, `Map`, and `Queue` are shared concurrent structures.
|
|
57
|
-
- `Pool` transfers ownership of mutable objects between Ractors.
|
|
65
|
+
- `Pool` transfers ownership of plain mutable objects between Ractors.
|
|
66
|
+
- `LocalPool` keeps live resources local to the Ractor that created them.
|
|
58
67
|
|
|
59
68
|
That distinction matters. A mutable pooled object is not shared by multiple Ractors
|
|
60
69
|
at the same time. It is moved to the caller on checkout and moved back to the pool
|
|
@@ -119,6 +128,11 @@ groups.append("jobs", "import") # => ["import"]
|
|
|
119
128
|
groups.add_to_set("workers", "alpha") # => #<Set: {"alpha"}>
|
|
120
129
|
```
|
|
121
130
|
|
|
131
|
+
Some `Map` methods hold an internal guard while a block runs or while a
|
|
132
|
+
reference is live. Avoid re-entering the same map from inside those blocks or
|
|
133
|
+
mutating the same key while holding a reference from `get` or `[]`. The API
|
|
134
|
+
docs cover the exact locking caveats.
|
|
135
|
+
|
|
122
136
|
### `Ratomic::Queue`
|
|
123
137
|
|
|
124
138
|
`Ratomic::Queue` is a Ractor-shareable multi-producer, multi-consumer queue.
|
|
@@ -219,6 +233,142 @@ The lower-level `Ratomic::FixedSizeObjectPool` native class may still exist, but
|
|
|
219
233
|
`Ratomic::Pool` does not inherit from it. The public `Pool` API is implemented
|
|
220
234
|
in Ruby so it can use Ruby's Ractor ownership primitives directly.
|
|
221
235
|
|
|
236
|
+
|
|
237
|
+
### `Ratomic::LocalPool`
|
|
238
|
+
|
|
239
|
+
`Ratomic::LocalPool` is the safe pool shape for live resources that should stay
|
|
240
|
+
local to the Ractor that created them.
|
|
241
|
+
|
|
242
|
+
Use it for resources such as:
|
|
243
|
+
|
|
244
|
+
- Redis clients
|
|
245
|
+
- database connections
|
|
246
|
+
- HTTP clients
|
|
247
|
+
- Kafka producers
|
|
248
|
+
- OpenSearch clients
|
|
249
|
+
- per-worker caches, buffers, encoders, or aggregators
|
|
250
|
+
|
|
251
|
+
Unlike `Ratomic::Pool`, `LocalPool` does **not** move pooled objects between
|
|
252
|
+
Ractors. The `LocalPool` instance is a shareable facade. Each Ractor lazily
|
|
253
|
+
creates and owns its own private, thread-safe resource pool behind that facade.
|
|
254
|
+
Threads inside the same Ractor share that local pool, but different Ractors
|
|
255
|
+
never share the live resources.
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
require "ratomic"
|
|
259
|
+
require "redis-client"
|
|
260
|
+
|
|
261
|
+
RedisFactory = Data.define(:host) do
|
|
262
|
+
def call
|
|
263
|
+
RedisClient.new(host: host)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
REDIS = Ratomic::LocalPool.new(
|
|
268
|
+
size: 10,
|
|
269
|
+
timeout: 1,
|
|
270
|
+
factory: RedisFactory.new("127.0.0.1".freeze)
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
REDIS.with do |client|
|
|
274
|
+
client.call("ping")
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Use `Pool` for plain mutable values where ownership transfer is the intended
|
|
279
|
+
safety model. Use `LocalPool` for live resources that should be created, used,
|
|
280
|
+
and reused inside the same Ractor.
|
|
281
|
+
|
|
282
|
+
The intended topology is:
|
|
283
|
+
|
|
284
|
+
```text
|
|
285
|
+
shareable LocalPool facade
|
|
286
|
+
↓
|
|
287
|
+
one local resource pool per Ractor
|
|
288
|
+
↓
|
|
289
|
+
threads inside that Ractor share local resources
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The mental model is intentionally close to a Ruby local variable: the resource is
|
|
293
|
+
local to the execution scope that owns it. For `LocalPool`, that scope is the
|
|
294
|
+
current Ractor.
|
|
295
|
+
|
|
296
|
+
#### Pure Ruby implementation
|
|
297
|
+
|
|
298
|
+
`LocalPool` is implemented in pure Ruby.
|
|
299
|
+
|
|
300
|
+
Unlike `Counter`, `Map`, and `Queue`, it is not backed by the Rust native
|
|
301
|
+
extension. Its safety comes from Ruby Ractor ownership boundaries and locality,
|
|
302
|
+
not from Rust synchronization primitives.
|
|
303
|
+
|
|
304
|
+
#### Why not use `Pool` for Redis clients?
|
|
305
|
+
|
|
306
|
+
`Pool` moves checked-out objects between Ractors. That is correct for plain
|
|
307
|
+
mutable Ruby values such as arrays or buffers, but it is a poor fit for live I/O
|
|
308
|
+
resources. Redis clients, database connections, sockets, and similar resources
|
|
309
|
+
carry internal connection state. Moving those objects across Ractor boundaries can
|
|
310
|
+
leave nested internal state unusable, producing errors such as
|
|
311
|
+
`Ractor::MovedError`.
|
|
312
|
+
|
|
313
|
+
`LocalPool` avoids that class of bug by not moving live resources at all. Work
|
|
314
|
+
moves between Ractors. Live resources stay local.
|
|
315
|
+
|
|
316
|
+
#### Redis smoke-test snapshot
|
|
317
|
+
|
|
318
|
+
The Redis POC includes two scripts under `redis_poc/`.
|
|
319
|
+
|
|
320
|
+
`basic_redis.rb` exercises repeated Redis operations from both Threads and
|
|
321
|
+
Ractors:
|
|
322
|
+
|
|
323
|
+
```text
|
|
324
|
+
Thread
|
|
325
|
+
[{"one" => 31501}, {"two" => 31379}, {"three" => 31320}, {"four" => 31410}, {"five" => 31454}]
|
|
326
|
+
|
|
327
|
+
Ractor
|
|
328
|
+
[{"one" => 42419}, {"two" => 42186}, {"three" => 42400}, {"four" => 42206}, {"five" => 42568}]
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`queue_redis.rb` exercises a producer/consumer Redis queue workload:
|
|
332
|
+
|
|
333
|
+
```text
|
|
334
|
+
[:start, Ractor, 2026-06-10 01:17:57.584727984 +0800]
|
|
335
|
+
[[:producer_done, 0], [:producer_done, 1]]
|
|
336
|
+
[[:consumer_done, 0, 7998], [:consumer_done, 1, 7987], [:consumer_done, 2, 7984], [:consumer_done, 3, 8035], [:consumer_done, 4, 7996]]
|
|
337
|
+
[{"one" => 0}, {"two" => 0}, {"three" => 0}, {"four" => 0}, {"five" => 0}]
|
|
338
|
+
[:end, 2026-06-10 01:18:02.226310862 +0800]
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
These numbers are a smoke-test snapshot, not a formal benchmark claim. The
|
|
342
|
+
important interpretation is:
|
|
343
|
+
|
|
344
|
+
- no `Ractor::MovedError`
|
|
345
|
+
- no `Ractor::IsolationError`
|
|
346
|
+
- no process crash
|
|
347
|
+
- all produced queue items were consumed
|
|
348
|
+
- Redis queues drained to zero
|
|
349
|
+
- live Redis clients remained owned by the Ractor that created them
|
|
350
|
+
|
|
351
|
+
#### Inception pool
|
|
352
|
+
|
|
353
|
+
Internally, `LocalPool` follows the "inception pool" shape discovered while
|
|
354
|
+
experimenting with Redis clients under Ruby Ractors:
|
|
355
|
+
|
|
356
|
+
```text
|
|
357
|
+
pool facade
|
|
358
|
+
↓
|
|
359
|
+
local pool
|
|
360
|
+
↓
|
|
361
|
+
resource
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
That same ownership pattern appears in hybrid execution runtimes: put parallel
|
|
365
|
+
workers on the outside, keep I/O concurrency and live resources inside the worker
|
|
366
|
+
that owns them. Ratomic keeps the primitive general-purpose and independent of
|
|
367
|
+
any specific runtime, scheduler, database, or message system.
|
|
368
|
+
|
|
369
|
+
`LocalPool#close` closes only the current Ractor's local pool. Other Ractors own
|
|
370
|
+
their own pools and must close them independently when needed.
|
|
371
|
+
|
|
222
372
|
## Contributing
|
|
223
373
|
|
|
224
374
|
Please read the [Code of Conduct](./CODE_OF_CONDUCT.md) before contributing.
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
module Ratomic
|
|
6
|
+
# Design Note
|
|
7
|
+
#
|
|
8
|
+
# LocalPool originated while investigating Redis clients under Ruby
|
|
9
|
+
# Ractors. The original goal was to reuse Pool, but ownership-transfer
|
|
10
|
+
# semantics proved incompatible with live resources containing internal
|
|
11
|
+
# state.
|
|
12
|
+
#
|
|
13
|
+
# The resulting architecture became known as the "Inception Pool"
|
|
14
|
+
# design:
|
|
15
|
+
#
|
|
16
|
+
# LocalPool facade
|
|
17
|
+
# ↓
|
|
18
|
+
# Ractor-local pool
|
|
19
|
+
# ↓
|
|
20
|
+
# Live resources
|
|
21
|
+
#
|
|
22
|
+
# or informally:
|
|
23
|
+
#
|
|
24
|
+
# Pool
|
|
25
|
+
# ↓
|
|
26
|
+
# Pool
|
|
27
|
+
# ↓
|
|
28
|
+
# Resource
|
|
29
|
+
#
|
|
30
|
+
# The public API intentionally uses the more descriptive name `LocalPool`.
|
|
31
|
+
#
|
|
32
|
+
# A shareable facade over resources that stay local to each Ractor.
|
|
33
|
+
#
|
|
34
|
+
# LocalPool is intended for live resources such as Redis clients,
|
|
35
|
+
# database connections, sockets, and other objects which must not be moved
|
|
36
|
+
# between Ractors. The facade itself is shareable, but each Ractor lazily
|
|
37
|
+
# creates and owns an independent thread-safe local pool. Threads inside the
|
|
38
|
+
# same Ractor share that local pool; different Ractors never share the live
|
|
39
|
+
# resources.
|
|
40
|
+
#
|
|
41
|
+
# This is the correct shape for resources with process, socket, connection,
|
|
42
|
+
# or native state. Move work across Ractor boundaries, not live clients.
|
|
43
|
+
#
|
|
44
|
+
# The factory must be Ractor-shareable because the facade stores it and each
|
|
45
|
+
# Ractor calls it when its local resource pool needs to create a resource. Prefer a
|
|
46
|
+
# small immutable callable object instead of a block when the pool will be
|
|
47
|
+
# used from multiple Ractors.
|
|
48
|
+
#
|
|
49
|
+
# @example Redis clients owned by each Ractor
|
|
50
|
+
# RedisFactory = Data.define(:host) do
|
|
51
|
+
# def call
|
|
52
|
+
# RedisClient.new(host: host)
|
|
53
|
+
# end
|
|
54
|
+
# end
|
|
55
|
+
#
|
|
56
|
+
# REDIS = Ratomic::LocalPool.new(
|
|
57
|
+
# size: 5,
|
|
58
|
+
# timeout: 1,
|
|
59
|
+
# factory: RedisFactory.new("127.0.0.1".freeze)
|
|
60
|
+
# )
|
|
61
|
+
#
|
|
62
|
+
# REDIS.with { |client| client.call("ping") }
|
|
63
|
+
#
|
|
64
|
+
# @note LocalPool is implemented in pure Ruby. It is not backed by the Rust
|
|
65
|
+
# native extension used by Counter, Map, and Queue. Its safety comes from
|
|
66
|
+
# Ruby Ractor locality: live resources are created and reused inside the
|
|
67
|
+
# Ractor that owns them.
|
|
68
|
+
#
|
|
69
|
+
# @see Pool Use Pool for plain mutable Ruby values where ownership transfer
|
|
70
|
+
# is the desired safety model.
|
|
71
|
+
class LocalPool
|
|
72
|
+
# Create a per-Ractor local pool facade.
|
|
73
|
+
#
|
|
74
|
+
# @param size [Integer] maximum number of resources in each Ractor-local pool
|
|
75
|
+
# @param timeout [Numeric, nil] checkout timeout in seconds, or nil to wait indefinitely
|
|
76
|
+
# @param factory [#call, nil] shareable object factory
|
|
77
|
+
# @yieldreturn [Object] resource created inside the current Ractor
|
|
78
|
+
# @raise [ArgumentError] if size, timeout, or factory is invalid
|
|
79
|
+
# @raise [LocalJumpError] if no factory or block is given
|
|
80
|
+
def initialize(size: 5, timeout: 1.0, factory: nil, &block)
|
|
81
|
+
raise ArgumentError, "pool size must be positive" unless size.is_a?(Integer) && size.positive?
|
|
82
|
+
raise ArgumentError, "pool timeout must be numeric or nil" unless timeout.nil? || timeout.is_a?(Numeric)
|
|
83
|
+
raise ArgumentError, "pool timeout must be non-negative" if timeout && timeout.negative?
|
|
84
|
+
raise ArgumentError, "use either factory: or block, not both" if factory && block
|
|
85
|
+
|
|
86
|
+
factory ||= block
|
|
87
|
+
raise LocalJumpError, "no factory given" unless factory
|
|
88
|
+
raise ArgumentError, "factory must respond to #call" unless factory.respond_to?(:call)
|
|
89
|
+
raise ArgumentError, "factory must be Ractor-shareable" unless Ractor.shareable?(factory)
|
|
90
|
+
|
|
91
|
+
@size = size
|
|
92
|
+
@timeout = timeout&.to_f
|
|
93
|
+
@factory = factory
|
|
94
|
+
@storage_key = :"ratomic_local_pool_#{object_id}"
|
|
95
|
+
|
|
96
|
+
freeze
|
|
97
|
+
Ractor.make_shareable(self)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Checkout a current-Ractor-owned resource, yield it, then return it to the
|
|
101
|
+
# same Ractor-local pool.
|
|
102
|
+
#
|
|
103
|
+
# No resource is moved between Ractors. The yielded object belongs to the
|
|
104
|
+
# Ractor which called this method.
|
|
105
|
+
#
|
|
106
|
+
# @yieldparam object [Object] current-Ractor-owned resource
|
|
107
|
+
# @raise [Ratomic::Error] if checkout times out
|
|
108
|
+
# @return [Object] the block return value
|
|
109
|
+
def with
|
|
110
|
+
local_pool.with { |object| yield object }
|
|
111
|
+
rescue Timeout::Error
|
|
112
|
+
raise Ratomic::Error, "pool checkout timeout"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Close the current Ractor's local pool, if it has been initialized.
|
|
116
|
+
#
|
|
117
|
+
# Other Ractors own independent local pools and are not affected. Available
|
|
118
|
+
# resources are closed if they respond to #close. Resources currently
|
|
119
|
+
# checked out by threads in this Ractor are closed when returned.
|
|
120
|
+
#
|
|
121
|
+
# @return [nil]
|
|
122
|
+
def close
|
|
123
|
+
pool = Ractor.current[@storage_key]
|
|
124
|
+
return nil unless pool
|
|
125
|
+
|
|
126
|
+
Ractor.current[@storage_key] = nil
|
|
127
|
+
pool.close
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Minimal thread-safe resource pool used inside exactly one Ractor.
|
|
132
|
+
class ResourcePool
|
|
133
|
+
def initialize(size:, timeout:, factory:)
|
|
134
|
+
@size = size
|
|
135
|
+
@timeout = timeout
|
|
136
|
+
@factory = factory
|
|
137
|
+
@available = []
|
|
138
|
+
@created = 0
|
|
139
|
+
@closed = false
|
|
140
|
+
@mutex = Mutex.new
|
|
141
|
+
@condition = ConditionVariable.new
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def with
|
|
145
|
+
object = checkout
|
|
146
|
+
yield object
|
|
147
|
+
ensure
|
|
148
|
+
checkin(object) if object
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def close
|
|
152
|
+
objects = nil
|
|
153
|
+
|
|
154
|
+
@mutex.synchronize do
|
|
155
|
+
@closed = true
|
|
156
|
+
objects = @available.dup
|
|
157
|
+
@available.clear
|
|
158
|
+
@condition.broadcast
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
objects.each { |object| close_object(object) }
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
private
|
|
166
|
+
|
|
167
|
+
def checkout
|
|
168
|
+
deadline = monotonic_deadline
|
|
169
|
+
|
|
170
|
+
should_create = @mutex.synchronize do
|
|
171
|
+
raise IOError, "pool is closed" if @closed
|
|
172
|
+
|
|
173
|
+
loop do
|
|
174
|
+
object = @available.pop
|
|
175
|
+
return object if object
|
|
176
|
+
|
|
177
|
+
if @created < @size
|
|
178
|
+
@created += 1
|
|
179
|
+
break true
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
wait_for_available(deadline)
|
|
183
|
+
raise IOError, "pool is closed" if @closed
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
create_object if should_create
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def checkin(object)
|
|
191
|
+
close_now = false
|
|
192
|
+
|
|
193
|
+
@mutex.synchronize do
|
|
194
|
+
if @closed
|
|
195
|
+
close_now = true
|
|
196
|
+
else
|
|
197
|
+
@available << object
|
|
198
|
+
@condition.signal
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
close_object(object) if close_now
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def create_object
|
|
207
|
+
@factory.call
|
|
208
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
209
|
+
@mutex.synchronize do
|
|
210
|
+
@created -= 1
|
|
211
|
+
@condition.signal
|
|
212
|
+
end
|
|
213
|
+
raise
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def close_object(object)
|
|
217
|
+
object.close if object.respond_to?(:close)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def monotonic_deadline
|
|
221
|
+
return nil unless @timeout
|
|
222
|
+
|
|
223
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) + @timeout
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def wait_for_available(deadline)
|
|
227
|
+
if deadline
|
|
228
|
+
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
229
|
+
raise Timeout::Error, "pool checkout timeout" if remaining <= 0
|
|
230
|
+
|
|
231
|
+
@condition.wait(@mutex, remaining)
|
|
232
|
+
else
|
|
233
|
+
@condition.wait(@mutex)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
private
|
|
239
|
+
|
|
240
|
+
def local_pool
|
|
241
|
+
Ractor.current[@storage_key] ||= ResourcePool.new(size: @size, timeout: @timeout, factory: @factory)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private_constant :ResourcePool
|
|
245
|
+
end
|
|
246
|
+
end
|
data/lib/ratomic/map.rb
CHANGED
|
@@ -11,6 +11,11 @@ module Ratomic
|
|
|
11
11
|
# This is not a full Hash replacement. Iteration and arbitrary mutable object
|
|
12
12
|
# borrowing are intentionally absent.
|
|
13
13
|
#
|
|
14
|
+
# Some methods on this type hold an internal entry guard while the block runs
|
|
15
|
+
# or while a reference is live. Do not call back into the same map from inside
|
|
16
|
+
# those blocks, and do not hold a reference from #get or #[] while mutating
|
|
17
|
+
# the same key. That can deadlock the underlying DashMap bucket.
|
|
18
|
+
#
|
|
14
19
|
# @example Store pipeline offsets
|
|
15
20
|
# OFFSETS = Ratomic::Map.new
|
|
16
21
|
# OFFSETS[:source_a] = 42
|
|
@@ -22,6 +27,10 @@ module Ratomic
|
|
|
22
27
|
# Missing keys return nil, so use #key? or #fetch when stored nil values
|
|
23
28
|
# need to be distinguished from missing entries.
|
|
24
29
|
#
|
|
30
|
+
# If you keep the returned reference alive and then mutate the same key, you
|
|
31
|
+
# can deadlock the underlying DashMap bucket. Copy out what you need and let
|
|
32
|
+
# the reference go out of scope before mutating.
|
|
33
|
+
#
|
|
25
34
|
# @param key [Object] lookup key
|
|
26
35
|
# @return [Object, nil] the stored value, or nil when the key is missing
|
|
27
36
|
#
|
|
@@ -102,6 +111,10 @@ module Ratomic
|
|
|
102
111
|
# into the same map from inside the block. Prefer native update helpers such
|
|
103
112
|
# as #increment when they fit the workflow.
|
|
104
113
|
#
|
|
114
|
+
# Never call this method from inside another live reference to the same
|
|
115
|
+
# entry or from a block that already holds the same map's guard. That can
|
|
116
|
+
# deadlock the bucket.
|
|
117
|
+
#
|
|
105
118
|
# If the block raises, the previous value is preserved. If the key was
|
|
106
119
|
# missing, no entry is inserted.
|
|
107
120
|
#
|
|
@@ -141,6 +154,10 @@ module Ratomic
|
|
|
141
154
|
# into the same map from inside the block. Prefer native update helpers such
|
|
142
155
|
# as #increment when they fit the workflow.
|
|
143
156
|
#
|
|
157
|
+
# Never call this method from inside another live reference to the same
|
|
158
|
+
# entry or from a block that already holds the same map's guard. That can
|
|
159
|
+
# deadlock the bucket.
|
|
160
|
+
#
|
|
144
161
|
# If the block raises, the previous value is preserved.
|
|
145
162
|
#
|
|
146
163
|
# @param key [Object] key to update
|
data/lib/ratomic/version.rb
CHANGED
data/lib/ratomic.rb
CHANGED
|
@@ -4,10 +4,11 @@ require "rbconfig"
|
|
|
4
4
|
|
|
5
5
|
# Ratomic provides mutable data structures for Ruby Ractors. Its primitives
|
|
6
6
|
# are backed by native Rust concurrency libraries so Ruby code can share useful
|
|
7
|
-
# state across Ractors without falling back to one global lock. Pool
|
|
8
|
-
#
|
|
7
|
+
# state across Ractors without falling back to one global lock. `Pool` and
|
|
8
|
+
# `LocalPool` are pure Ruby primitives which use Ruby Ractor ownership and
|
|
9
|
+
# locality semantics instead of the native Rust path.
|
|
9
10
|
#
|
|
10
|
-
# The public API currently includes {Counter}, {Map}, {Queue}, and {
|
|
11
|
+
# The public API currently includes {Counter}, {Map}, {Queue}, {Pool}, and {LocalPool}.
|
|
11
12
|
module Ratomic
|
|
12
13
|
# Base error class for Ratomic-specific failures.
|
|
13
14
|
class Error < StandardError; end
|
|
@@ -53,3 +54,5 @@ require "ratomic/counter"
|
|
|
53
54
|
require "ratomic/map"
|
|
54
55
|
require "ratomic/queue"
|
|
55
56
|
require "ratomic/pool"
|
|
57
|
+
|
|
58
|
+
require "ratomic/local_pool"
|
data/sig/ratomic.rbs
CHANGED
|
@@ -62,6 +62,14 @@ module Ratomic
|
|
|
62
62
|
def length: () -> Integer
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
+
class LocalPool
|
|
66
|
+
def self.new: (?size: Integer, ?timeout: (Numeric | nil), factory: untyped) -> LocalPool
|
|
67
|
+
| (?size: Integer, ?timeout: (Numeric | nil)) { () -> untyped } -> LocalPool
|
|
68
|
+
|
|
69
|
+
def with: () { (untyped object) -> untyped } -> untyped
|
|
70
|
+
def close: () -> nil
|
|
71
|
+
end
|
|
72
|
+
|
|
65
73
|
class Pool
|
|
66
74
|
def self.new: (?Integer size, ?(Numeric | nil) timeout) { () -> untyped } -> Pool
|
|
67
75
|
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Perham
|
|
@@ -50,6 +50,7 @@ files:
|
|
|
50
50
|
- ext/ratomic/src/sem.rs
|
|
51
51
|
- lib/ratomic.rb
|
|
52
52
|
- lib/ratomic/counter.rb
|
|
53
|
+
- lib/ratomic/local_pool.rb
|
|
53
54
|
- lib/ratomic/map.rb
|
|
54
55
|
- lib/ratomic/pool.rb
|
|
55
56
|
- lib/ratomic/queue.rb
|