ratomic 0.4.0 → 0.4.2

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: a08c265d5b7a1c6f780a36dc2448b7cb49421c53f15957a242dd1457503b33c1
4
- data.tar.gz: 5dcbb8b1fd92a0e162dcc4daa9ca6cec31117c918bb984ddf03a5ccb68df8779
3
+ metadata.gz: 764ab083a8815f4f2bd0958be83d5209b10b21d8765dc19720fedd6d042eec89
4
+ data.tar.gz: 5bf02090ace17e0ca172bcc9f9f2a56f79030f0a92ecf669adecba72aa2ec641
5
5
  SHA512:
6
- metadata.gz: 0dccc6d4fcceed16ac0437d2550c25deb6ad83c4de57a5b0465d0de604f4376baa776312607d9bd361073c53eeef14e88c651db5cf3efb09bedcd3a38e9ee93d
7
- data.tar.gz: b6e9154857321453b8e0b2919ee35551beab7763b80c02358172537e35375628420f10c434d3e97432f87f99e567794de6343855a791d74aa0540d7d11e5ea7c
6
+ metadata.gz: c7a3e86bfe551b27b28af6655bd7853c1be683d69e759d6b628388282e7443030fe8b56c7e036e1c25f8234de74052fc621747eb94484e84e979b771aaf7ebcc
7
+ data.tar.gz: 03e9c06d8c5771ab340e1219113b5f67c05da3e97c5ad2a5b7ddefb9e5b54103988eb6cb7c299e09a0808e27fec660b3eaea8a7a3fc200a5aa8dd84161faf484
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## 0.4.2
4
+
5
+ ### Fixed
6
+
7
+ - Prevent `Map#compute` from deadlocking when multiple Ruby threads contend for the same key.
8
+
9
+ ## 0.4.1
10
+
11
+ ### Documentation
12
+
13
+ - Document the `LocalPool` factory contract.
14
+ - Clarify ownership-transfer versus resource-locality semantics.
15
+ - Add guidance for designing Ractor-shareable factory objects.
16
+ - Add examples of correct and incorrect factory implementations.
17
+ - Improve `LocalPool` API documentation and YARD comments.
18
+
19
+ ### Smoke Tests
20
+
21
+ - Add PostgreSQL smoke tests alongside Redis smoke tests.
22
+ - Consolidate smoke-test documentation.
23
+ - Add captured Redis and PostgreSQL validation snapshots.
24
+ - Document expected outcomes and interpretation of smoke-test runs.
25
+
26
+ ### Validation
27
+
28
+ Redis and PostgreSQL smoke tests now demonstrate:
29
+
30
+ - successful Thread and Ractor execution
31
+ - no `Ractor::MovedError`
32
+ - no `Ractor::IsolationError`
33
+ - successful queue draining
34
+ - preservation of live resource ownership
35
+ - safe reuse of Redis clients and PostgreSQL connections
36
+
37
+ These tests validate that `LocalPool` is a general-purpose
38
+ resource-locality primitive for stateful network clients rather
39
+ than a Redis-specific abstraction.
40
+
3
41
  ## [0.4.0] - 2026-06-10
4
42
 
5
43
  ### Added
@@ -10,6 +48,8 @@
10
48
  * Added RBS definitions for `LocalPool`.
11
49
  * Expanded API documentation and usage examples.
12
50
 
51
+
52
+
13
53
  ### Design
14
54
 
15
55
  `Ratomic::Pool` and `Ratomic::LocalPool` serve different ownership models:
data/README.md CHANGED
@@ -42,8 +42,8 @@ RBS signatures are included under `sig/` for downstream type checking.
42
42
 
43
43
  ## Examples And Benchmarks
44
44
 
45
- - [`redis_poc`](./redis_poc) contains local Redis scripts that exercise
46
- `Ratomic::Map`, `Ratomic::Counter`, and `Ratomic::LocalPool` under Thread and
45
+ - [`smoke_tests`](./smoke_tests) contains local Redis and PostgreSQL scripts that
46
+ exercise `Ratomic::LocalPool` with real stateful clients under Thread and
47
47
  Ractor workloads.
