safe_memoize 1.7.0 → 1.7.1

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: e7a646739aa7f6c178b20192ddc8e94768df0c6b000097586796153f5d2138bc
4
- data.tar.gz: 9454f9a6cf6b8e420d27b82f39740484eddab506d871e94034483ae8caf12622
3
+ metadata.gz: 0b2558ad027e3b8bb09ec956d8a18eed759b56c9baa1234a173fdebd7e5e9913
4
+ data.tar.gz: ae5302c29efe49c336a323bc7a6ad8dcd4a6b10cebc394e786fccd12c00a66ab
5
5
  SHA512:
6
- metadata.gz: f569af43ba1ca765dcdd9490f5629bb572f1dbedaa676538a08c199282fe55a6b40afd29f8870ddbab86c091a20a89c10e87c3b060a124f4994313b2a2ffdda4
7
- data.tar.gz: 2d0634278dd32e98fc0c23a74abfb98bf1c8d5d087e7eb7a8006dd54846bd11f0bbce27d704e4f82bf0fc81c961c34b1c62fb8dc92cddafbcec26c2b930e79d8
6
+ metadata.gz: 5822061de749e6001cee636a687c19bfd59253ed075b4d9e82123aa88954578a3a8f2dcd0a5a9cbfb29cad8d86b6768a5461a454210dfd7bf56ba624e4e6df6c
7
+ data.tar.gz: 89e21d16cb9e37a00333f98ec12adf7b9ec6a3e746bb1a9f8377cfafd16c27abbd92de5a79074e207f51fcacaa25a0e9c58092da94a82798be501d35a677489c
@@ -34,6 +34,9 @@ jobs:
34
34
  test:
35
35
  name: Ruby ${{ matrix.ruby }}
36
36
  runs-on: ubuntu-latest
37
+ # Normal runs finish in under a minute; fail fast if the suite hangs
38
+ # (default is 6 hours).
39
+ timeout-minutes: 10
37
40
  strategy:
38
41
  fail-fast: false
39
42
  matrix:
@@ -55,6 +58,12 @@ jobs:
55
58
  - name: Run test suite
56
59
  run: bundle exec rake spec
57
60
 
61
+ # SimpleCov marks :nocov: lines as "ignored"; Codecov rejects non-numeric
62
+ # line values and discards the whole report, so treat them as non-lines.
63
+ - name: Normalize ignored lines for Codecov
64
+ if: matrix.ruby == '3.4'
65
+ run: sed -i 's/"ignored"/null/g' coverage/coverage.json
66
+
58
67
  - name: Upload coverage to Codecov
59
68
  uses: codecov/codecov-action@v5
60
69
  if: matrix.ruby == '3.4'
@@ -62,3 +71,25 @@ jobs:
62
71
  token: ${{ secrets.CODECOV_TOKEN }}
63
72
  files: coverage/coverage.json
64
73
 
74
+ # The upload step succeeds even when Codecov later fails to process the
75
+ # report (state "error", 0% coverage), so poll the API and fail loudly.
76
+ - name: Verify Codecov processed the report
77
+ if: matrix.ruby == '3.4' && github.event.pull_request.head.repo.fork != true
78
+ env:
79
+ SHA: ${{ github.event.pull_request.head.sha || github.sha }}
80
+ run: |
81
+ owner="${GITHUB_REPOSITORY%%/*}"
82
+ repo="${GITHUB_REPOSITORY#*/}"
83
+ url="https://api.codecov.io/api/v2/github/${owner}/repos/${repo}/commits/${SHA}/"
84
+ for attempt in $(seq 1 18); do
85
+ state=$(curl -fsS "$url" | jq -r '.state' || true)
86
+ echo "Codecov state for ${SHA}: ${state:-unknown} (attempt ${attempt})"
87
+ case "$state" in
88
+ complete) exit 0 ;;
89
+ error)
90
+ echo "::error::Codecov failed to process the coverage report for ${SHA}"
91
+ exit 1 ;;
92
+ esac
93
+ sleep 10
94
+ done
95
+ echo "::warning::Timed out waiting for Codecov to process ${SHA}"
data/CHANGELOG.md CHANGED
@@ -8,6 +8,13 @@ from v1.0.0 onwards. Prior 0.x releases may include breaking changes between min
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [1.7.1] - 2026-10-26
12
+
13
+ ### Fixed
14
+
15
+ - `ractor_safe: true` could hang forever on Ruby 3.x when several threads in the same Ractor called memoized methods (or `reset_all_ractor_memos` and friends) at once. Ruby can lose a wakeup when multiple threads wait in `Ractor.receive_if` on the Ractor's single incoming queue, leaving one caller blocked indefinitely. Each request/reply round trip to the supervisor is now serialized per Ractor; throughput is unchanged since the supervisor already handles requests one at a time.
16
+ - `ractor_safe: true` now works on Ruby 4.0. Ruby 4.0 removed `Ractor.receive_if` and `Ractor#take`, so every call raised `NoMethodError`. On 4.0 each request now gets its own `Ractor::Port` for the supervisor's reply; Ruby 3.x behaviour is unchanged. The `ractor_safe` specs previously skipped themselves on 4.0 and now run there.
17
+
11
18
  ## [1.7.0] - 2026-06-02
12
19
 
13
20
  ### Added
@@ -268,7 +275,18 @@ from v1.0.0 onwards. Prior 0.x releases may include breaking changes between min
268
275
 
269
276
  - Initial release
270
277
 
