safe_memoize 1.4.0 → 1.6.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 +24 -0
- data/README.md +178 -1
- data/ROADMAP.md +0 -20
- data/lib/safe_memoize/class_methods.rb +77 -1
- data/lib/safe_memoize/public_methods.rb +31 -0
- data/lib/safe_memoize/stores/circuit_breaker.rb +178 -0
- data/lib/safe_memoize/version.rb +1 -1
- data/lib/safe_memoize.rb +1 -0
- data/sig/safe_memoize.rbs +36 -1
- 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: 1920c31de4a855b818c5a4f37f35260f38489b7ff6ffe221b43d91e862133bde
|
|
4
|
+
data.tar.gz: 948979dfc49f4e42b16d21926344869fc06a39da3252ab1033697a5748a7362a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 415c4095ce702b5823a887858f342fdf5742887394dc2cc113ccf711ef1f5789430e036d69ee3660d663a2153f34375f3622f1ff92d73143072e2376d2ae087b
|
|
7
|
+
data.tar.gz: 1fed071b0feb6591d4b95c69604f4c14440c88cefbdb13ce9f2e50a2eba5fad4723b28c1584f2cb7aaa33f7ba4e2140b5a16c45b99ba0cd9b13f47b2a16b0d58
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,30 @@ from v1.0.0 onwards. Prior 0.x releases may include breaking changes between min
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.6.0] - 2026-06-02
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `SafeMemoize::Stores::CircuitBreaker` — a `Stores::Base` wrapper that protects any external store adapter with a three-state circuit breaker (`:closed` → `:open` → `:half_open`). When the wrapped store raises on `read` or `write`, the error is swallowed: reads return `MISS` (triggering the per-instance in-process fallback cache) and writes are no-ops (the return value is unaffected). After a configurable number of consecutive failures (`error_threshold:`, default 5) the circuit opens and all store calls are bypassed until a probe interval elapses (`probe_interval:`, default 30 s), at which point a single probe request is let through; success closes the circuit, failure resets the timer. Any successful call in `:closed` state resets the consecutive error counter, so transient blips do not accumulate toward the threshold.
|
|
16
|
+
- `state` — returns `:closed`, `:open`, or `:half_open`
|
|
17
|
+
- `open?` — `true` when the circuit is not fully closed
|
|
18
|
+
- `error_count` — current consecutive error count
|
|
19
|
+
- `reset!` — manually close the circuit and clear the counter
|
|
20
|
+
- `wrapped_store`, `error_threshold`, `probe_interval` — readers
|
|
21
|
+
- `circuit_breaker:` option on `memoize` — syntactic sugar that auto-wraps the configured `store:` adapter in a `CircuitBreaker`. Pass `true` to use defaults, or a `Hash` with `:error_threshold` and/or `:probe_interval` keys to customise. Raises `ArgumentError` if no store is configured. Does not double-wrap a store that is already a `CircuitBreaker`. Accepted by `safe_memoize_options` as a class-wide default.
|
|
22
|
+
|
|
23
|
+
## [1.5.0] - 2026-06-02
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- `group:` option on `memoize` — assigns a method to a named invalidation group (`memoize :find, group: :database`). Groups are stored on the class and survive re-memoization; a method can belong to at most one group at a time (re-memoizing with a different group moves it). Accepts any non-empty Symbol or String. Can be set as a class default via `safe_memoize_options group: :my_group`.
|
|
28
|
+
- `reset_memo_group(group_name)` instance method — clears all per-instance cached entries for every method in the named group in a single call; each evicted entry fires the `:on_evict` hook. A no-op for unknown groups.
|
|
29
|
+
- `reset_shared_memo_group(group_name)` class method — the shared-cache equivalent of `reset_memo_group`; clears all shared-cache entries for every method in the group that was memoized with `shared: true`.
|
|
30
|
+
- `memo_group_methods(group_name)` instance method — returns the array of method names belonging to the given group on the instance's class (empty array for unknown groups).
|
|
31
|
+
- `memo_groups` instance method — returns all group names registered on the instance's class.
|
|
32
|
+
- `safe_memo_group_methods(group_name)` class method — class-level equivalent of `memo_group_methods`.
|
|
33
|
+
- `safe_memo_groups` class method — class-level equivalent of `memo_groups`.
|
|
34
|
+
|
|
11
35
|
## [1.4.0] - 2026-06-02
|
|
12
36
|
|
|
13
37
|
### Added
|
data/README.md
CHANGED
|
@@ -79,6 +79,8 @@ SafeMemoize uses Ruby's `prepend` mechanism. When you call `memoize :method_name
|
|
|
79
79
|
- [Plugin / extension architecture — `SafeMemoize::Extension` DSL for adding custom `memoize` options and global lifecycle handlers without monkey-patching](#plugin--extension-architecture)
|
|
80
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
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)
|
|
82
84
|
|
|
83
85
|
## Installation
|
|
84
86
|
|
|
@@ -194,6 +196,79 @@ obj.reset_all_memos # Clears all memoized values
|
|
|
194
196
|
|
|
195
197
|
[↑ Back to features](#features)
|
|
196
198
|
|
|
199
|
+
### Cache invalidation groups
|
|
200
|
+
|
|
201
|
+
Tag related methods with `group:` and bust them all at once with a single `reset_memo_group` call:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
class RepoService
|
|
205
|
+
prepend SafeMemoize
|
|
206
|
+
|
|
207
|
+
def find_user(id) = db.query("SELECT * FROM users WHERE id=?", id)
|
|
208
|
+
def find_post(id) = db.query("SELECT * FROM posts WHERE id=?", id)
|
|
209
|
+
def site_config = db.query("SELECT * FROM config LIMIT 1")
|
|
210
|
+
|
|
211
|
+
memoize :find_user, group: :database
|
|
212
|
+
memoize :find_post, group: :database
|
|
213
|
+
memoize :site_config # no group — unaffected by group reset
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
svc = RepoService.new
|
|
217
|
+
svc.find_user(1)
|
|
218
|
+
svc.find_post(42)
|
|
219
|
+
svc.site_config
|
|
220
|
+
|
|
221
|
+
svc.reset_memo_group(:database) # invalidates find_user and find_post only
|
|
222
|
+
svc.memoized?(:site_config) # => true — unaffected
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
For `shared: true` methods, use the class method:
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class CatalogService
|
|
229
|
+
prepend SafeMemoize
|
|
230
|
+
|
|
231
|
+
def products = fetch_all_products
|
|
232
|
+
def categories = fetch_all_categories
|
|
233
|
+
|
|
234
|
+
memoize :products, shared: true, group: :catalog
|
|
235
|
+
memoize :categories, shared: true, group: :catalog
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
CatalogService.reset_shared_memo_group(:catalog) # clears shared cache for both methods
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Introspection
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
svc.memo_groups # => [:database] — all groups on the class
|
|
245
|
+
svc.memo_group_methods(:database) # => [:find_user, :find_post]
|
|
246
|
+
CatalogService.safe_memo_groups # => [:catalog]
|
|
247
|
+
CatalogService.safe_memo_group_methods(:catalog) # => [:products, :categories]
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### Class-wide group default
|
|
251
|
+
|
|
252
|
+
Use `safe_memoize_options` to assign all subsequently memoized methods to the same group:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
class ApiClient
|
|
256
|
+
prepend SafeMemoize
|
|
257
|
+
safe_memoize_options group: :api
|
|
258
|
+
|
|
259
|
+
def users = http.get("/users")
|
|
260
|
+
def orders = http.get("/orders")
|
|
261
|
+
|
|
262
|
+
memoize :users # group: :api
|
|
263
|
+
memoize :orders # group: :api
|
|
264
|
+
memoize :health, group: nil # override — no group
|
|
265
|
+
end
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
A method belongs to at most one group at a time; re-memoizing with a different `group:` moves it.
|
|
269
|
+
|
|
270
|
+
[↑ Back to features](#features)
|
|
271
|
+
|
|
197
272
|
### Lifecycle hooks
|
|
198
273
|
|
|
199
274
|
Register callbacks that fire when cached entries are evicted or expire.
|
|
@@ -1374,6 +1449,90 @@ end
|
|
|
1374
1449
|
|
|
1375
1450
|
[↑ Back to features](#features)
|
|
1376
1451
|
|
|
1452
|
+
## Circuit breaker for external stores
|
|
1453
|
+
|
|
1454
|
+
`SafeMemoize::Stores::CircuitBreaker` wraps any `Stores::Base` adapter and silently falls back to the per-instance in-process cache when the external store is unavailable, rather than propagating exceptions to callers.
|
|
1455
|
+
|
|
1456
|
+
### How it works
|
|
1457
|
+
|
|
1458
|
+
The breaker moves through three states:
|
|
1459
|
+
|
|
1460
|
+
| State | Behaviour |
|
|
1461
|
+
|---|---|
|
|
1462
|
+
| `:closed` | Normal — every call passes through to the wrapped store; consecutive errors are counted |
|
|
1463
|
+
| `:open` | Tripped — reads return `MISS` (triggering the per-instance fallback), writes are no-ops; no calls reach the store until the probe interval elapses |
|
|
1464
|
+
| `:half_open` | Probe — calls are let through; the first success closes the circuit; any failure re-opens it and resets the timer |
|
|
1465
|
+
|
|
1466
|
+
Any successful call in `:closed` state resets the consecutive error counter, so transient blips do not accumulate toward the threshold.
|
|
1467
|
+
|
|
1468
|
+
### Usage
|
|
1469
|
+
|
|
1470
|
+
**Direct wrapping:**
|
|
1471
|
+
|
|
1472
|
+
```ruby
|
|
1473
|
+
redis = SafeMemoize::Stores::CircuitBreaker.new(
|
|
1474
|
+
MyRedisStore.new,
|
|
1475
|
+
error_threshold: 5, # trip after 5 consecutive failures (default)
|
|
1476
|
+
probe_interval: 30 # wait 30 s before probing (default)
|
|
1477
|
+
)
|
|
1478
|
+
|
|
1479
|
+
class CatalogService
|
|
1480
|
+
prepend SafeMemoize
|
|
1481
|
+
|
|
1482
|
+
def products = fetch_from_redis
|
|
1483
|
+
memoize :products, store: redis
|
|
1484
|
+
end
|
|
1485
|
+
```
|
|
1486
|
+
|
|
1487
|
+
**Via the `circuit_breaker:` option** (auto-wraps the configured store):
|
|
1488
|
+
|
|
1489
|
+
```ruby
|
|
1490
|
+
class CatalogService
|
|
1491
|
+
prepend SafeMemoize
|
|
1492
|
+
|
|
1493
|
+
def products = fetch_from_redis
|
|
1494
|
+
memoize :products, store: MyRedisStore.new, circuit_breaker: true
|
|
1495
|
+
|
|
1496
|
+
def orders = fetch_orders
|
|
1497
|
+
memoize :orders,
|
|
1498
|
+
store: MyRedisStore.new,
|
|
1499
|
+
circuit_breaker: { error_threshold: 3, probe_interval: 60 }
|
|
1500
|
+
end
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
When the store raises, `products` falls back to the per-instance in-memory hash — callers see no exceptions and computation still runs once per instance until the circuit closes.
|
|
1504
|
+
|
|
1505
|
+
### Introspection and manual control
|
|
1506
|
+
|
|
1507
|
+
```ruby
|
|
1508
|
+
cb = SafeMemoize::Stores::CircuitBreaker.new(store)
|
|
1509
|
+
|
|
1510
|
+
cb.state # => :closed | :open | :half_open
|
|
1511
|
+
cb.open? # => false
|
|
1512
|
+
cb.error_count # => 0
|
|
1513
|
+
cb.error_threshold # => 5
|
|
1514
|
+
cb.probe_interval # => 30.0
|
|
1515
|
+
cb.wrapped_store # => the inner adapter
|
|
1516
|
+
cb.reset! # manually close the circuit and clear error count
|
|
1517
|
+
```
|
|
1518
|
+
|
|
1519
|
+
### Class-wide default
|
|
1520
|
+
|
|
1521
|
+
```ruby
|
|
1522
|
+
class ApiService
|
|
1523
|
+
prepend SafeMemoize
|
|
1524
|
+
safe_memoize_options circuit_breaker: { error_threshold: 3, probe_interval: 15 }
|
|
1525
|
+
|
|
1526
|
+
def users = http.get("/users")
|
|
1527
|
+
def orders = http.get("/orders")
|
|
1528
|
+
|
|
1529
|
+
memoize :users, store: redis
|
|
1530
|
+
memoize :orders, store: redis # both get the circuit breaker
|
|
1531
|
+
end
|
|
1532
|
+
```
|
|
1533
|
+
|
|
1534
|
+
[↑ Back to features](#features)
|
|
1535
|
+
|
|
1377
1536
|
## Per-class default options (`safe_memoize_options`)
|
|
1378
1537
|
|
|
1379
1538
|
`safe_memoize_options` sets option defaults for every `memoize` call on the class, eliminating repetition when many methods share the same TTL, LRU cap, or other option. Per-call options still take precedence; class defaults take precedence over global `SafeMemoize.configure` defaults.
|
|
@@ -1611,6 +1770,8 @@ Anything **not** listed here — internal modules, private methods, `@__safe_mem
|
|
|
1611
1770
|
| `shared_cache:` | `String \| nil` | `nil` | Name of a globally-registered shared store; incompatible with `shared:`, `store:`, `fiber_local:`, `ractor_safe:`, and `max_size:` |
|
|
1612
1771
|
| `cache_bust:` | `Proc \| Symbol \| nil` | `nil` | Version-token callable; invoked on the instance at each lookup; token is folded into the key; incompatible with `key:` |
|
|
1613
1772
|
| `copy_on_read:` | `Boolean` | `false` | Return a `dup`/`deep_dup` of the cached value on every read; protects shared state from caller mutation; nil and frozen values pass through; incompatible with `ractor_safe:` |
|
|
1773
|
+
| `group:` | `Symbol \| String \| nil` | `nil` | Assigns the method to a named invalidation group; call `reset_memo_group` / `reset_shared_memo_group` to bust all methods in the group at once; a method belongs to at most one group |
|
|
1774
|
+
| `circuit_breaker:` | `true \| Hash \| nil` | `nil` | Wraps the configured `store:` in a `Stores::CircuitBreaker`; `true` uses defaults (`error_threshold: 5`, `probe_interval: 30`); pass a Hash to customise; requires a store to be set; does not double-wrap |
|
|
1614
1775
|
| *(extension options)* | any | — | Unknown kwargs are validated against registered extensions; raise `ArgumentError` if unclaimed |
|
|
1615
1776
|
|
|
1616
1777
|
### `memoize_all` options (class method)
|
|
@@ -1628,7 +1789,7 @@ All `memoize` option keys above, plus:
|
|
|
1628
1789
|
|
|
1629
1790
|
| Option key | Type | Default | Notes |
|
|
1630
1791
|
|---|---|---|---|
|
|
1631
|
-
| any `memoize` key except mode-switches | — | — | Accepts `ttl:`, `max_size:`, `ttl_refresh:`, `if:`, `unless:`, `key:`, `cache_bust:`, `copy_on_read:`, `namespace:`, `store:`; raises `ArgumentError` for `shared:`, `fiber_local:`, `ractor_safe:`, `shared_cache:` |
|
|
1792
|
+
| any `memoize` key except mode-switches | — | — | Accepts `ttl:`, `max_size:`, `ttl_refresh:`, `if:`, `unless:`, `key:`, `cache_bust:`, `copy_on_read:`, `namespace:`, `store:`, `group:`, `circuit_breaker:`; raises `ArgumentError` for `shared:`, `fiber_local:`, `ractor_safe:`, `shared_cache:` |
|
|
1632
1793
|
|
|
1633
1794
|
### Instance methods (public)
|
|
1634
1795
|
|
|
@@ -1650,10 +1811,18 @@ All `memoize` option keys above, plus:
|
|
|
1650
1811
|
| Method | Returns |
|
|
1651
1812
|
|---|---|
|
|
1652
1813
|
| `reset_memo(method_name, *args, **kwargs)` | `nil` |
|
|
1814
|
+
| `reset_memo_group(group_name)` | `nil` |
|
|
1653
1815
|
| `reset_all_memos` | `nil` |
|
|
1654
1816
|
| `memo_touch(method_name, *args, ttl: nil, **kwargs)` | `Boolean` |
|
|
1655
1817
|
| `memo_refresh(method_name, *args, **kwargs)` | cached value |
|
|
1656
1818
|
|
|
1819
|
+
**Group introspection**
|
|
1820
|
+
|
|
1821
|
+
| Method | Returns |
|
|
1822
|
+
|---|---|
|
|
1823
|
+
| `memo_groups` | `Array<Symbol>` — all group names on the class |
|
|
1824
|
+
| `memo_group_methods(group_name)` | `Array<Symbol>` — methods in the group |
|
|
1825
|
+
|
|
1657
1826
|
**Warm-up and persistence**
|
|
1658
1827
|
|
|
1659
1828
|
| Method | Returns |
|
|
@@ -1705,11 +1874,19 @@ All `memoize` option keys above, plus:
|
|
|
1705
1874
|
|---|---|
|
|
1706
1875
|
| `reset_shared_memo(method_name, *args, **kwargs)` | `nil` |
|
|
1707
1876
|
| `reset_all_shared_memos` | `nil` |
|
|
1877
|
+
| `reset_shared_memo_group(group_name)` | `nil` |
|
|
1708
1878
|
| `shared_memoized?(method_name, *args, **kwargs)` | `Boolean` |
|
|
1709
1879
|
| `shared_memo_count(method_name = nil)` | `Integer` |
|
|
1710
1880
|
| `shared_memo_age(method_name, *args, **kwargs)` | `Numeric \| nil` |
|
|
1711
1881
|
| `shared_memo_stale?(method_name, *args, **kwargs)` | `Boolean` |
|
|
1712
1882
|
|
|
1883
|
+
### Group class methods (available on any class that uses `group:`)
|
|
1884
|
+
|
|
1885
|
+
| Method | Returns |
|
|
1886
|
+
|---|---|
|
|
1887
|
+
| `safe_memo_groups` | `Array<Symbol>` — all group names on the class |
|
|
1888
|
+
| `safe_memo_group_methods(group_name)` | `Array<Symbol>` — methods belonging to the group |
|
|
1889
|
+
|
|
1713
1890
|
**Ractor-safe shared cache (added when any method uses `ractor_safe: true`)**
|
|
1714
1891
|
|
|
1715
1892
|
| Method | Returns |
|
data/ROADMAP.md
CHANGED
|
@@ -4,26 +4,6 @@ This document tracks the planned evolution of SafeMemoize through v1.0.0 and bey
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## v1.5.0 — Cache Invalidation
|
|
8
|
-
|
|
9
|
-
*Goal: group-level cache invalidation so related methods can be busted in one operation.*
|
|
10
|
-
|
|
11
|
-
| Feature | Description | Status |
|
|
12
|
-
|---|---|---|
|
|
13
|
-
| Memoization groups | `memoize :find, group: :database` then `reset_memo_group(:database)` to invalidate all methods tagged with the same group at once; groups can span multiple methods on the same class | Planned |
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## v1.6.0 — Resilience
|
|
18
|
-
|
|
19
|
-
*Goal: make external-store memoization resilient to infrastructure failures.*
|
|
20
|
-
|
|
21
|
-
| Feature | Description | Status |
|
|
22
|
-
|---|---|---|
|
|
23
|
-
| Circuit breaker for external stores | When a `store:` adapter raises on `read` or `write`, automatically fall back to the per-instance in-process hash rather than propagating the exception; configurable error threshold and recovery probe interval | Planned |
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
7
|
## v1.7.0 — Advanced Store Features
|
|
28
8
|
|
|
29
9
|
*Goal: multi-process performance patterns for high-traffic deployments.*
|
|
@@ -64,6 +64,16 @@ module SafeMemoize
|
|
|
64
64
|
# itself. Prevents callers from mutating shared cached state. Frozen and +nil+
|
|
65
65
|
# values are returned as-is. Incompatible with +ractor_safe:+ (ractor values are
|
|
66
66
|
# always frozen; use that guarantee instead).
|
|
67
|
+
# @param group [Symbol, String, nil] assigns the method to a named invalidation
|
|
68
|
+
# group. Call {PublicMethods#reset_memo_group} on an instance (or
|
|
69
|
+
# {.reset_shared_memo_group} for shared-mode methods) to bust every method in the
|
|
70
|
+
# group at once. A method may belong to at most one group; re-memoizing with a
|
|
71
|
+
# different group moves it. Must be a non-empty Symbol or String.
|
|
72
|
+
# @param circuit_breaker [Boolean, Hash, nil] wraps the configured +store:+ adapter
|
|
73
|
+
# in a {Stores::CircuitBreaker}. Pass +true+ to use defaults (+error_threshold: 5+,
|
|
74
|
+
# +probe_interval: 30+), or a +Hash+ with +:error_threshold+ and/or +:probe_interval+
|
|
75
|
+
# keys to customise. Requires a +store:+ to be set (per-method, class-level, or
|
|
76
|
+
# global default); raises +ArgumentError+ otherwise.
|
|
67
77
|
# @return [void]
|
|
68
78
|
# @raise [ArgumentError] if the method does not exist, or option values are invalid
|
|
69
79
|
#
|
|
@@ -84,7 +94,7 @@ module SafeMemoize
|
|
|
84
94
|
# @example With a custom store
|
|
85
95
|
# STORE = SafeMemoize::Stores::Memory.new
|
|
86
96
|
# memoize :fetch, store: STORE, ttl: 300
|
|
87
|
-
def memoize(method_name, ttl: UNSET, max_size: UNSET, ttl_refresh: UNSET, if: UNSET, unless: UNSET, shared: UNSET, key: UNSET, store: UNSET, fiber_local: UNSET, ractor_safe: UNSET, namespace: UNSET, shared_cache: UNSET, cache_bust: UNSET, copy_on_read: UNSET, **extension_options)
|
|
97
|
+
def memoize(method_name, ttl: UNSET, max_size: UNSET, ttl_refresh: UNSET, if: UNSET, unless: UNSET, shared: UNSET, key: UNSET, store: UNSET, fiber_local: UNSET, ractor_safe: UNSET, namespace: UNSET, shared_cache: UNSET, cache_bust: UNSET, copy_on_read: UNSET, group: UNSET, circuit_breaker: UNSET, **extension_options)
|
|
88
98
|
method_name = method_name.to_sym
|
|
89
99
|
|
|
90
100
|
unless method_defined?(method_name) || private_method_defined?(method_name) || protected_method_defined?(method_name)
|
|
@@ -126,6 +136,8 @@ module SafeMemoize
|
|
|
126
136
|
copy_on_read = cls_defaults[:copy_on_read] if copy_on_read.equal?(UNSET) && cls_defaults.key?(:copy_on_read)
|
|
127
137
|
namespace = cls_defaults[:namespace] if namespace.equal?(UNSET) && cls_defaults.key?(:namespace)
|
|
128
138
|
store = cls_defaults[:store] if store.equal?(UNSET) && cls_defaults.key?(:store)
|
|
139
|
+
group = cls_defaults[:group] if group.equal?(UNSET) && cls_defaults.key?(:group)
|
|
140
|
+
circuit_breaker = cls_defaults[:circuit_breaker] if circuit_breaker.equal?(UNSET) && cls_defaults.key?(:circuit_breaker)
|
|
129
141
|
end
|
|
130
142
|
|
|
131
143
|
# Normalize remaining UNSET to original per-call defaults
|
|
@@ -141,6 +153,8 @@ module SafeMemoize
|
|
|
141
153
|
shared_cache = nil if shared_cache.equal?(UNSET)
|
|
142
154
|
cache_bust = nil if cache_bust.equal?(UNSET)
|
|
143
155
|
copy_on_read = false if copy_on_read.equal?(UNSET)
|
|
156
|
+
group = nil if group.equal?(UNSET)
|
|
157
|
+
circuit_breaker = nil if circuit_breaker.equal?(UNSET)
|
|
144
158
|
cond_if = nil if cond_if.equal?(UNSET)
|
|
145
159
|
cond_unless = nil if cond_unless.equal?(UNSET)
|
|
146
160
|
|
|
@@ -213,6 +227,15 @@ module SafeMemoize
|
|
|
213
227
|
__safe_memo_method_namespaces__[method_name] = namespace
|
|
214
228
|
end
|
|
215
229
|
|
|
230
|
+
if group
|
|
231
|
+
unless group.is_a?(Symbol) || group.is_a?(String)
|
|
232
|
+
raise ArgumentError, "group: must be a Symbol or String (got #{group.class})"
|
|
233
|
+
end
|
|
234
|
+
group = group.to_sym
|
|
235
|
+
raise ArgumentError, "group: must not be empty" if group.empty?
|
|
236
|
+
__safe_memo_register_group__(method_name, group)
|
|
237
|
+
end
|
|
238
|
+
|
|
216
239
|
if shared_cache
|
|
217
240
|
raise ArgumentError, "shared_cache: must be a String (got #{shared_cache.class})" unless shared_cache.is_a?(String)
|
|
218
241
|
raise ArgumentError, "shared_cache: must not be empty" if shared_cache.empty?
|
|
@@ -244,6 +267,19 @@ module SafeMemoize
|
|
|
244
267
|
end
|
|
245
268
|
end
|
|
246
269
|
|
|
270
|
+
if circuit_breaker
|
|
271
|
+
unless circuit_breaker == true || circuit_breaker.is_a?(Hash)
|
|
272
|
+
raise ArgumentError, "circuit_breaker: must be true or a Hash of options (got #{circuit_breaker.class})"
|
|
273
|
+
end
|
|
274
|
+
unless effective_store
|
|
275
|
+
raise ArgumentError, "circuit_breaker: requires a store: to be configured (no store is set for :#{method_name})"
|
|
276
|
+
end
|
|
277
|
+
unless effective_store.is_a?(Stores::CircuitBreaker)
|
|
278
|
+
cb_opts = circuit_breaker.is_a?(Hash) ? circuit_breaker : {}
|
|
279
|
+
effective_store = Stores::CircuitBreaker.new(effective_store, **cb_opts)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
247
283
|
__safe_memo_class_key_generators__[method_name] = key if key
|
|
248
284
|
__safe_memo_class_cache_bust_generators__[method_name] = cache_bust if cache_bust
|
|
249
285
|
|
|
@@ -763,6 +799,35 @@ module SafeMemoize
|
|
|
763
799
|
end
|
|
764
800
|
end
|
|
765
801
|
|
|
802
|
+
# Clears all shared-cache entries for every method in the given group.
|
|
803
|
+
#
|
|
804
|
+
# Only affects methods memoized with +shared: true+. For per-instance cache
|
|
805
|
+
# invalidation use {PublicMethods#reset_memo_group} on the instance.
|
|
806
|
+
#
|
|
807
|
+
# @param group_name [Symbol, String]
|
|
808
|
+
# @return [void]
|
|
809
|
+
def reset_shared_memo_group(group_name)
|
|
810
|
+
group_name = group_name.to_sym
|
|
811
|
+
(__safe_memo_groups__[group_name] || []).each { |m| reset_shared_memo(m) }
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
# Returns the method names belonging to the given invalidation group, or an
|
|
815
|
+
# empty array when the group is unknown.
|
|
816
|
+
#
|
|
817
|
+
# @param group_name [Symbol, String]
|
|
818
|
+
# @return [Array<Symbol>]
|
|
819
|
+
def safe_memo_group_methods(group_name)
|
|
820
|
+
group_name = group_name.to_sym
|
|
821
|
+
(__safe_memo_groups__[group_name] || []).dup
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Returns all group names registered on this class.
|
|
825
|
+
#
|
|
826
|
+
# @return [Array<Symbol>]
|
|
827
|
+
def safe_memo_groups
|
|
828
|
+
__safe_memo_groups__.keys
|
|
829
|
+
end
|
|
830
|
+
|
|
766
831
|
private
|
|
767
832
|
|
|
768
833
|
def __safe_memo_shared_cache__
|
|
@@ -793,6 +858,17 @@ module SafeMemoize
|
|
|
793
858
|
@__safe_memoize_defaults__
|
|
794
859
|
end
|
|
795
860
|
|
|
861
|
+
def __safe_memo_groups__
|
|
862
|
+
@__safe_memo_groups__ ||= {}
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def __safe_memo_register_group__(method_name, group)
|
|
866
|
+
groups = __safe_memo_groups__
|
|
867
|
+
# Remove method from any prior group it belonged to
|
|
868
|
+
groups.each_value { |methods| methods.delete(method_name) }
|
|
869
|
+
(groups[group] ||= []) << method_name
|
|
870
|
+
end
|
|
871
|
+
|
|
796
872
|
# Resolves the effective first-element key sym for a given bare method name,
|
|
797
873
|
# applying the active namespace. Used by class-level cache operations where
|
|
798
874
|
# instance methods (compute_cache_key) are unavailable.
|
|
@@ -359,6 +359,37 @@ module SafeMemoize
|
|
|
359
359
|
end
|
|
360
360
|
end
|
|
361
361
|
|
|
362
|
+
# Clears all per-instance cached entries for every method belonging to the
|
|
363
|
+
# given invalidation group (declared via +memoize :method, group: :name+).
|
|
364
|
+
#
|
|
365
|
+
# A no-op when the group is unknown or has no members. Each evicted entry
|
|
366
|
+
# fires the +:on_evict+ hook. For shared-mode methods use the class-level
|
|
367
|
+
# {ClassMethods.reset_shared_memo_group} instead.
|
|
368
|
+
#
|
|
369
|
+
# @param group_name [Symbol, String]
|
|
370
|
+
# @return [void]
|
|
371
|
+
def reset_memo_group(group_name)
|
|
372
|
+
group_name = group_name.to_sym
|
|
373
|
+
(self.class.send(:__safe_memo_groups__)[group_name] || []).each { |m| reset_memo(m) }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Returns the method names belonging to the given invalidation group on
|
|
377
|
+
# this instance's class, or an empty array when the group is unknown.
|
|
378
|
+
#
|
|
379
|
+
# @param group_name [Symbol, String]
|
|
380
|
+
# @return [Array<Symbol>]
|
|
381
|
+
def memo_group_methods(group_name)
|
|
382
|
+
group_name = group_name.to_sym
|
|
383
|
+
(self.class.send(:__safe_memo_groups__)[group_name] || []).dup
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Returns all invalidation group names registered on this instance's class.
|
|
387
|
+
#
|
|
388
|
+
# @return [Array<Symbol>]
|
|
389
|
+
def memo_groups
|
|
390
|
+
self.class.send(:__safe_memo_groups__).keys
|
|
391
|
+
end
|
|
392
|
+
|
|
362
393
|
# Clears all cached entries for every method on this instance.
|
|
363
394
|
# Each evicted entry fires the +:on_evict+ hook.
|
|
364
395
|
#
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeMemoize
|
|
4
|
+
module Stores
|
|
5
|
+
# Wraps any {Base} store adapter with a circuit breaker that silently falls
|
|
6
|
+
# back to the per-instance in-process cache when the external store is
|
|
7
|
+
# unavailable, rather than propagating exceptions to callers.
|
|
8
|
+
#
|
|
9
|
+
# === States
|
|
10
|
+
#
|
|
11
|
+
# * +:closed+ — normal; every call goes through to the wrapped store;
|
|
12
|
+
# consecutive errors are counted
|
|
13
|
+
# * +:open+ — tripped; reads return {MISS} and writes are no-ops so the
|
|
14
|
+
# memoize wrapper falls back to the per-instance hash; no
|
|
15
|
+
# calls reach the wrapped store until the probe interval elapses
|
|
16
|
+
# * +:half_open+ — probe period (probe interval elapsed); calls are let
|
|
17
|
+
# through to the wrapped store; the first success closes the
|
|
18
|
+
# circuit, any failure re-opens it and resets the timer
|
|
19
|
+
#
|
|
20
|
+
# Any successful call while the circuit is +:closed+ resets the consecutive
|
|
21
|
+
# error counter, so transient blips do not accumulate toward the threshold.
|
|
22
|
+
#
|
|
23
|
+
# @example Wrap a custom Redis store
|
|
24
|
+
# store = SafeMemoize::Stores::CircuitBreaker.new(
|
|
25
|
+
# MyRedisStore.new,
|
|
26
|
+
# error_threshold: 5,
|
|
27
|
+
# probe_interval: 30
|
|
28
|
+
# )
|
|
29
|
+
# memoize :fetch, store: store
|
|
30
|
+
#
|
|
31
|
+
# @example Via the circuit_breaker: option (auto-wraps the configured store)
|
|
32
|
+
# memoize :fetch, store: MyRedisStore.new, circuit_breaker: true
|
|
33
|
+
# memoize :fetch, store: MyRedisStore.new,
|
|
34
|
+
# circuit_breaker: { error_threshold: 3, probe_interval: 60 }
|
|
35
|
+
class CircuitBreaker < Base
|
|
36
|
+
DEFAULT_ERROR_THRESHOLD = 5
|
|
37
|
+
DEFAULT_PROBE_INTERVAL = 30.0
|
|
38
|
+
|
|
39
|
+
# @return [Stores::Base] the wrapped inner store
|
|
40
|
+
attr_reader :wrapped_store
|
|
41
|
+
# @return [Integer] number of consecutive errors that trip the circuit
|
|
42
|
+
attr_reader :error_threshold
|
|
43
|
+
# @return [Float] seconds after tripping before a probe is attempted
|
|
44
|
+
attr_reader :probe_interval
|
|
45
|
+
|
|
46
|
+
# @param store [Stores::Base] the backing store to protect
|
|
47
|
+
# @param error_threshold [Integer] consecutive errors that trip the circuit (default 5)
|
|
48
|
+
# @param probe_interval [Numeric] seconds to wait before probing (default 30)
|
|
49
|
+
# @raise [ArgumentError] if +store+ is not a {Stores::Base} instance, or
|
|
50
|
+
# if threshold / interval are invalid
|
|
51
|
+
def initialize(store, error_threshold: DEFAULT_ERROR_THRESHOLD, probe_interval: DEFAULT_PROBE_INTERVAL)
|
|
52
|
+
unless store.is_a?(Base)
|
|
53
|
+
raise ArgumentError, "CircuitBreaker requires a Stores::Base instance (got #{store.class})"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@wrapped_store = store
|
|
57
|
+
@error_threshold = Integer(error_threshold)
|
|
58
|
+
@probe_interval = Float(probe_interval)
|
|
59
|
+
|
|
60
|
+
raise ArgumentError, "error_threshold must be positive" unless @error_threshold > 0
|
|
61
|
+
raise ArgumentError, "probe_interval must be positive" unless @probe_interval > 0
|
|
62
|
+
|
|
63
|
+
@mutex = Mutex.new
|
|
64
|
+
@error_count = 0
|
|
65
|
+
@opened_at = nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Read from the wrapped store, returning {MISS} on error or when the
|
|
69
|
+
# circuit is open instead of raising.
|
|
70
|
+
def read(key)
|
|
71
|
+
st = current_state
|
|
72
|
+
return MISS if st == :open
|
|
73
|
+
|
|
74
|
+
result = @wrapped_store.read(key)
|
|
75
|
+
record_success(st)
|
|
76
|
+
result
|
|
77
|
+
rescue
|
|
78
|
+
record_failure
|
|
79
|
+
MISS
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Write to the wrapped store, silently swallowing errors so the caller's
|
|
83
|
+
# return value is unaffected. A no-op when the circuit is open.
|
|
84
|
+
def write(key, value, expires_in: nil)
|
|
85
|
+
st = current_state
|
|
86
|
+
return if st == :open
|
|
87
|
+
|
|
88
|
+
@wrapped_store.write(key, value, expires_in: expires_in)
|
|
89
|
+
record_success(st)
|
|
90
|
+
rescue
|
|
91
|
+
record_failure
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Delete from the wrapped store. A no-op when the circuit is open.
|
|
95
|
+
def delete(key)
|
|
96
|
+
return if current_state == :open
|
|
97
|
+
|
|
98
|
+
@wrapped_store.delete(key)
|
|
99
|
+
rescue
|
|
100
|
+
record_failure
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Clear the wrapped store. Errors are recorded but not re-raised.
|
|
104
|
+
def clear
|
|
105
|
+
@wrapped_store.clear
|
|
106
|
+
rescue
|
|
107
|
+
record_failure
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns live keys from the wrapped store, or an empty array when the
|
|
111
|
+
# circuit is open or the store raises.
|
|
112
|
+
def keys
|
|
113
|
+
return [] if current_state == :open
|
|
114
|
+
|
|
115
|
+
@wrapped_store.keys
|
|
116
|
+
rescue
|
|
117
|
+
record_failure
|
|
118
|
+
[]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Returns the current circuit state: +:closed+, +:open+, or +:half_open+.
|
|
122
|
+
# @return [Symbol]
|
|
123
|
+
def state
|
|
124
|
+
current_state
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Returns +true+ when the circuit is not fully closed (i.e. open or half-open).
|
|
128
|
+
# @return [Boolean]
|
|
129
|
+
def open?
|
|
130
|
+
current_state != :closed
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns the current consecutive error count.
|
|
134
|
+
# @return [Integer]
|
|
135
|
+
def error_count
|
|
136
|
+
@mutex.synchronize { @error_count }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Manually resets the circuit to +:closed+, clearing the error counter.
|
|
140
|
+
# @return [void]
|
|
141
|
+
def reset!
|
|
142
|
+
@mutex.synchronize do
|
|
143
|
+
@error_count = 0
|
|
144
|
+
@opened_at = nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def current_state
|
|
151
|
+
@mutex.synchronize do
|
|
152
|
+
next :closed if @opened_at.nil?
|
|
153
|
+
|
|
154
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - @opened_at
|
|
155
|
+
(elapsed >= @probe_interval) ? :half_open : :open
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def record_success(prior_state)
|
|
160
|
+
return unless prior_state == :half_open || @error_count > 0
|
|
161
|
+
|
|
162
|
+
@mutex.synchronize do
|
|
163
|
+
@error_count = 0
|
|
164
|
+
@opened_at = nil
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def record_failure
|
|
169
|
+
@mutex.synchronize do
|
|
170
|
+
@error_count += 1
|
|
171
|
+
if @error_count >= @error_threshold || !@opened_at.nil?
|
|
172
|
+
@opened_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
data/lib/safe_memoize/version.rb
CHANGED
data/lib/safe_memoize.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "safe_memoize/configuration"
|
|
|
5
5
|
require_relative "safe_memoize/extension"
|
|
6
6
|
require_relative "safe_memoize/stores/base"
|
|
7
7
|
require_relative "safe_memoize/stores/memory"
|
|
8
|
+
require_relative "safe_memoize/stores/circuit_breaker"
|
|
8
9
|
require_relative "safe_memoize/adapters/statsd"
|
|
9
10
|
require_relative "safe_memoize/adapters/opentelemetry"
|
|
10
11
|
require_relative "safe_memoize/adapters/concurrent_ruby"
|
data/sig/safe_memoize.rbs
CHANGED
|
@@ -17,6 +17,7 @@ module SafeMemoize
|
|
|
17
17
|
@__safe_memo_shared_cache__: Hash[memo_key, memo_record]?
|
|
18
18
|
@__safe_memo_shared_mutex__: Mutex?
|
|
19
19
|
@__safe_memo_shared_lru_order__: Hash[Symbol, Hash[memo_key, true]]?
|
|
20
|
+
@__safe_memo_groups__: Hash[Symbol, Array[Symbol]]?
|
|
20
21
|
|
|
21
22
|
UNSET: untyped
|
|
22
23
|
SHARED_CACHE_REGISTRY: Hash[String, Stores::Base]
|
|
@@ -65,7 +66,7 @@ module SafeMemoize
|
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
module ClassMethods
|
|
68
|
-
def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?store: Stores::Base?, ?fiber_local: bool, ?ractor_safe: bool, ?namespace: String?, ?shared_cache: String?, ?cache_bust: (^() -> untyped) | Symbol | nil, ?copy_on_read: bool, **untyped extension_options) -> void
|
|
69
|
+
def memoize: (Symbol | String method_name, ?ttl: Numeric?, ?max_size: Integer?, ?ttl_refresh: bool, ?if: (^(untyped result) -> boolish)?, ?unless: (^(untyped result) -> boolish)?, ?shared: bool, ?key: (^(*untyped args, **untyped kwargs) -> untyped)?, ?store: Stores::Base?, ?fiber_local: bool, ?ractor_safe: bool, ?namespace: String?, ?shared_cache: String?, ?cache_bust: (^() -> untyped) | Symbol | nil, ?copy_on_read: bool, ?group: Symbol | String | nil, ?circuit_breaker: bool | Hash[Symbol, untyped] | nil, **untyped extension_options) -> void
|
|
69
70
|
def safe_memoize_store: () -> Stores::Base?
|
|
70
71
|
def safe_memoize_store=: (Stores::Base?) -> Stores::Base?
|
|
71
72
|
def safe_memoize_namespace: () -> String?
|
|
@@ -78,6 +79,9 @@ module SafeMemoize
|
|
|
78
79
|
def shared_memo_count: (?Symbol | String method_name) -> Integer
|
|
79
80
|
def shared_memo_age: (Symbol | String method_name, *untyped args, **untyped kwargs) -> Float?
|
|
80
81
|
def shared_memo_stale?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
82
|
+
def reset_shared_memo_group: (Symbol | String group_name) -> void
|
|
83
|
+
def safe_memo_group_methods: (Symbol | String group_name) -> Array[Symbol]
|
|
84
|
+
def safe_memo_groups: () -> Array[Symbol]
|
|
81
85
|
|
|
82
86
|
private
|
|
83
87
|
|
|
@@ -88,6 +92,8 @@ module SafeMemoize
|
|
|
88
92
|
def __safe_memo_method_namespaces__: () -> Hash[Symbol, String]
|
|
89
93
|
def __safe_memo_class_cache_bust_generators__: () -> Hash[Symbol, Proc | Symbol]
|
|
90
94
|
def __safe_memoize_defaults__: () -> Hash[Symbol, untyped]?
|
|
95
|
+
def __safe_memo_groups__: () -> Hash[Symbol, Array[Symbol]]
|
|
96
|
+
def __safe_memo_register_group__: (Symbol method_name, Symbol group) -> void
|
|
91
97
|
def memoized_method_visibility: (Symbol method_name) -> Symbol
|
|
92
98
|
end
|
|
93
99
|
|
|
@@ -114,6 +120,9 @@ module SafeMemoize
|
|
|
114
120
|
def memo_age: (Symbol | String method_name, *untyped args, **untyped kwargs) -> Float?
|
|
115
121
|
def memo_stale?: (Symbol | String method_name, *untyped args, **untyped kwargs) -> bool
|
|
116
122
|
def reset_memo: (Symbol | String method_name, *untyped args, **untyped kwargs) -> void
|
|
123
|
+
def reset_memo_group: (Symbol | String group_name) -> void
|
|
124
|
+
def memo_group_methods: (Symbol | String group_name) -> Array[Symbol]
|
|
125
|
+
def memo_groups: () -> Array[Symbol]
|
|
117
126
|
def reset_all_memos: () -> void
|
|
118
127
|
def memo_inspect: (Symbol | String method_name, *untyped args, **untyped kwargs) -> { cached: bool, value: untyped, hits: Integer, misses: Integer, ttl_remaining: Float?, age: Float?, custom_key: untyped, lru_position: Integer? }?
|
|
119
128
|
end
|
|
@@ -291,6 +300,32 @@ module SafeMemoize
|
|
|
291
300
|
|
|
292
301
|
def expired?: ({ expires_at: Float?, value: untyped, cached_at: Float }) -> bool
|
|
293
302
|
end
|
|
303
|
+
|
|
304
|
+
class CircuitBreaker < Base
|
|
305
|
+
DEFAULT_ERROR_THRESHOLD: Integer
|
|
306
|
+
DEFAULT_PROBE_INTERVAL: Float
|
|
307
|
+
|
|
308
|
+
attr_reader wrapped_store: Base
|
|
309
|
+
attr_reader error_threshold: Integer
|
|
310
|
+
attr_reader probe_interval: Float
|
|
311
|
+
|
|
312
|
+
def initialize: (Base store, ?error_threshold: Integer, ?probe_interval: Numeric) -> void
|
|
313
|
+
def read: (untyped key) -> untyped
|
|
314
|
+
def write: (untyped key, untyped value, ?expires_in: Numeric?) -> void
|
|
315
|
+
def delete: (untyped key) -> void
|
|
316
|
+
def clear: () -> void
|
|
317
|
+
def keys: () -> Array[untyped]
|
|
318
|
+
def state: () -> Symbol
|
|
319
|
+
def open?: () -> bool
|
|
320
|
+
def error_count: () -> Integer
|
|
321
|
+
def reset!: () -> void
|
|
322
|
+
|
|
323
|
+
private
|
|
324
|
+
|
|
325
|
+
def current_state: () -> Symbol
|
|
326
|
+
def record_success: (Symbol prior_state) -> void
|
|
327
|
+
def record_failure: () -> void
|
|
328
|
+
end
|
|
294
329
|
end
|
|
295
330
|
|
|
296
331
|
module Adapters
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: safe_memoize
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chuck Smith
|
|
@@ -77,6 +77,7 @@ files:
|
|
|
77
77
|
- lib/safe_memoize/rails/request_scoped.rb
|
|
78
78
|
- lib/safe_memoize/release_tooling.rb
|
|
79
79
|
- lib/safe_memoize/stores/base.rb
|
|
80
|
+
- lib/safe_memoize/stores/circuit_breaker.rb
|
|
80
81
|
- lib/safe_memoize/stores/memory.rb
|
|
81
82
|
- lib/safe_memoize/stores/rails_cache.rb
|
|
82
83
|
- lib/safe_memoize/stores/redis.rb
|