48
48
  - [`pgoutput-parser`](https://github.com/kanutocd/pgoutput-parser#relation-metadata-tracking)
49
49
  uses `Ratomic::Map` for relation metadata tracking in a real CDC pipeline
@@ -313,11 +313,12 @@ leave nested internal state unusable, producing errors such as
313
313
  `LocalPool` avoids that class of bug by not moving live resources at all. Work
314
314
  moves between Ractors. Live resources stay local.
315
315
 
316
- #### Redis smoke-test snapshot
316
+ #### Redis and PostgreSQL smoke-test snapshots
317
317
 
318
- The Redis POC includes two scripts under `redis_poc/`.
318
+ The smoke tests live under [`smoke_tests/`](./smoke_tests/) and exercise `LocalPool` with real
319
+ Redis clients and PostgreSQL connections.
319
320
 
320
- `basic_redis.rb` exercises repeated Redis operations from both Threads and
321
+ `redis/basic_redis.rb` exercises repeated Redis operations from both Threads and
321
322
  Ractors:
322
323
 
323
324
  ```text
@@ -328,7 +329,7 @@ Ractor
328
329
  [{"one" => 42419}, {"two" => 42186}, {"three" => 42400}, {"four" => 42206}, {"five" => 42568}]
329
330
  ```
330
331
 
331
- `queue_redis.rb` exercises a producer/consumer Redis queue workload:
332
+ `redis/queue_redis.rb` exercises a producer/consumer Redis queue workload:
332
333
 
333
334
  ```text
334
335
  [:start, Ractor, 2026-06-10 01:17:57.584727984 +0800]
@@ -338,15 +339,24 @@ Ractor
338
339
  [:end, 2026-06-10 01:18:02.226310862 +0800]
339
340
  ```
340
341
 
341
- These numbers are a smoke-test snapshot, not a formal benchmark claim. The
342
+ `postgres/basic_postgres.rb` exercises repeated PostgreSQL upserts from both
343
+ Threads and Ractors. `postgres/queue_postgres.rb` exercises a table-backed
344
+ producer/consumer workload using `FOR UPDATE SKIP LOCKED`.
345
+
346
+ These numbers are smoke-test snapshots, not formal benchmark claims. The
342
347
  important interpretation is:
343
348
 
344
349
  - no `Ractor::MovedError`
345
350
  - no `Ractor::IsolationError`
346
351
  - no process crash
347
352
  - all produced queue items were consumed
348
- - Redis queues drained to zero
349
- - live Redis clients remained owned by the Ractor that created them
353
+ - Redis queues and PostgreSQL table queues drained to zero
354
+ - live Redis clients and PostgreSQL connections remained owned by the Ractor
355
+ that created them
356
+
357
+ Redis proves the network-client shape. PostgreSQL proves the database-connection
358
+ shape. Together they demonstrate that `LocalPool` is not Redis-specific; it is a
359
+ resource-locality primitive for live stateful clients.
350
360
 
351
361
  #### Inception pool
352
362
 
@@ -54,6 +54,21 @@ impl MapStore {
54
54
  self.map.len()
55
55
  }
56
56
 
57
+ fn with_entry<F, R, W>(&self, key: VALUE, mut on_locked: W, f: F) -> R
58
+ where
59
+ F: FnOnce(Entry<'_, RubyHashEql, VALUE>) -> R,
60
+ W: FnMut(),
61
+ {
62
+ let mut f = Some(f);
63
+ loop {
64
+ if let Some(entry) = self.map.try_entry(RubyHashEql(key)) {
65
+ return f.take().expect("entry closure already used")(entry);
66
+ }
67
+
68
+ on_locked();
69
+ }
70
+ }
71
+
57
72
  pub fn fetch_and_modify<F>(&self, key: VALUE, f: F)
58
73
  where
59
74
  F: FnOnce(VALUE) -> VALUE,
@@ -61,11 +76,18 @@ impl MapStore {
61
76
  self.map.alter(&RubyHashEql(key), |_, value| f(value));
62
77
  }
63
78
 
64
- pub fn compute<F, E>(&self, key: VALUE, missing: VALUE, f: F) -> Result<VALUE, E>
79
+ pub fn compute<F, E, W>(
80
+ &self,
81
+ key: VALUE,
82
+ missing: VALUE,
83
+ on_locked: W,
84
+ f: F,
85
+ ) -> Result<VALUE, E>
65
86
  where
66
87
  F: FnOnce(VALUE) -> Result<VALUE, E>,
88
+ W: FnMut(),
67
89
  {
68
- match self.map.entry(RubyHashEql(key)) {
90
+ self.with_entry(key, on_locked, |entry| match entry {
69
91
  Entry::Occupied(mut entry) => {
70
92
  let new_value = f(*entry.get())?;
71
93
  entry.insert(new_value);
@@ -76,7 +98,7 @@ impl MapStore {
76
98
  entry.insert(new_value);
77
99
  Ok(new_value)
78
100
  }
79
- }
101
+ })
80
102
  }
81
103
 
82
104
  pub fn update<F, E>(&self, key: VALUE, f: F) -> Result<VALUE, E>
@@ -18,7 +18,7 @@ use magnus::{
18
18
  use mpmc_queue::MpmcQueue;
19
19
  use parking_lot::Mutex;
20
20
  use rb_sys::{rb_ext_ractor_safe, rb_thread_call_without_gvl, ruby_special_consts, VALUE};
21
- use std::{ffi::c_void, mem::transmute};
21
+ use std::{ffi::c_void, mem::transmute, time::Duration};
22
22
 
23
23
  fn value_to_raw(value: Value) -> VALUE {
24
24
  unsafe { transmute::<Value, VALUE>(value) }
@@ -38,6 +38,22 @@ fn make_shareable(ruby: &Ruby, value: Value) -> Result<Value, Error> {
38
38
  ractor.funcall("make_shareable", (value,))
39
39
  }
40
40
 
41
+ unsafe extern "C" fn sleep_briefly_without_gvl(_: *mut c_void) -> *mut c_void {
42
+ std::thread::sleep(Duration::from_millis(1));
43
+ std::ptr::null_mut()
44
+ }
45
+
46
+ fn wait_for_entry_without_gvl() {
47
+ unsafe {
48
+ rb_thread_call_without_gvl(
49
+ Some(sleep_briefly_without_gvl),
50
+ std::ptr::null_mut(),
51
+ None,
52
+ std::ptr::null_mut(),
53
+ );
54
+ }
55
+ }
56
+
41
57
  struct Counter(AtomicCounter);
42
58
 
43
59
  impl Counter {
@@ -158,10 +174,15 @@ impl HashMap {
158
174
  }
159
175
 
160
176
  let proc = ruby.block_proc()?;
161
- let raw = rb_self.0.compute(value_to_raw(key), qnil_raw(), |value| {
162
- proc.call::<_, Value>((unsafe { value_from_raw(value) },))
163
- .map(value_to_raw)
164
- })?;
177
+ let raw = rb_self.0.compute(
178
+ value_to_raw(key),
179
+ qnil_raw(),
180
+ wait_for_entry_without_gvl,
181
+ |value| {
182
+ proc.call::<_, Value>((unsafe { value_from_raw(value) },))
183
+ .map(value_to_raw)
184
+ },
185
+ )?;
165
186
 
166
187
  Ok(unsafe { value_from_raw(raw) }.into_value_with(ruby))
167
188
  }
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Ratomic
4
4
  # Current gem version string.
5
- VERSION = "0.4.0"
5
+ VERSION = "0.4.2"
6
6
  end
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.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Perham
@@ -81,7 +81,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  requirements: []
84
- rubygems_version: 4.0.10
84
+ rubygems_version: 4.0.16
85
85
  specification_version: 4
86
86
  summary: Ractor-safe concurrent data structures for Ruby
87
87
  test_files: []