271
- [Unreleased]: https://github.com/eclectic-coding/safe_memoize/compare/v0.7.0...HEAD
278
+ [Unreleased]: https://github.com/eclectic-coding/safe_memoize/compare/v1.7.1...HEAD
279
+ [1.7.1]: https://github.com/eclectic-coding/safe_memoize/compare/v1.7.0...v1.7.1
280
+ [1.7.0]: https://github.com/eclectic-coding/safe_memoize/compare/v1.6.0...v1.7.0
281
+ [1.6.0]: https://github.com/eclectic-coding/safe_memoize/compare/v1.5.0...v1.6.0
282
+ [1.5.0]: https://github.com/eclectic-coding/safe_memoize/compare/v1.4.0...v1.5.0
283
+ [1.4.0]: https://github.com/eclectic-coding/safe_memoize/compare/v1.3.0...v1.4.0
284
+ [1.3.0]: https://github.com/eclectic-coding/safe_memoize/compare/v1.2.0...v1.3.0
285
+ [1.2.0]: https://github.com/eclectic-coding/safe_memoize/compare/v1.1.0...v1.2.0
286
+ [1.1.0]: https://github.com/eclectic-coding/safe_memoize/compare/v1.0.0...v1.1.0
287
+ [1.0.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.9.0...v1.0.0
288
+ [0.9.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.8.0...v0.9.0
289
+ [0.8.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.7.0...v0.8.0
272
290
  [0.7.0]: https://github.com/eclectic-coding/safe_memoize/compare/v0.6.3...v0.7.0
273
291
  [0.6.3]: https://github.com/eclectic-coding/safe_memoize/compare/v0.6.2...v0.6.3
274
292
  [0.6.2]: https://github.com/eclectic-coding/safe_memoize/compare/v0.6.1...v0.6.2
data/README.md CHANGED
@@ -12,6 +12,57 @@ SafeMemoize is a production-ready, zero-dependency memoization library for Ruby.
12
12
 
13
13
  Beyond the basics, SafeMemoize ships with TTL expiration (including sliding window refresh via `ttl_refresh:`), LRU cache size capping, conditional caching via `if:`/`unless:` predicates, lifecycle hooks for cache hits, evictions, and expirations, per-instance metrics (hit rate, miss rate, average computation time), targeted and bulk cache invalidation, custom cache key generators, and rich introspection helpers (`memoized?`, `memo_count`, `memo_keys`, `memo_values`, `memo_ttl_remaining`). It preserves method visibility (public, protected, and private) and requires no runtime dependencies.
14
14
 
15
+ ## Table of Contents
16
+
17
+ - [The Problem](#the-problem)
18
+ - [How It Works](#how-it-works)
19
+ - [Installation](#installation)
20
+ - [Usage](#usage)
21
+ - [Basic memoization](#basic-memoization)
22
+ - [With arguments](#with-arguments)
23
+ - [Nil and false safety](#nil-and-false-safety)
24
+ - [Works with private methods](#works-with-private-methods)
25
+ - [Cache reset](#cache-reset)
26
+ - [Cache invalidation groups](#cache-invalidation-groups)
27
+ - [Lifecycle hooks](#lifecycle-hooks)
28
+ - [TTL expiration](#ttl-expiration)
29
+ - [Sliding window TTL](#sliding-window-ttl)
30
+ - [LRU cache size limit](#lru-cache-size-limit)
31
+ - [Conditional caching](#conditional-caching)
32
+ - [Cache warm-up and persistence](#cache-warm-up-and-persistence)
33
+ - [Shared cache](#shared-cache)
34
+ - [Fiber-local memoization](#fiber-local-memoization)
35
+ - [Bulk memoization](#bulk-memoization)
36
+ - [Custom cache keys](#custom-cache-keys)
37
+ - [Cache inspection](#cache-inspection)
38
+ - [Cache metrics](#cache-metrics)
39
+ - [Plugin / extension architecture](#plugin--extension-architecture)
40
+ - [Automatic cache busting](#automatic-cache-busting)
41
+ - [Named shared caches](#named-shared-caches)
42
+ - [Cache namespacing](#cache-namespacing)
43
+ - [Global configuration](#global-configuration)
44
+ - [ActiveSupport::Notifications](#activesupportnotifications)
45
+ - [StatsD](#statsd)
46
+ - [OpenTelemetry](#opentelemetry)
47
+ - [Rails request-scope](#rails-request-scope)
48
+ - [Pluggable cache stores](#pluggable-cache-stores)
49
+ - [Deprecation](#deprecation)
50
+ - [Circuit breaker for external stores](#circuit-breaker-for-external-stores)
51
+ - [Multi-level caching](#multi-level-caching)
52
+ - [Stampede protection](#stampede-protection)
53
+ - [Per-class default options](#per-class-default-options-safe_memoize_options)
54
+ - [Copy-on-read](#copy-on-read)
55
+ - [Ractor-safe shared cache](#ractor-safe-shared-cache)
56
+ - [Ractor compatibility](#ractor-compatibility)
57
+ - [Development](#development)
58
+ - [Releasing](#releasing)
59
+ - [Public API and versioning guarantee](#public-api-and-versioning-guarantee)
60
+ - [Ruby version support](#ruby-version-support)
61
+ - [Benchmarks](benchmarks/BENCHMARK.md)
62
+ - [Roadmap](#roadmap)
63
+ - [Contributing](#contributing)
64
+ - [License](#license)
65
+
15
66
  ## The Problem
16
67
 
17
68
  Ruby's common memoization pattern breaks with falsy values:
@@ -28,62 +79,6 @@ SafeMemoize uses `Hash#key?` to distinguish "not yet cached" from "cached nil/fa
28
79
 
29
80
  SafeMemoize uses Ruby's `prepend` mechanism. When you call `memoize :method_name`, it creates an anonymous module with a wrapper method and prepends it onto your class. The wrapper calls `super` to invoke the original method and stores the result in a per-instance hash. Thread safety is achieved with a per-instance `Mutex` using double-check locking.
30
81
 
31
- ## Features
32
-
33
- - [Correctly memoizes `nil` and `false` return values](#nil-and-false-safety)
34
- - [Caches per unique arguments (positional and keyword)](#with-arguments)
35
- - [Thread-safe via double-check locking](#how-it-works)
36
- - [Simple `prepend` + `memoize` API](#usage)
37
- - [Preserves public, protected, and private method visibility](#works-with-private-methods)
38
- - [Supports targeted cache invalidation by argument combination](#cache-reset)
39
- - [Includes a `memoized?` helper for cache inspection](#cache-inspection)
40
- - [Includes a `memo_count` helper for cache size stats](#cache-inspection)
41
- - [Includes a `memo_keys` helper for inspecting cached signatures](#cache-inspection)
42
- - [Includes a `memo_values` helper for inspecting cached signatures and values](#cache-inspection)
43
- - [Optional TTL expiration support for cached entries](#ttl-expiration)
44
- - [Sliding window TTL via `ttl_refresh: true`](#sliding-window-ttl)
45
- - [Optional LRU cache size limit per method via `max_size:`](#lru-cache-size-limit)
46
- - [Conditional caching via `if:` and `unless:` predicates](#conditional-caching)
47
- - [Lifecycle hooks for hit, miss, eviction, and expiration events](#lifecycle-hooks)
48
- - [Per-instance cache metrics (hit rate, miss rate, computation time)](#cache-metrics)
49
- - [Cache warm-up, export, and restore (`warm_memo`, `dump_memo`, `load_memo`)](#cache-warm-up-and-persistence)
50
- - [Class-level shared cache via `shared: true` with optional LRU](#shared-cache)
51
- - [Bulk memoization via `memoize_all` (public, protected, and private)](#bulk-memoization)
52
- - [Custom cache key generation per method](#custom-cache-keys)
53
- - [TTL introspection via `memo_ttl_remaining`](#cache-inspection)
54
- - [Deep single-entry inspection via `memo_inspect`](#cache-inspection)
55
- - [`ArgumentError` at definition time when memoizing an undefined method](#basic-memoization)
56
- - [Hook error isolation — hook exceptions never propagate to callers](#lifecycle-hooks)
57
- - [Deprecation infrastructure for gem authors](#deprecation)
58
- - [Optional `ActiveSupport::Notifications` integration for Rails observability](#activesupportnotifications)
59
- - [Optional StatsD adapter for metrics pipelines](#statsd)
60
- - [Optional OpenTelemetry adapter for distributed tracing](#opentelemetry)
61
- - [Rails request-scope helpers for controllers and service objects](#rails-request-scope)
62
- - [Batch cache warm-up via `memo_preload`](#cache-warm-up-and-persistence)
63
- - [`on_memo_store` hook fires on every cache write](#lifecycle-hooks)
64
- - [Global default TTL and max size via `SafeMemoize.configure`](#global-configuration)
65
- - [`memo_touch` resets the expiry clock without recomputing](#ttl-expiration)
66
- - [`memo_refresh` force-recomputes and re-caches in one call](#cache-inspection)
67
- - [`memo_age` and `memo_stale?` for TTL introspection](#cache-inspection)
68
- - [Class-level `key:` option for shared cache key generation](#custom-cache-keys)
69
- - [`shared_memo_age` and `shared_memo_stale?` for shared cache TTL inspection](#shared-cache)
70
- - [Pluggable external cache stores — Redis, Rails.cache, or any custom adapter](#pluggable-cache-stores)
71
- - [Global default store via `Configuration#default_store`](#pluggable-cache-stores)
72
- - [`SafeMemoize::Adapters::ConcurrentRuby` — optional `concurrent-ruby` store with parallel-read locking](#concurrent-ruby-adapter)
73
- - [Class-level `.safe_memoize_store=` — set a per-class default store without touching global config](#class-level-default-store-safe_memoize_store)
74
- - [Fiber-local memoization via `fiber_local: true` — isolated per-fiber cache, no mutex, works with Async/Falcon](#fiber-local-memoization)
75
- - [Ractor-safe shared cache via `ractor_safe: true` — supervisor Ractor replaces the Mutex; worker Ractors can call the memoized method directly](#ractor-safe-shared-cache)
76
- - [Cache namespacing — per-method `namespace:`, class-level `.safe_memoize_namespace=`, and global `Configuration#namespace` for multi-tenant and versioned deployments](#cache-namespacing)
77
- - [Named shared caches via `shared_cache: "name"` — cross-class cache sharing backed by a globally-registered store](#named-shared-caches)
78
- - [Automatic cache busting via `cache_bust:` — version-token-based invalidation; works with ActiveRecord `updated_at` and any comparable value](#automatic-cache-busting)
79
- - [Plugin / extension architecture — `SafeMemoize::Extension` DSL for adding custom `memoize` options and global lifecycle handlers without monkey-patching](#plugin--extension-architecture)
80
- - [Per-class default options via `safe_memoize_options` — set TTL, max size, copy-on-read, and other defaults for every `memoize` call on the class without repeating them](#per-class-default-options-safe_memoize_options)
81
- - [Copy-on-read via `copy_on_read: true` — returns a `dup`/`deep_dup` on every cache read to protect shared cached state from caller mutation](#copy-on-read)
82
- - [Cache invalidation groups via `group:` — tag related methods with a group name and bust them all with a single `reset_memo_group` call](#cache-invalidation-groups)
83
- - [Circuit breaker for external stores — `Stores::CircuitBreaker` wraps any store adapter and falls back to the per-instance cache when the store is down; configurable error threshold and probe interval](#circuit-breaker-for-external-stores)
84
- - [Multi-level (L1/L2) caching — `Stores::Multilevel` or `store: [l1, l2]` shorthand; reads from the fastest layer first and promotes on miss](#multi-level-caching)
85
- - [Stampede protection — `stampede_protection:` option applies the XFetch algorithm to the per-instance cache; `Stores::XFetch` applies it to external stores](#stampede-protection)
86
-
87
82
  ## Installation
88
83
 
89
84
  Add to your Gemfile:
@@ -122,7 +117,7 @@ end
122
117
 
123
118
  Calling `memoize` on a method name that does not exist raises `ArgumentError` immediately at class definition time rather than at the first runtime call.
124
119
 
125
- [↑ Back to features](#features)
120
+ [↑ Back to top](#table-of-contents)
126
121
 
127
122
  ### With arguments
128
123
 
@@ -147,7 +142,7 @@ calc.compute(3, 4) # computes and caches (different args)
147
142
 
148
143
  Argument arrays, hashes, and strings are deep-frozen into an independent copy when the cache key is built, so mutating arguments after a call cannot corrupt or miss a cached entry.
149
144
 
150
- [↑ Back to features](#features)
145
+ [↑ Back to top](#table-of-contents)
151
146
 
152
147
  ### Nil and false safety
153
148
 
@@ -163,7 +158,7 @@ class Config
163
158
  end
164
159
  ```
165
160
 
166
- [↑ Back to features](#features)
161
+ [↑ Back to top](#table-of-contents)
167
162
 
168
163
  ### Works with private methods
169
164
 
@@ -184,7 +179,7 @@ class TokenProvider
184
179
  end
185
180
  ```
186
181
 
187
- [↑ Back to features](#features)
182
+ [↑ Back to top](#table-of-contents)
188
183
 
189
184
  ### Cache reset
190
185
 
@@ -196,7 +191,7 @@ obj.reset_memo(:search, "ruby", page: 2) # Clears one positional/keyword c
196
191
  obj.reset_all_memos # Clears all memoized values
197
192
  ```
198
193
 
199
- [↑ Back to features](#features)
194
+ [↑ Back to top](#table-of-contents)
200
195
 
201
196
  ### Cache invalidation groups
202
197
 
@@ -269,7 +264,7 @@ end
269
264
 
270
265
  A method belongs to at most one group at a time; re-memoizing with a different `group:` moves it.
271
266
 
272
- [↑ Back to features](#features)
267
+ [↑ Back to top](#table-of-contents)
273
268
 
274
269
  ### Lifecycle hooks
275
270
 
@@ -347,7 +342,7 @@ end
347
342
 
348
343
  Set `c.on_hook_error = :raise` to re-raise exceptions instead of swallowing them.
349
344
 
350
- [↑ Back to features](#features)
345
+ [↑ Back to top](#table-of-contents)
351
346
 
352
347
  ### TTL expiration
353
348
 
@@ -379,7 +374,7 @@ obj.memo_refresh(:current_quote) # Recomputes and re-caches
379
374
  obj.memo_refresh(:find, 42) # Recomputes for one argument combination
380
375
  ```
381
376
 
382
- [↑ Back to features](#features)
377
+ [↑ Back to top](#table-of-contents)
383
378
 
384
379
  ### Sliding window TTL
385
380
 
@@ -398,7 +393,7 @@ end
398
393
 
399
394
  Without `ttl_refresh:`, the entry expires 300 seconds after it was first cached. With it, the clock resets on every read — the entry is evicted only if the method goes 300 seconds without being called. `ttl_refresh: true` requires `ttl:` to be set and works with both per-instance and `shared: true` memoization.
400
395
 
401
- [↑ Back to features](#features)
396
+ [↑ Back to top](#table-of-contents)
402
397
 
403
398
  ### LRU cache size limit
404
399
 
@@ -433,7 +428,7 @@ memoize :find, max_size: 50, ttl: 300
433
428
 
434
429
  The `on_evict` hook fires for LRU-evicted entries the same way it does for manual `reset_memo` calls.
435
430
 
436
- [↑ Back to features](#features)
431
+ [↑ Back to top](#table-of-contents)
437
432
 
438
433
  ### Conditional caching
439
434
 
@@ -469,7 +464,7 @@ Both options accept any callable and compose with `ttl:` and `max_size:`:
469
464
  memoize :find, if: ->(result) { !result.nil? }, ttl: 60, max_size: 500
470
465
  ```
471
466
 
472
- [↑ Back to features](#features)
467
+ [↑ Back to top](#table-of-contents)
473
468
 
474
469
  ### Cache warm-up and persistence
475
470
 
@@ -535,7 +530,7 @@ obj.load_memo(Marshal.load(raw)) if raw
535
530
 
536
531
  Loaded entries have no TTL — they persist until explicitly reset. Expired entries are excluded from `dump_memo` output, so snapshots never contain stale data.
537
532
 
538
- [↑ Back to features](#features)
533
+ [↑ Back to top](#table-of-contents)
539
534
 
540
535
  ### Shared cache
541
536
 
@@ -585,7 +580,7 @@ memoize :find, shared: true, max_size: 500
585
580
 
586
581
  Hooks (`on_memo_hit`, `on_memo_miss`, `on_memo_expire`, `on_memo_evict`) fire on the calling instance as usual.
587
582
 
588
- [↑ Back to features](#features)
583
+ [↑ Back to top](#table-of-contents)
589
584
 
590
585
  ### Fiber-local memoization
591
586
 
@@ -627,7 +622,7 @@ obj.reset_all_fiber_memos # clear all fiber-local entries for thi
627
622
 
628
623
  Lifecycle hooks and cache metrics work the same as for regular memoization. The existing `memoized?`, `reset_memo`, and `memo_count` methods operate on the instance-variable cache; use the `fiber_local_*` / `reset_fiber_*` API for fiber-local entries.
629
624
 
630
- [↑ Back to features](#features)
625
+ [↑ Back to top](#table-of-contents)
631
626
 
632
627
  ### Bulk memoization
633
628
 
@@ -685,7 +680,7 @@ memoize_all include_protected: true, include_private: true
685
680
 
686
681
  Inherited methods are never affected regardless of visibility.
687
682
 
688
- [↑ Back to features](#features)
683
+ [↑ Back to top](#table-of-contents)
689
684
 
690
685
  ### Custom cache keys
691
686
 
@@ -734,7 +729,7 @@ svc.clear_custom_keys(:generate) # Remove generator for one method
734
729
  svc.clear_custom_keys # Remove all custom key generators
735
730
  ```
736
731
 
737
- [↑ Back to features](#features)
732
+ [↑ Back to top](#table-of-contents)
738
733
 
739
734
  ### Cache inspection
740
735
 
@@ -780,7 +775,7 @@ obj.memo_inspect(:find, 42)
780
775
 
781
776
  Returns `nil` when the entry is not cached.
782
777
 
783
- [↑ Back to features](#features)
778
+ [↑ Back to top](#table-of-contents)
784
779
 
785
780
  ### Cache metrics
786
781
 
@@ -810,7 +805,7 @@ obj.cache_metrics_reset(:find) # Clears metrics for one method only
810
805
 
811
806
  Metrics are per-instance and reset independently from the cache itself — clearing metrics does not evict cached values.
812
807
 
813
- [↑ Back to features](#features)
808
+ [↑ Back to top](#table-of-contents)
814
809
 
815
810
  ### Plugin / extension architecture
816
811
 
@@ -893,7 +888,7 @@ An extension does not need to `extend SafeMemoize::Extension`. Any object respon
893
888
  - Unknown `memoize` keywords raise `ArgumentError` unless a registered extension claims them — typos are still caught.
894
889
  - `on_cache_event` handlers run on the main Ractor only; they are silently skipped from worker Ractors.
895
890
 
896
- [↑ Back to features](#features)
891
+ [↑ Back to top](#table-of-contents)
897
892
 
898
893
  ### Automatic cache busting
899
894
 
@@ -954,7 +949,7 @@ obj.reset_memo(:summary) # clears ALL versions
954
949
  - Incompatible with `key:` — both define the cache key shape; raises `ArgumentError` at `memoize` time.
955
950
  - Composes with `namespace:`, `ttl:`, `if:`, `unless:`, and `shared_cache:`.
956
951
 
957
- [↑ Back to features](#features)
952
+ [↑ Back to top](#table-of-contents)
958
953
 
959
954
  ### Named shared caches
960
955
 
@@ -1025,7 +1020,7 @@ memoize :find, shared_cache: "orders", namespace: "presenter" # [:"presenter:fi
1025
1020
  - `register_shared_cache` must be called before the class that uses the name is defined.
1026
1021
  - Test suites should call `SafeMemoize.reset_shared_caches!` in an `after` hook to prevent state leaking between examples.
1027
1022
 
1028
- [↑ Back to features](#features)
1023
+ [↑ Back to top](#table-of-contents)
1029
1024
 
1030
1025
  ### Cache namespacing
1031
1026
 
@@ -1086,7 +1081,7 @@ end
1086
1081
  - Namespacing works with all memoize paths (standard, `store:`, `fiber_local:`, `shared:`, `ractor_safe:`).
1087
1082
  - Adding or changing a namespace changes the cache keys, so existing entries become unreachable (they expire naturally or can be cleared by `reset_all_memos`).
1088
1083
 
1089
- [↑ Back to features](#features)
1084
+ [↑ Back to top](#table-of-contents)
1090
1085
 
1091
1086
  ### Global configuration
1092
1087
 
@@ -1107,7 +1102,7 @@ SafeMemoize.reset_configuration!
1107
1102
 
1108
1103
  The configure block also accepts `on_hook_error`, `on_deprecation`, `active_support_notifications`, `statsd_client`, `default_store`, and `namespace` (covered in [Hook error isolation](#hook-error-isolation), [Deprecation](#deprecation), [ActiveSupport::Notifications](#activesupportnotifications), [StatsD](#statsd), [Pluggable cache stores](#pluggable-cache-stores), and [Cache namespacing](#cache-namespacing)).
1109
1104
 
1110
- [↑ Back to features](#features)
1105
+ [↑ Back to top](#table-of-contents)
1111
1106
 
1112
1107
  ### ActiveSupport::Notifications
1113
1108
 
@@ -1149,7 +1144,7 @@ end
1149
1144
 
1150
1145
  The integration is a no-op when ActiveSupport is not loaded — there is no overhead for non-Rails projects. `active_support_notifications` defaults to `false`.
1151
1146
 
1152
- [↑ Back to features](#features)
1147
+ [↑ Back to top](#table-of-contents)
1153
1148
 
1154
1149
  ### StatsD
1155
1150
 
@@ -1175,7 +1170,7 @@ Each call includes two tags: `method:method_name` and `class:ClassName`. The cli
1175
1170
 
1176
1171
  If the client raises, the error is rescued and a warning is emitted to stderr rather than propagated to the caller. `statsd_client` defaults to `nil` (disabled).
1177
1172
 
1178
- [↑ Back to features](#features)
1173
+ [↑ Back to top](#table-of-contents)
1179
1174
 
1180
1175
  ### OpenTelemetry
1181
1176
 
@@ -1200,7 +1195,7 @@ SafeMemoize then wraps every cache miss (the actual method call, not cache hits)
1200
1195
 
1201
1196
  Cache hits produce no spans, so tracing overhead is zero for cached calls. The adapter is compatible with any tracer that responds to `in_span(name, attributes:, &block)` — the interface provided by `opentelemetry-sdk`, `opentelemetry-api`, and no-op tracers alike. If `opentelemetry_tracer` is `nil` (the default), the adapter is completely bypassed.
1202
1197
 
1203
- [↑ Back to features](#features)
1198
+ [↑ Back to top](#table-of-contents)
1204
1199
 
1205
1200
  ### Rails request-scope
1206
1201
 
@@ -1269,7 +1264,7 @@ end
1269
1264
 
1270
1265
  `SafeMemoize::Rails::Middleware` calls `reset_all_memos` on every tracked instance in the current thread at the end of the request, even if the app raises. Tracking is thread-local, so concurrent requests never interfere. The tracked list is cleared automatically after each reset.
1271
1266
 
1272
- [↑ Back to features](#features)
1267
+ [↑ Back to top](#table-of-contents)
1273
1268
 
1274
1269
  ### Pluggable cache stores
1275
1270
 
@@ -1418,7 +1413,7 @@ A per-method `store:` option always takes precedence. Methods using `max_size:`
1418
1413
 
1419
1414
  The `store:` option composes with `ttl:`, `ttl_refresh:`, `if:`, `unless:`, lifecycle hooks, and cache metrics. It is incompatible with `max_size:` (use the store adapter's own eviction) and `shared:` (raise `ArgumentError` if combined).
1420
1415
 
1421
- [↑ Back to features](#features)
1416
+ [↑ Back to top](#table-of-contents)
1422
1417
 
1423
1418
  ### Deprecation
1424
1419
 
@@ -1449,7 +1444,7 @@ SafeMemoize.configure do |c|
1449
1444
  end
1450
1445
  ```
1451
1446
 
1452
- [↑ Back to features](#features)
1447
+ [↑ Back to top](#table-of-contents)
1453
1448
 
1454
1449
  ## Circuit breaker for external stores
1455
1450
 
@@ -1533,7 +1528,7 @@ class ApiService
1533
1528
  end
1534
1529
  ```
1535
1530
 
1536
- [↑ Back to features](#features)
1531
+ [↑ Back to top](#table-of-contents)
1537
1532
 
1538
1533
  ## Multi-level caching
1539
1534
 
@@ -1572,7 +1567,7 @@ store = SafeMemoize::Stores::Multilevel.new(SafeMemoize::Stores::Memory.new, s
1572
1567
  memoize :catalog, store: store, ttl: 300
1573
1568
  ```
1574
1569
 
1575
- [↑ Back to features](#features)
1570
+ [↑ Back to top](#table-of-contents)
1576
1571
 
1577
1572
  ## Stampede protection
1578
1573
 
@@ -1629,7 +1624,7 @@ store = SafeMemoize::Stores::XFetch.new(
1629
1624
  )
1630
1625
  ```
1631
1626
 
1632
- [↑ Back to features](#features)
1627
+ [↑ Back to top](#table-of-contents)
1633
1628
 
1634
1629
  ## Per-class default options (`safe_memoize_options`)
1635
1630
 
@@ -1668,7 +1663,7 @@ Call with no arguments to clear all class-level defaults:
1668
1663
  MyClass.safe_memoize_options # clears — subsequent memoize calls use global config or per-call options only
1669
1664
  ```
1670
1665
 
1671
- [↑ Back to features](#features)
1666
+ [↑ Back to top](#table-of-contents)
1672
1667
 
1673
1668
  ## Copy-on-read
1674
1669
 
@@ -1706,7 +1701,7 @@ class ReportService
1706
1701
  end
1707
1702
  ```
1708
1703
 
1709
- [↑ Back to features](#features)
1704
+ [↑ Back to top](#table-of-contents)
1710
1705
 
1711
1706
  ## Ractor-safe shared cache
1712
1707
 
@@ -1727,7 +1722,7 @@ end
1727
1722
  20.times.map { Thread.new { PriceService.new.fetch_price(42) } }.map(&:value)
1728
1723
 
1729
1724
  # Worker Ractors also read from and write to the same supervisor cache
1730
- result = Ractor.new(PriceService) { |s| s.new.fetch_price(42) }.take
1725
+ result = Ractor.new(PriceService) { |s| s.new.fetch_price(42) }.value # .take on Ruby 3.x
1731
1726
  ```
1732
1727
 
1733
1728
  ### How it works
@@ -1735,7 +1730,7 @@ result = Ractor.new(PriceService) { |s| s.new.fetch_price(42) }.take
1735
1730
  - A **supervisor `Ractor`** is created once per class the first time a `ractor_safe: true` method is memoized. It owns a plain Ruby `Hash` and responds to `:fetch`, `:store`, `:delete_all`, `:delete_one`, `:clear`, `:memoized`, and `:count` messages.
1736
1731
  - The memoize **wrapper Proc** is frozen via `Ractor.make_shareable` before being registered with `define_method`, so the class can be passed directly into `Ractor.new` blocks.
1737
1732
  - Cached values are deep-frozen via `Ractor.make_shareable`. Values that cannot be made shareable (e.g. a `Mutex`) raise `ArgumentError`.
1738
- - **Thread safety** inside the main Ractor (multiple threads) is handled by per-call tags (`Thread.current.object_id`) combined with `Ractor.receive_if`, so concurrent threads never consume each other's replies.
1733
+ - **Thread safety** inside the main Ractor (multiple threads): on Ruby 4.0+ each call replies through its own `Ractor::Port`, so callers never share a queue. On Ruby 3.x, replies are matched by per-call tags (`Thread.current.object_id`) with `Ractor.receive_if`, and each send/receive pair is serialized per Ractor because concurrent `receive_if` waiters can otherwise lose a wakeup and hang.
1739
1734
  - `ttl:` is supported. Expired entries are skipped by the supervisor's `:fetch` handler.
1740
1735
 
1741
1736
  ### Constraints
@@ -1761,7 +1756,7 @@ PriceService.reset_ractor_memo(:fetch_price) # → clear all entries for
1761
1756
  PriceService.reset_all_ractor_memos # → clear entire shared cache
1762
1757
  ```
1763
1758
 
1764
- [↑ Back to features](#features)
1759
+ [↑ Back to top](#table-of-contents)
1765
1760
 
1766
1761
  ## Ractor compatibility
1767
1762
 
@@ -1779,7 +1774,7 @@ Regular `memoize` (without `ractor_safe: true`) is **not Ractor-compatible**. Pa
1779
1774
 
1780
1775
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt.
1781
1776
 
1782
- To run the benchmark suite: `bundle exec ruby benchmarks/benchmark.rb`.
1777
+ To run the benchmark suite: `bundle exec ruby benchmarks/benchmark.rb`. Pre-recorded results and analysis are in [benchmarks/BENCHMARK.md](benchmarks/BENCHMARK.md).
1783
1778
 
1784
1779
  To generate API documentation locally: `bundle exec rake doc`. Output is written to `doc/` (gitignored). The online reference is published automatically to [RubyDoc.info](https://rubydoc.info/gems/safe_memoize) on every release. Install `memery` and `memo_wise` first if you want comparison columns against those gems.
1785
1780
 
@@ -0,0 +1,114 @@
1
+ # SafeMemoize Benchmark Results
2
+
3
+ Run with `bundle exec ruby benchmarks/benchmark.rb`.
4
+
5
+ **Environment:** Ruby 3.4.8 (arm64-darwin25, PRISM) · `benchmark-ips` 5 s measurement / 2 s warmup
6
+
7
+ ---
8
+
9
+ ## What is being measured
10
+
11
+ SafeMemoize is compared against two vanilla Ruby baselines:
12
+
13
+ | Label | Pattern | Correctness |
14
+ |---|---|---|
15
+ | `raw \|\|= (unsafe)` | `@v \|\|= compute` | Broken — re-runs if result is `nil` or `false` |
16
+ | `raw safe ivar` | `return @v if defined?(@v); @v = compute` | Correct |
17
+ | `safe_memoize` | `prepend SafeMemoize` + `memoize :method` | Correct |
18
+
19
+ The raw patterns are intentionally simple and carry none of SafeMemoize's feature overhead (thread safety, argument keying, TTL, hooks, metrics). The numbers below quantify exactly what that feature set costs.
20
+
21
+ ---
22
+
23
+ ## Results
24
+
25
+ ### 1. Zero-arg cache HIT — steady-state throughput
26
+
27
+ A primed object called repeatedly. This is the hot path.
28
+
29
+ | Implementation | i/s | ns/call | vs fastest |
30
+ |---|---|---|---|
31
+ | `raw safe ivar` | 21.51 M | 46.5 | — |
32
+ | `raw \|\|= (unsafe)` | 16.69 M | 59.9 | 1.29× slower |
33
+ | `safe_memoize` | 296.8 K | 3,370 | **72×** slower |
34
+
35
+ **Takeaway:** SafeMemoize's hit path costs ~3.4 µs per call — the price of a hash lookup, mutex check, and key construction. For methods that are called millions of times per second in a tight loop, raw ivars win. For methods that do real work (DB queries, HTTP, serialization), this overhead is negligible.
36
+
37
+ ---
38
+
39
+ ### 2. Zero-arg cache MISS — first-call overhead
40
+
41
+ A fresh instance is created each iteration so every call is a miss.
42
+
43
+ | Implementation | i/s | µs/call | vs fastest |
44
+ |---|---|---|---|
45
+ | `raw safe ivar` | 8.47 M | 0.12 | — |
46
+ | `safe_memoize` | 189.6 K | 5.28 | **45×** slower |
47
+
48
+ **Takeaway:** The miss path allocates the cache hash, builds the key, and acquires the mutex — ~5 µs. Still fast in absolute terms; any method body doing more than a trivial computation will dominate.
49
+
50
+ ---
51
+
52
+ ### 3. With-argument cache HIT — keyed lookup
53
+
54
+ A primed object called with the same argument each iteration.
55
+
56
+ | Implementation | i/s | ns/call | vs fastest |
57
+ |---|---|---|---|
58
+ | `raw safe ivar` | 14.17 M | 70.6 | — |
59
+ | `safe_memoize` | 289.2 K | 3,460 | **49×** slower |
60
+
61
+ **Takeaway:** The argument hash lookup adds roughly the same overhead as the zero-arg case. Key construction (deep-freeze copy of args) dominates, not the hash lookup itself.
62
+
63
+ ---
64
+
65
+ ### 4. Fast path vs locked path (internal)
66
+
67
+ Both are `safe_memoize`. Adding `max_size:` enables LRU tracking, which holds the mutex for the full read/write cycle.
68
+
69
+ | Configuration | i/s | µs/call | vs fast path |
70
+ |---|---|---|---|
71
+ | No `max_size:` (fast path) | 287.7 K | 3.48 | — |
72
+ | `max_size: 100` (locked path) | 218.0 K | 4.59 | 1.32× slower |
73
+
74
+ **Takeaway:** LRU costs an extra ~1.1 µs per hit. Use it when eviction matters; skip it for unbounded caches.
75
+
76
+ ---
77
+
78
+ ### 5. Shared cache vs instance cache (internal)
79
+
80
+ Both are `safe_memoize`. `shared: true` stores results on the class rather than the instance.
81
+
82
+ | Configuration | i/s | µs/call | difference |
83
+ |---|---|---|---|
84
+ | Instance cache | 292.5 K | 3.42 | — |
85
+ | Shared cache | 275.3 K | 3.63 | within margin of error |
86
+
87
+ **Takeaway:** Class-level shared storage is essentially free compared to per-instance caching. The mutex contention at the class level does not measurably affect single-threaded throughput.
88
+
89
+ ---
90
+
91
+ ### 6. Concurrent throughput — 8 threads × 50,000 iterations
92
+
93
+ | Implementation | Total time | i/s |
94
+ |---|---|---|
95
+ | `raw safe ivar` | 0.033 s | 12.01 M |
96
+ | `safe_memoize` (instance cache) | 1.363 s | 293.4 K |
97
+ | `safe_memoize` (shared cache) | 1.393 s | 287.1 K |
98
+
99
+ **Takeaway:** Under real thread contention the raw ivar baseline has no locking at all — it is technically unsafe here. SafeMemoize's per-instance mutex serialises writes correctly at ~293 K i/s; the class-level shared mutex performs identically at ~287 K i/s.
100
+
101
+ ---
102
+
103
+ ## Summary
104
+
105
+ | Scenario | SafeMemoize overhead |
106
+ |---|---|
107
+ | Hot cache hit (zero-arg) | ~3.4 µs — 72× vs raw ivar |
108
+ | Hot cache hit (with args) | ~3.5 µs — 49× vs raw ivar |
109
+ | First call / cache miss | ~5.3 µs — 45× vs raw ivar |
110
+ | LRU enabled (`max_size:`) | +1.1 µs vs fast path |
111
+ | Shared vs instance cache | no measurable difference |
112
+ | Concurrent throughput | ~293 K i/s (thread-safe) |
113
+
114
+ SafeMemoize is slower than a raw instance variable in absolute terms, but the overhead is measured in **microseconds**, not milliseconds. Any method that does I/O, calls a database, hits an external API, or performs non-trivial computation will completely dominate the memoization overhead — and that is precisely the use case SafeMemoize is designed for.
data/benchmarks/README.md CHANGED
@@ -65,4 +65,6 @@ bundle exec ruby benchmarks/benchmark.rb
65
65
  safe_memoize (shared) ~323 K i/s
66
66
  ```
67
67
 
68
- Results vary by hardware, Ruby version, and GVL scheduling. Run on your own hardware for authoritative numbers.
68
+ Results vary by hardware, Ruby version, and GVL scheduling. Run on your own hardware for authoritative numbers.
69
+
70
+ For a full recorded run with analysis, see [BENCHMARK.md](BENCHMARK.md).
@@ -958,9 +958,7 @@ module SafeMemoize
958
958
 
959
959
  cache_key = Ractor.make_shareable([method_name, deep_freeze_copy(args), deep_freeze_copy(kwargs)])
960
960
 
961
- tag = Thread.current.object_id
962
- supervisor.send(Ractor.make_shareable([Ractor.current, tag, :fetch, cache_key]))
963
- response = Ractor.receive_if { |m| m.is_a?(Array) && m[0] == tag }[1]
961
+ response = RactorSharedMethods.round_trip(supervisor, :fetch, cache_key)
964
962
 
965
963
  if response[:hit]
966
964
  record_cache_hit(cache_key)
@@ -985,8 +983,7 @@ module SafeMemoize
985
983
  cached_at: now
986
984
  })
987
985
 
988
- supervisor.send(Ractor.make_shareable([Ractor.current, tag, :store, cache_key, record]))
989
- stored = Ractor.receive_if { |m| m.is_a?(Array) && m[0] == tag }[1]
986
+ stored = RactorSharedMethods.round_trip(supervisor, :store, cache_key, record)
990
987
  stored_record = stored[:stored]
991
988
 
992
989
  record_cache_miss(cache_key, elapsed_time)
@@ -15,9 +15,65 @@ module SafeMemoize
15
15
  # - +if:+, +unless:+, +max_size:+, +ttl_refresh:+, +key:+, and +store:+ are
16
16
  # incompatible and raise +ArgumentError+ at +memoize+ time.
17
17
  # - When calling a ractor-safe memoized method from the main Ractor with multiple
18
- # threads, responses are matched by thread identity so concurrent callers do not
19
- # consume each other's replies.
18
+ # threads, responses are matched by thread identity and each send/receive pair
19
+ # is serialized per Ractor, so concurrent callers do not consume each other's
20
+ # replies or lose a wakeup.
20
21
  module RactorSharedMethods
22
+ MUTEX_KEY = :__safe_memo_ractor_mutex__
23
+
24
+ # Ruby 4.0 replaced +Ractor.receive_if+ with +Ractor::Port+.
25
+ PORTS = defined?(Ractor::Port) ? true : false
26
+
27
+ # Sends a request to +supervisor+ and blocks until its reply arrives,
28
+ # returning the reply payload.
29
+ def self.round_trip(supervisor, op, *args)
30
+ # :nocov:
31
+ return round_trip_via_port(supervisor, op, *args) if PORTS
32
+ # :nocov:
33
+
34
+ round_trip_via_queue(supervisor, op, *args)
35
+ end
36
+
37
+ # Ruby 4.0+: each call gets its own port, so callers never share a queue.
38
+ # :nocov:
39
+ def self.round_trip_via_port(supervisor, op, *args)
40
+ port = Ractor::Port.new
41
+ supervisor.send(Ractor.make_shareable([port, Thread.current.object_id, op, *args]))
42
+ port.receive[1]
43
+ ensure
44
+ port&.close
45
+ end
46
+ # :nocov:
47
+
48
+ # Ruby 3.x: replies are matched by thread identity, but every thread in a
49
+ # Ractor waits on that Ractor's single incoming queue. Ruby 3.x can lose a
50
+ # wakeup when several threads sit in +Ractor.receive_if+ at once, leaving one
51
+ # waiting forever, so the send/receive pair is serialized per Ractor. The
52
+ # supervisor handles requests one at a time anyway, so this costs no
53
+ # throughput.
54
+ def self.round_trip_via_queue(supervisor, op, *args)
55
+ tag = Thread.current.object_id
56
+ msg = Ractor.make_shareable([Ractor.current, tag, op, *args])
57
+
58
+ ractor_mutex.synchronize do
59
+ supervisor.send(msg)
60
+ Ractor.receive_if { |m| m.is_a?(Array) && m[0] == tag }[1]
61
+ end
62
+ end
63
+
64
+ # Per-Ractor mutex held in Ractor-local storage (a plain +Mutex+ constant is
65
+ # not Ractor-shareable).
66
+ def self.ractor_mutex
67
+ Ractor.current[MUTEX_KEY] ||
68
+ if Ractor.respond_to?(:store_if_absent)
69
+ Ractor.store_if_absent(MUTEX_KEY) { Mutex.new }
70
+ else
71
+ # :nocov:
72
+ Ractor.current[MUTEX_KEY] ||= Mutex.new # Ruby 3.3 lacks store_if_absent
73
+ # :nocov:
74
+ end
75
+ end
76
+
21
77
  # Clears one or all entries from the Ractor-safe shared cache.
22
78
  #
23
79
  # @param method_name [Symbol, String]
@@ -76,13 +132,8 @@ module SafeMemoize
76
132
  private
77
133
 
78
134
  # Sends a message to the supervisor and blocks until the tagged response arrives.
79
- # Uses Thread.current.object_id as a per-call tag so concurrent threads in the
80
- # main Ractor do not steal each other's replies.
81
135
  def __ractor_cache_send__(supervisor, op, *args)
82
- tag = Thread.current.object_id
83
- msg = Ractor.make_shareable([Ractor.current, tag, op, *args])
84
- supervisor.send(msg)
85
- Ractor.receive_if { |m| m.is_a?(Array) && m[0] == tag }[1]
136
+ RactorSharedMethods.round_trip(supervisor, op, *args)
86
137
  end
87
138
 
88
139
  # Creates the supervisor Ractor that owns this class's Ractor-safe shared cache.
@@ -10,6 +10,7 @@ module SafeMemoize
10
10
  VERSION_PATTERN = /VERSION = "[^"]+"/
11
11
  SEMVER_PATTERN = /\A\d+\.\d+\.\d+(?:[-.][0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?\z/
12
12
  UNRELEASED_HEADING = "## [Unreleased]"
13
+ UNRELEASED_LINK = %r{^\[Unreleased\]: (?<base>\S+)/compare/v(?<previous>[^.\s]\S*?)\.\.\.HEAD$}
13
14
 
14
15
  def normalize_version(version)
15
16
  normalized_version = version.to_s.sub(/\Av/, "")
@@ -43,7 +44,24 @@ module SafeMemoize
43
44
  raise ArgumentError, "CHANGELOG.md already contains #{normalized_version}"
44
45
  end
45
46
 
46
- contents.sub(UNRELEASED_HEADING, "#{UNRELEASED_HEADING}\n\n#{release_heading}")
47
+ contents = contents.sub(UNRELEASED_HEADING, "#{UNRELEASED_HEADING}\n\n#{release_heading}")
48
+ update_version_links(contents, normalized_version)
49
+ end
50
+
51
+ # Keeps the reference-style link definitions at the bottom of the CHANGELOG
52
+ # in sync: points [Unreleased] at the new tag and adds a compare link for
53
+ # the new version. Left untouched when no [Unreleased] link is present.
54
+ def update_version_links(contents, version)
55
+ match = contents.match(UNRELEASED_LINK)
56
+ return contents unless match
57
+
58
+ base = match[:base]
59
+ replacement = [
60
+ "[Unreleased]: #{base}/compare/v#{version}...HEAD",
61
+ "[#{version}]: #{base}/compare/v#{match[:previous]}...v#{version}"
62
+ ].join("\n")
63
+
64
+ contents.sub(UNRELEASED_LINK) { replacement }
47
65
  end
48
66
 
49
67
  # Removes milestone sections from ROADMAP.md where every feature row has
@@ -2,5 +2,5 @@
2
2
 
3
3
  module SafeMemoize
4
4
  # The current gem version string.
5
- VERSION = "1.7.0"
5
+ VERSION = "1.7.1"
6
6
  end
data/sig/safe_memoize.rbs CHANGED
@@ -251,6 +251,14 @@ module SafeMemoize
251
251
  end
252
252
 
253
253
  module RactorSharedMethods
254
+ MUTEX_KEY: Symbol
255
+ PORTS: bool
256
+
257
+ def self.round_trip: (untyped supervisor, Symbol op, *untyped args) -> untyped
258
+ def self.round_trip_via_port: (untyped supervisor, Symbol op, *untyped args) -> untyped
259
+ def self.round_trip_via_queue: (untyped supervisor, Symbol op, *untyped args) -> untyped
260
+ def self.ractor_mutex: () -> Thread::Mutex
261
+
254
262
  def reset_ractor_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
255
263
  def reset_all_ractor_memos: () -> void
256
264
  def ractor_memoized?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
metadata CHANGED
@@ -1,28 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe_memoize
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chuck Smith
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: simplecov
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '0.22'
19
- type: :development
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '0.22'
11
+ dependencies: []
26
12
  description: 'SafeMemoize is a production-ready, zero-dependency memoization library
27
13
  for Ruby. It uses Ruby''s prepend mechanism to wrap methods with a thread-safe cache
28
14
  (Mutex + double-check locking) that correctly handles nil and false return values
@@ -49,6 +35,7 @@ files:
49
35
  - ROADMAP.md
50
36
  - Rakefile
51
37
  - UPGRADING.md
38
+ - benchmarks/BENCHMARK.md
52
39
  - benchmarks/README.md
53
40
  - benchmarks/benchmark.rb
54
41
  - codecov.yml