safe_memoize 0.9.0 → 1.0.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/.yardopts +10 -0
- data/CHANGELOG.md +12 -0
- data/README.md +181 -2
- data/ROADMAP.md +7 -22
- data/Rakefile +5 -0
- data/UPGRADING.md +197 -0
- data/lib/safe_memoize/adapters/opentelemetry.rb +25 -0
- data/lib/safe_memoize/adapters/statsd.rb +31 -0
- data/lib/safe_memoize/cache_metrics_methods.rb +1 -0
- data/lib/safe_memoize/cache_record_methods.rb +1 -0
- data/lib/safe_memoize/cache_store_methods.rb +1 -0
- data/lib/safe_memoize/class_methods.rb +101 -18
- data/lib/safe_memoize/configuration.rb +40 -2
- data/lib/safe_memoize/custom_key_methods.rb +1 -0
- data/lib/safe_memoize/hooks_methods.rb +1 -0
- data/lib/safe_memoize/inspection_methods.rb +2 -1
- data/lib/safe_memoize/instance_methods.rb +1 -0
- data/lib/safe_memoize/lru_methods.rb +1 -0
- data/lib/safe_memoize/public_custom_key_methods.rb +23 -0
- data/lib/safe_memoize/public_methods.rb +162 -5
- data/lib/safe_memoize/public_metrics_methods.rb +20 -0
- data/lib/safe_memoize/rails/middleware.rb +2 -0
- data/lib/safe_memoize/rails/request_scoped.rb +3 -0
- data/lib/safe_memoize/release_tooling.rb +1 -0
- data/lib/safe_memoize/version.rb +2 -1
- data/lib/safe_memoize.rb +54 -0
- data/rbi/safe_memoize.rbi +245 -0
- data/sig/safe_memoize.rbs +15 -5
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6beffd3f5a1de6f8582c9a164502f353b8ac6c55caec6326edb4c52b0bf913ec
|
|
4
|
+
data.tar.gz: 44ebaf0f89254c692b9d33d8962577c9644df6d910818905e46395ba5e28cf89
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0f22cd22816499ec3400a7b98cf51c3a3b77a43a0e26451ff97d6f01877d02df8772b7b3be0209e86b5c6c00d435bc96e7db8fd3a28619169e1f0e97b8ab0263
|
|
7
|
+
data.tar.gz: af315893620e46fe419a2a61ff4092f7d27f71bbfe98185ba0714162148c7d19b2d31ed9db3909d500f3e12f474f51d66ec027d81cd2d7e9948e4bc388d3a5db
|
data/.yardopts
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,18 @@ from v1.0.0 onwards. Prior 0.x releases may include breaking changes between min
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.0.0] - 2026-05-22
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Ractor compatibility audit — `spec/ractor_spec.rb` documents the specific failure modes (non-shareable closures in `define_method` blocks, `Ractor::IsolationError` on `SafeMemoize.configuration`); README section explains the limitation and the Thread-based workaround
|
|
16
|
+
- Semantic versioning guarantee — README `## Public API and versioning guarantee` section enumerates every public constant, method, option key, and `Configuration` attribute covered by semver from v1.0.0 onwards; opt-in extensions (`SafeMemoize::Rails`, `SafeMemoize::Adapters::*`) are explicitly called out as not yet covered until their owning milestone ships
|
|
17
|
+
- Full API reference — YARD documentation added to all public methods, classes, and modules; `SafeMemoize::Adapters::StatsD` and `SafeMemoize::Adapters::OpenTelemetry` fully documented with usage examples; internal modules marked `@api private`; `.yardopts` and `rake doc` task added; `gem "yard"` added as a development dependency
|
|
18
|
+
- Deprecation sweep — pre-v1.0.0 API consistency audit: `memoized?`, `memo_ttl_remaining`, `memo_touch`, `memo_age`, `memo_stale?` now use `compute_cache_key` instead of `safe_memo_cache_key` so they correctly resolve entries stored with a custom key (instance-level `memoize_with_custom_key` or class-level `key:`); `memo_matcher_for` (used by `reset_memo` and `memo_refresh`) receives the same fix; `SafeMemoize::Error` added to the public API guarantee table and to RBS + Sorbet signatures; RBS and `.rbi` `warm_memo` block annotation corrected back to mandatory (was incorrectly marked optional in v0.9.0 signatures)
|
|
19
|
+
- Ruby version policy — README `## Ruby version support` section formalises the supported version window (Ruby ≥ 3.3; current stable plus two previous non-EOL minors), the cadence for dropping EOL versions (minor release only, never a patch), and a history table of dropped versions; CI matrix documents covered versions with their EOL dates
|
|
20
|
+
- Complete RBS + Sorbet signatures — `sig/safe_memoize.rbs` corrected: `SafeMemoize::Adapters::StatsD` added; `memo_count`, `memo_keys`, `memo_values` fixed from rest-arg to proper optional single arg; `clear_memo_hooks` and `clear_custom_keys` optional-arg annotations corrected; `warm_memo` block marked optional; new `rbi/safe_memoize.rbi` ships Sorbet stubs covering the full public API, all `Configuration` attributes, adapters, and opt-in Rails helpers
|
|
21
|
+
- Upgrade guide — `UPGRADING.md` documents every breaking change introduced across the 0.x series, with before/after code examples and migration steps for each; covers Ruby 3.2 removal, TTL clock change, `memo_keys`/`memo_values` shape change, `memoize` definition-time raise, argument mutation fix, hook exception isolation, and the two custom-key introspection fixes landing in v1.0.0
|
|
22
|
+
|
|
11
23
|
## [0.9.0] - 2026-05-22
|
|
12
24
|
|
|
13
25
|
### Added
|
data/README.md
CHANGED
|
@@ -895,11 +895,25 @@ end
|
|
|
895
895
|
|
|
896
896
|
[↑ Back to features](#features)
|
|
897
897
|
|
|
898
|
+
## Ractor compatibility
|
|
899
|
+
|
|
900
|
+
SafeMemoize is **not Ractor-compatible** in its current form. Passing a class that uses `memoize` into a `Ractor.new` block raises `RuntimeError: defined with an un-shareable Proc in a different Ractor`. There are two root causes:
|
|
901
|
+
|
|
902
|
+
1. **Non-shareable closures.** `ClassMethods#memoize` builds anonymous modules using `define_method` with blocks that close over local variables (`ttl`, `max_size`, `condition`, `shared_mutex`, …). Ruby marks those Procs as non-Ractor-shareable, so the host class cannot be sent to a Ractor.
|
|
903
|
+
|
|
904
|
+
2. **Mutable module-level state.** `SafeMemoize.configuration` reads `@configuration` from the `SafeMemoize` module — a mutable ivar on a shared constant — which raises `Ractor::IsolationError` from a non-main Ractor. This affects every memoized call because hooks and adapters always read the configuration.
|
|
905
|
+
|
|
906
|
+
**Workaround:** Use Ruby Threads instead of Ractors — SafeMemoize is fully thread-safe via double-check locking and per-instance Mutexes. If you need true parallelism with Ractors, perform computation inside the Ractor without memoization and send frozen results back via `Ractor#send`.
|
|
907
|
+
|
|
908
|
+
Ractor support is tracked in the v1.0.0 roadmap. The fix would require replacing closed-over variables with frozen shareable bindings and making `Configuration` a frozen value object, which is a significant redesign.
|
|
909
|
+
|
|
898
910
|
## Development
|
|
899
911
|
|
|
900
912
|
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.
|
|
901
913
|
|
|
902
|
-
To run the benchmark suite: `bundle exec ruby benchmarks/benchmark.rb`.
|
|
914
|
+
To run the benchmark suite: `bundle exec ruby benchmarks/benchmark.rb`.
|
|
915
|
+
|
|
916
|
+
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.
|
|
903
917
|
|
|
904
918
|
GitHub Actions also runs the full `bundle exec rake` suite automatically for pull requests, manual workflow runs, and pushes to `main` via `.github/workflows/ci.yml`.
|
|
905
919
|
|
|
@@ -936,9 +950,174 @@ To preview the changelog/version update without changing anything, use:
|
|
|
936
950
|
bin/release 0.1.1 --dry-run
|
|
937
951
|
```
|
|
938
952
|
|
|
953
|
+
## Public API and versioning guarantee
|
|
954
|
+
|
|
955
|
+
From **v1.0.0** onwards SafeMemoize follows [Semantic Versioning](https://semver.org/). The table below declares every constant, method, and option key that forms the public contract. If you only call items listed here, you are guaranteed that:
|
|
956
|
+
|
|
957
|
+
- **Patch** releases (1.x.**y**) contain bug fixes only — no behaviour changes.
|
|
958
|
+
- **Minor** releases (1.**x**.0) add new features in a backwards-compatible way.
|
|
959
|
+
- **Major** releases (**x**.0.0) may break the items below; a migration guide will be published for every such release.
|
|
960
|
+
|
|
961
|
+
Anything **not** listed here — internal modules, private methods, `@__safe_memo_*__` ivars, the structure of the cache hash itself — is subject to change without notice in any release, including patch releases.
|
|
962
|
+
|
|
963
|
+
### Top-level module
|
|
964
|
+
|
|
965
|
+
| Symbol | Kind | Notes |
|
|
966
|
+
|---|---|---|
|
|
967
|
+
| `SafeMemoize::VERSION` | constant | Semver string, always present |
|
|
968
|
+
| `SafeMemoize::Error` | class | Base error class (`< StandardError`) for rescuing any SafeMemoize-raised exception |
|
|
969
|
+
| `SafeMemoize.configure { \|c\| … }` | module method | Yields `Configuration`; sets global defaults |
|
|
970
|
+
| `SafeMemoize.configuration` | module method | Returns the current `Configuration` |
|
|
971
|
+
| `SafeMemoize.reset_configuration!` | module method | Restores all configuration to defaults |
|
|
972
|
+
| `SafeMemoize.deprecate(subject, message:, horizon:)` | module method | Emits a structured deprecation warning |
|
|
973
|
+
|
|
974
|
+
### `memoize` DSL (class method, added by `prepend SafeMemoize`)
|
|
975
|
+
|
|
976
|
+
| Option key | Type | Default | Notes |
|
|
977
|
+
|---|---|---|---|
|
|
978
|
+
| `ttl:` | `Numeric \| nil` | `nil` | Seconds until entry expires |
|
|
979
|
+
| `ttl_refresh:` | `Boolean` | `false` | Sliding window — resets clock on every hit |
|
|
980
|
+
| `max_size:` | `Integer \| nil` | `nil` | LRU entry limit per method |
|
|
981
|
+
| `if:` | `Symbol \| Proc \| nil` | `nil` | Store only when truthy |
|
|
982
|
+
| `unless:` | `Symbol \| Proc \| nil` | `nil` | Store only when falsy |
|
|
983
|
+
| `shared:` | `Boolean` | `false` | Class-level shared cache |
|
|
984
|
+
| `key:` | `Proc \| nil` | `nil` | Class-level custom key generator |
|
|
985
|
+
|
|
986
|
+
### `memoize_all` options (class method)
|
|
987
|
+
|
|
988
|
+
All `memoize` option keys above, plus:
|
|
989
|
+
|
|
990
|
+
| Option key | Type | Default |
|
|
991
|
+
|---|---|---|
|
|
992
|
+
| `except:` | `Array<Symbol>` | `[]` |
|
|
993
|
+
| `only:` | `Array<Symbol>` | `[]` |
|
|
994
|
+
| `include_protected:` | `Boolean` | `false` |
|
|
995
|
+
| `include_private:` | `Boolean` | `false` |
|
|
996
|
+
|
|
997
|
+
### Instance methods (public)
|
|
998
|
+
|
|
999
|
+
**Inspection**
|
|
1000
|
+
|
|
1001
|
+
| Method | Returns |
|
|
1002
|
+
|---|---|
|
|
1003
|
+
| `memoized?(method_name, *args, **kwargs)` | `Boolean` |
|
|
1004
|
+
| `memo_count(method_name = nil)` | `Integer` |
|
|
1005
|
+
| `memo_keys(method_name = nil)` | `Array` |
|
|
1006
|
+
| `memo_values(method_name = nil)` | `Array` |
|
|
1007
|
+
| `memo_inspect(method_name, *args, **kwargs)` | `Hash \| nil` |
|
|
1008
|
+
| `memo_ttl_remaining(method_name, *args, **kwargs)` | `Numeric \| nil` |
|
|
1009
|
+
| `memo_age(method_name, *args, **kwargs)` | `Numeric \| nil` |
|
|
1010
|
+
| `memo_stale?(method_name, *args, **kwargs)` | `Boolean` |
|
|
1011
|
+
|
|
1012
|
+
**Invalidation and mutation**
|
|
1013
|
+
|
|
1014
|
+
| Method | Returns |
|
|
1015
|
+
|---|---|
|
|
1016
|
+
| `reset_memo(method_name, *args, **kwargs)` | `nil` |
|
|
1017
|
+
| `reset_all_memos` | `nil` |
|
|
1018
|
+
| `memo_touch(method_name, *args, ttl: nil, **kwargs)` | `Boolean` |
|
|
1019
|
+
| `memo_refresh(method_name, *args, **kwargs)` | cached value |
|
|
1020
|
+
|
|
1021
|
+
**Warm-up and persistence**
|
|
1022
|
+
|
|
1023
|
+
| Method | Returns |
|
|
1024
|
+
|---|---|
|
|
1025
|
+
| `warm_memo(method_name, *args, ttl: nil, **kwargs)` | cached value |
|
|
1026
|
+
| `memo_preload(method_name, *arg_sets)` | `Array` |
|
|
1027
|
+
| `dump_memo(method_name = nil)` | `Hash` |
|
|
1028
|
+
| `load_memo(snapshot)` | `nil` |
|
|
1029
|
+
|
|
1030
|
+
**Lifecycle hooks**
|
|
1031
|
+
|
|
1032
|
+
| Method | Fires when |
|
|
1033
|
+
|---|---|
|
|
1034
|
+
| `on_memo_hit { \|key\| … }` | cache hit |
|
|
1035
|
+
| `on_memo_miss { \|key\| … }` | cache miss |
|
|
1036
|
+
| `on_memo_store { \|key, value\| … }` | value written |
|
|
1037
|
+
| `on_memo_expire { \|key\| … }` | TTL expires |
|
|
1038
|
+
| `on_memo_evict { \|key\| … }` | LRU eviction |
|
|
1039
|
+
| `clear_memo_hooks(hook_type = nil)` | — |
|
|
1040
|
+
|
|
1041
|
+
**Metrics**
|
|
1042
|
+
|
|
1043
|
+
| Method | Returns |
|
|
1044
|
+
|---|---|
|
|
1045
|
+
| `cache_stats` | `Hash` |
|
|
1046
|
+
| `cache_stats_for(method_name)` | `Hash` |
|
|
1047
|
+
| `cache_hit_rate` | `Float` |
|
|
1048
|
+
| `cache_miss_rate` | `Float` |
|
|
1049
|
+
| `cache_metrics_reset(method_name = nil)` | `nil` |
|
|
1050
|
+
|
|
1051
|
+
**Custom keys**
|
|
1052
|
+
|
|
1053
|
+
| Method | Notes |
|
|
1054
|
+
|---|---|
|
|
1055
|
+
| `memoize_with_custom_key(method_name) { \|*args, **kwargs\| … }` | Instance-level key generator |
|
|
1056
|
+
| `clear_custom_keys(method_name = nil)` | Remove one or all key generators |
|
|
1057
|
+
|
|
1058
|
+
### Shared-cache class methods (added when any method uses `shared: true`)
|
|
1059
|
+
|
|
1060
|
+
| Method | Returns |
|
|
1061
|
+
|---|---|
|
|
1062
|
+
| `reset_shared_memo(method_name, *args, **kwargs)` | `nil` |
|
|
1063
|
+
| `reset_all_shared_memos` | `nil` |
|
|
1064
|
+
| `shared_memoized?(method_name, *args, **kwargs)` | `Boolean` |
|
|
1065
|
+
| `shared_memo_count(method_name = nil)` | `Integer` |
|
|
1066
|
+
| `shared_memo_age(method_name, *args, **kwargs)` | `Numeric \| nil` |
|
|
1067
|
+
| `shared_memo_stale?(method_name, *args, **kwargs)` | `Boolean` |
|
|
1068
|
+
|
|
1069
|
+
### `SafeMemoize::Configuration` attributes
|
|
1070
|
+
|
|
1071
|
+
| Attribute | Type | Default |
|
|
1072
|
+
|---|---|---|
|
|
1073
|
+
| `default_ttl` | `Numeric \| nil` | `nil` |
|
|
1074
|
+
| `default_max_size` | `Integer \| nil` | `nil` |
|
|
1075
|
+
| `on_deprecation` | `Proc \| nil` | `nil` (writes to stderr) |
|
|
1076
|
+
| `on_hook_error` | `Proc \| nil` | `nil` (warns to stderr) |
|
|
1077
|
+
| `active_support_notifications` | `Boolean` | `false` |
|
|
1078
|
+
| `statsd_client` | `Object \| nil` | `nil` |
|
|
1079
|
+
| `opentelemetry_tracer` | `Object \| nil` | `nil` |
|
|
1080
|
+
|
|
1081
|
+
### Opt-in extensions (not guaranteed until their owning milestone ships)
|
|
1082
|
+
|
|
1083
|
+
The following are available now but reside under `require "safe_memoize/rails"` and are not covered by the v1.0.0 semver guarantee until the v1.x milestone that owns them is declared stable:
|
|
1084
|
+
|
|
1085
|
+
- `SafeMemoize::Rails` module (`track`, `reset_tracked!`)
|
|
1086
|
+
- `SafeMemoize::Rails::RequestScoped` concern
|
|
1087
|
+
- `SafeMemoize::Rails::Middleware` Rack middleware
|
|
1088
|
+
- `SafeMemoize::Adapters::StatsD`
|
|
1089
|
+
- `SafeMemoize::Adapters::OpenTelemetry`
|
|
1090
|
+
|
|
1091
|
+
## Ruby version support
|
|
1092
|
+
|
|
1093
|
+
### Supported versions
|
|
1094
|
+
|
|
1095
|
+
SafeMemoize requires **Ruby ≥ 3.3**. Every non-EOL Ruby version in the table below is actively tested in CI and receives bug-fix backports for critical issues.
|
|
1096
|
+
|
|
1097
|
+
| Ruby | Status | EOL |
|
|
1098
|
+
|---|---|---|
|
|
1099
|
+
| 3.3 | Supported | Mar 2027 |
|
|
1100
|
+
| 3.4 | Supported | Mar 2028 |
|
|
1101
|
+
| 4.0 | Supported | ~ Dec 2028 |
|
|
1102
|
+
|
|
1103
|
+
EOL dates follow the [Ruby maintenance schedule](https://www.ruby-lang.org/en/downloads/branches/).
|
|
1104
|
+
|
|
1105
|
+
### Policy
|
|
1106
|
+
|
|
1107
|
+
- **Dropping an EOL version is a minor-version change**, not a major one — it will appear in the CHANGELOG under `### Removed` and the gemspec `required_ruby_version` will be updated accordingly.
|
|
1108
|
+
- SafeMemoize targets the **current stable release plus the two previous non-EOL minors** at any given time. When Ruby releases a new version in December, CI gains a new column; when a version reaches EOL the next minor release removes it.
|
|
1109
|
+
- **No patch release will ever raise the minimum Ruby version.** Only `x.y.0` minor releases may do so.
|
|
1110
|
+
- Prerelease Rubies (dev / preview builds) are not officially supported but breakage is investigated on a best-effort basis.
|
|
1111
|
+
|
|
1112
|
+
### History
|
|
1113
|
+
|
|
1114
|
+
| Dropped in | Ruby version removed |
|
|
1115
|
+
|---|---|
|
|
1116
|
+
| v0.5.0 | Ruby 3.2 (reached EOL) |
|
|
1117
|
+
|
|
939
1118
|
## Roadmap
|
|
940
1119
|
|
|
941
|
-
See [ROADMAP.md](ROADMAP.md) for the planned path
|
|
1120
|
+
See [ROADMAP.md](ROADMAP.md) for the planned path to v1.0.0 and beyond, including upcoming features, API stability goals, and the versioning policy.
|
|
942
1121
|
|
|
943
1122
|
## Contributing
|
|
944
1123
|
|
data/ROADMAP.md
CHANGED
|
@@ -4,34 +4,19 @@ This document tracks the planned evolution of SafeMemoize through v1.0.0 and bey
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## v0.9.0 — Observability & Ecosystem Integration
|
|
8
|
-
|
|
9
|
-
*Goal: make SafeMemoize a first-class citizen in Rails/ActiveSupport stacks and in observability pipelines.*
|
|
10
|
-
|
|
11
|
-
| Feature | Description | Status |
|
|
12
|
-
|---|---|---|
|
|
13
|
-
| ActiveSupport::Notifications integration | Emit `cache.hit`, `cache.miss`, `cache.evict`, and `cache.expire` events when ActiveSupport is available (opt-in via configuration) | Shipped |
|
|
14
|
-
| StatsD adapter | Thin optional module (`SafeMemoize::Adapters::StatsD`) that routes lifecycle hooks to a StatsD client with sensible metric names and tags | Shipped |
|
|
15
|
-
| OpenTelemetry spans | Optional adapter (`SafeMemoize::Adapters::OpenTelemetry`) wrapping computation time in a trace span for distributed tracing pipelines | Shipped |
|
|
16
|
-
| Rails request-scope helper | Guide + optional mixin for resetting instance memos at the end of each request (controller concern, middleware, or Active Model pattern) | Shipped |
|
|
17
|
-
| Formal benchmark suite | `benchmarks/` directory with comparisons against `memery`, `memo_wise`, and raw `||=`, covering single-threaded throughput and contention under concurrent load | Shipped |
|
|
18
|
-
| Concurrency stress tests | Dedicated spec suite hammering shared-cache paths and LRU eviction under high thread counts to surface race conditions | Shipped |
|
|
19
|
-
|
|
20
|
-
---
|
|
21
|
-
|
|
22
7
|
## v1.0.0 — Stable API
|
|
23
8
|
|
|
24
9
|
*Goal: declare a stable, semver-governed public API that downstream code can depend on with confidence.*
|
|
25
10
|
|
|
26
11
|
| Feature | Description | Status |
|
|
27
12
|
|---|---|---|
|
|
28
|
-
| Semantic versioning guarantee | Document which constants, methods, and option keys are public API; breaking changes require a major bump henceforth |
|
|
29
|
-
| Complete RBS + Sorbet signatures | Cover all public methods including overloads for optional keyword arguments; publish `.rbi` stubs as a companion package if demand warrants |
|
|
30
|
-
| Full API reference | Generated documentation hosted on RubyDoc or a dedicated docs site; all public methods documented with parameter types, return values, and usage examples |
|
|
31
|
-
| Ractor compatibility audit | Investigate and either support Ractor-compatible operation (Mutex replacement, shared-cache storage) or document the limitation clearly |
|
|
32
|
-
| Ruby version policy | Formalise the supported Ruby version window and cadence for dropping EOL versions |
|
|
33
|
-
| Deprecation sweep | Resolve or formally deprecate any unstable internal APIs before the stable release |
|
|
34
|
-
| Upgrade guide | Document all breaking changes from 0.x and provide a migration path for users of deprecated behaviour |
|
|
13
|
+
| Semantic versioning guarantee | Document which constants, methods, and option keys are public API; breaking changes require a major bump henceforth | Shipped |
|
|
14
|
+
| Complete RBS + Sorbet signatures | Cover all public methods including overloads for optional keyword arguments; publish `.rbi` stubs as a companion package if demand warrants | Shipped |
|
|
15
|
+
| Full API reference | Generated documentation hosted on RubyDoc or a dedicated docs site; all public methods documented with parameter types, return values, and usage examples | Shipped |
|
|
16
|
+
| Ractor compatibility audit | Investigate and either support Ractor-compatible operation (Mutex replacement, shared-cache storage) or document the limitation clearly | Shipped |
|
|
17
|
+
| Ruby version policy | Formalise the supported Ruby version window and cadence for dropping EOL versions | Shipped |
|
|
18
|
+
| Deprecation sweep | Resolve or formally deprecate any unstable internal APIs before the stable release | Shipped |
|
|
19
|
+
| Upgrade guide | Document all breaking changes from 0.x and provide a migration path for users of deprecated behaviour | Shipped |
|
|
35
20
|
|
|
36
21
|
---
|
|
37
22
|
|
data/Rakefile
CHANGED
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Upgrading to SafeMemoize 1.0.0
|
|
2
|
+
|
|
3
|
+
This guide covers every behavioral change introduced across the 0.x series that could affect code written against an earlier release. Work through the sections that apply to your starting version.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Summary of breaking changes
|
|
8
|
+
|
|
9
|
+
| Change | Introduced | Impact |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Ruby 3.2 dropped | v0.5.0 | High if still on Ruby 3.2 |
|
|
12
|
+
| TTL clock starts at first call, not definition | v0.6.0 | Medium — affects TTL precision |
|
|
13
|
+
| `memo_keys`/`memo_values` shape for custom keys | v0.6.1 | Low — only if inspecting key metadata |
|
|
14
|
+
| `memoize` raises at definition time for undefined methods | v0.8.0 | Medium — affects dynamic class construction |
|
|
15
|
+
| Argument mutation no longer corrupts cache | v0.8.0 | Low — was silent bug; may surface latent test issues |
|
|
16
|
+
| Hook exceptions no longer propagate | v0.8.0 | Medium — only if catching hook exceptions |
|
|
17
|
+
| `memoized?` / introspection methods now respect custom keys | v1.0.0 | Low — bug fix; only if using custom keys |
|
|
18
|
+
| `reset_memo` with args and `memo_refresh` respect custom keys | v1.0.0 | Low — bug fix; only if using custom keys |
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Upgrading from 0.1.x or 0.2.x
|
|
23
|
+
|
|
24
|
+
Follow every section below in order.
|
|
25
|
+
|
|
26
|
+
## Upgrading from 0.3.x – 0.4.x
|
|
27
|
+
|
|
28
|
+
Follow sections starting from [Ruby 3.2 dropped](#ruby-32-dropped-v050).
|
|
29
|
+
|
|
30
|
+
## Upgrading from 0.5.x
|
|
31
|
+
|
|
32
|
+
Follow sections starting from [TTL clock fix](#ttl-clock-now-starts-at-first-call-v060).
|
|
33
|
+
|
|
34
|
+
## Upgrading from 0.6.x
|
|
35
|
+
|
|
36
|
+
Follow sections starting from [memoize on undefined methods](#memoize-on-an-undefined-method-raises-at-definition-time-v080).
|
|
37
|
+
|
|
38
|
+
## Upgrading from 0.7.x – 0.9.x
|
|
39
|
+
|
|
40
|
+
Follow sections starting from [custom key introspection fix](#introspection-methods-now-respect-custom-keys-v100).
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Ruby 3.2 dropped (v0.5.0)
|
|
45
|
+
|
|
46
|
+
**Impact:** High — the gem will not install on Ruby 3.2.
|
|
47
|
+
|
|
48
|
+
Ruby 3.2 reached end-of-life and was removed from the supported matrix in v0.5.0. The gemspec now enforces `ruby >= 3.3.0`.
|
|
49
|
+
|
|
50
|
+
**Migration:** Upgrade to Ruby 3.3 or later before updating the gem.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## TTL clock now starts at first call, not definition (v0.6.0)
|
|
55
|
+
|
|
56
|
+
**Impact:** Medium — affects how long entries actually live in the cache.
|
|
57
|
+
|
|
58
|
+
Before v0.6.0, the TTL countdown began when `memoize :method, ttl: N` was evaluated (class load time). If a class was loaded 30 seconds before the first call, entries would expire 30 seconds earlier than expected.
|
|
59
|
+
|
|
60
|
+
From v0.6.0 onwards, the clock starts on the first cache write — the entry lives for exactly `ttl` seconds after it is populated.
|
|
61
|
+
|
|
62
|
+
**Migration:** No code change required. Entries now live for the duration you specified. If you intentionally set very short TTLs and relied on the early-start behaviour (unlikely), reduce your TTL value accordingly.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## `memo_keys`/`memo_values` shape change for custom-keyed entries (v0.6.1)
|
|
67
|
+
|
|
68
|
+
**Impact:** Low — only affects code that reads the hashes returned by these methods.
|
|
69
|
+
|
|
70
|
+
Before v0.6.1, entries stored via `memoize_with_custom_key` were surfaced in `memo_keys` and `memo_values` as:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
{ args: <the_custom_key>, kwargs: nil }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
From v0.6.1 onwards they are correctly surfaced as:
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
{ custom_key: <the_custom_key> }
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Migration:** If your code inspects the hash returned by `memo_keys` or `memo_values` and checks for an `:args` key to detect custom-keyed entries, update it to check for `:custom_key` instead:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
# Before
|
|
86
|
+
entry[:args] # custom key was smuggled here
|
|
87
|
+
|
|
88
|
+
# After
|
|
89
|
+
entry[:custom_key] # explicit field
|
|
90
|
+
entry[:args] # only present for default-keyed entries
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## `memoize` on an undefined method raises at definition time (v0.8.0)
|
|
96
|
+
|
|
97
|
+
**Impact:** Medium — affects any code that calls `memoize` before the method is defined,
|
|
98
|
+
or that dynamically defines methods after `memoize` is called.
|
|
99
|
+
|
|
100
|
+
Before v0.8.0, calling `memoize :missing_method` silently succeeded at class load time. The error only appeared at runtime when the memoized wrapper tried to call `super` and found nothing to call.
|
|
101
|
+
|
|
102
|
+
From v0.8.0 onwards, `memoize` raises `ArgumentError` immediately if the named method does not exist at the time of the call.
|
|
103
|
+
|
|
104
|
+
**Migration:** Ensure `memoize` is always called *after* the method it wraps:
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Wrong — memoize called before def (raises ArgumentError in v0.8.0+)
|
|
108
|
+
memoize :compute
|
|
109
|
+
def compute = expensive_work
|
|
110
|
+
|
|
111
|
+
# Correct
|
|
112
|
+
def compute = expensive_work
|
|
113
|
+
memoize :compute
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If you use `Module#prepend` or `include` to add methods dynamically, make sure `memoize` is called after the module is prepended/included:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
include MyMethods # defines :compute
|
|
120
|
+
memoize :compute # now safe
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Argument mutation no longer corrupts the cache (v0.8.0)
|
|
126
|
+
|
|
127
|
+
**Impact:** Low — this was a silent bug. Existing code is unlikely to rely on it, but test suites that mutate arguments after a call and expect a cache miss may need updating.
|
|
128
|
+
|
|
129
|
+
Before v0.8.0, mutable cache keys (arrays, hashes, strings passed as arguments) were stored by reference. Mutating an argument after a call could cause the cache to behave unpredictably — sometimes missing on identical arguments, sometimes returning stale values.
|
|
130
|
+
|
|
131
|
+
From v0.8.0 onwards, argument arrays, hashes, and strings are deep-frozen into an independent copy when the cache key is built. Callers can mutate their arguments after a call without affecting the cache.
|
|
132
|
+
|
|
133
|
+
**Migration:** No migration required for production code. If a test mutates arguments after a memoized call and expects a cache miss, the test was relying on the buggy behaviour — update it to use distinct argument values instead.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Hook exceptions no longer propagate to callers (v0.8.0)
|
|
138
|
+
|
|
139
|
+
**Impact:** Medium — only affects code that wraps memoized calls in `rescue` to catch errors raised by hooks.
|
|
140
|
+
|
|
141
|
+
Before v0.8.0, an exception raised inside an `on_memo_hit`, `on_memo_miss`, `on_memo_store`, `on_memo_expire`, or `on_memo_evict` hook would propagate through the memoized method call and be visible to the caller.
|
|
142
|
+
|
|
143
|
+
From v0.8.0 onwards, hook exceptions are isolated. By default a `[SafeMemoize] Hook error in <type>: <message>` warning is written to `$stderr` and execution continues normally.
|
|
144
|
+
|
|
145
|
+
**Migration:** If you need hook exceptions to propagate (e.g. in a strict test environment), configure `on_hook_error` to re-raise:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
SafeMemoize.configure do |c|
|
|
149
|
+
c.on_hook_error = ->(error, hook_type, cache_key) { raise error }
|
|
150
|
+
end
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Or to route them to your error tracker without raising:
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
SafeMemoize.configure do |c|
|
|
157
|
+
c.on_hook_error = ->(error, _type, _key) { Bugsnag.notify(error) }
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Introspection methods now respect custom keys (v1.0.0)
|
|
164
|
+
|
|
165
|
+
**Impact:** Low — only affects code that uses `memoize_with_custom_key` or the `key:` option together with any of the introspection methods listed below.
|
|
166
|
+
|
|
167
|
+
Before v1.0.0, the following methods looked up cache entries using the *default* key (derived from raw arguments) rather than the *custom* key generator. This meant they always returned incorrect results when a custom key was active:
|
|
168
|
+
|
|
169
|
+
- `memoized?`
|
|
170
|
+
- `memo_ttl_remaining`
|
|
171
|
+
- `memo_touch`
|
|
172
|
+
- `memo_age`
|
|
173
|
+
- `memo_stale?`
|
|
174
|
+
|
|
175
|
+
From v1.0.0 onwards, all five methods correctly call `compute_cache_key`, which checks for a custom key generator first.
|
|
176
|
+
|
|
177
|
+
**Migration:** No code change required — the methods now return correct results. If your code had workarounds that bypassed these methods (e.g. manually inspecting `memo_keys` to determine if an entry existed), you can simplify them to use `memoized?` directly.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## `reset_memo` with args and `memo_refresh` respect custom keys (v1.0.0)
|
|
182
|
+
|
|
183
|
+
**Impact:** Low — only affects code that uses custom keys and calls `reset_memo` with specific arguments, or calls `memo_refresh`.
|
|
184
|
+
|
|
185
|
+
Before v1.0.0, `reset_memo(:method, *args)` with explicit arguments built a default key from raw args to match entries. When the method used a custom key generator, no entry was found and nothing was cleared. `memo_refresh` inherited the same flaw — the old entry survived and `refresh` returned the cached (stale) value instead of recomputing.
|
|
186
|
+
|
|
187
|
+
From v1.0.0 onwards, both methods resolve the cache key through `compute_cache_key`.
|
|
188
|
+
|
|
189
|
+
**Migration:** No code change required — the methods now behave correctly. Note that `reset_memo(:method)` with *no* arguments still clears all entries for the method regardless of key format; this behaviour is unchanged.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## New stable API
|
|
194
|
+
|
|
195
|
+
All symbols listed in the README `## Public API and versioning guarantee` section are now covered by [Semantic Versioning](https://semver.org/) from v1.0.0 onwards. Breaking changes to any of those symbols require a major version bump.
|
|
196
|
+
|
|
197
|
+
Opt-in extensions (`SafeMemoize::Rails`, `SafeMemoize::Adapters::StatsD`, `SafeMemoize::Adapters::OpenTelemetry`) are available but are *not* included in the v1.0.0 semver guarantee; they will be stabilised in a subsequent minor release.
|
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SafeMemoize
|
|
4
|
+
# Optional adapters for external observability systems.
|
|
5
|
+
# None are auto-required; load them explicitly when needed.
|
|
4
6
|
module Adapters
|
|
7
|
+
# Optional OpenTelemetry adapter.
|
|
8
|
+
#
|
|
9
|
+
# Wraps each cache-miss computation in an OpenTelemetry span so the time
|
|
10
|
+
# spent computing uncached values is visible in distributed traces.
|
|
11
|
+
#
|
|
12
|
+
# Configure via {Configuration#opentelemetry_tracer}:
|
|
13
|
+
#
|
|
14
|
+
# SafeMemoize.configure do |c|
|
|
15
|
+
# c.opentelemetry_tracer = OpenTelemetry.tracer_provider.tracer("safe_memoize")
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Each span is named {SPAN_NAME} and carries the attributes
|
|
19
|
+
# +safe_memoize.method+, +safe_memoize.class+, and +safe_memoize.cache_hit+.
|
|
20
|
+
# Falls back to untraced execution when no tracer is configured or the tracer
|
|
21
|
+
# does not respond to +#in_span+.
|
|
5
22
|
module OpenTelemetry
|
|
23
|
+
# The name given to every span created by this adapter.
|
|
6
24
|
SPAN_NAME = "safe_memoize.compute"
|
|
7
25
|
|
|
26
|
+
# Wraps the block in a span if a tracer is available; otherwise yields directly.
|
|
27
|
+
#
|
|
28
|
+
# @param tracer [Object, nil] an object responding to +#in_span+
|
|
29
|
+
# @param method_name [Symbol, String]
|
|
30
|
+
# @param class_name [String, nil]
|
|
31
|
+
# @yield the computation block (cache-miss path)
|
|
32
|
+
# @return [Object] the result of the block
|
|
8
33
|
def self.trace(tracer, method_name, class_name)
|
|
9
34
|
return yield unless tracer&.respond_to?(:in_span)
|
|
10
35
|
|
|
@@ -2,7 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module SafeMemoize
|
|
4
4
|
module Adapters
|
|
5
|
+
# Optional StatsD adapter.
|
|
6
|
+
#
|
|
7
|
+
# Routes SafeMemoize lifecycle events to any StatsD-compatible client
|
|
8
|
+
# (any object that responds to +#increment+).
|
|
9
|
+
#
|
|
10
|
+
# Configure via {Configuration#statsd_client}:
|
|
11
|
+
#
|
|
12
|
+
# SafeMemoize.configure do |c|
|
|
13
|
+
# c.statsd_client = Datadog::Statsd.new
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Emitted metrics:
|
|
17
|
+
#
|
|
18
|
+
# | Metric | Fires on |
|
|
19
|
+
# |---|---|
|
|
20
|
+
# | +safe_memoize.hit+ | cache hit |
|
|
21
|
+
# | +safe_memoize.miss+ | cache miss |
|
|
22
|
+
# | +safe_memoize.store+ | value written |
|
|
23
|
+
# | +safe_memoize.evict+ | LRU eviction |
|
|
24
|
+
# | +safe_memoize.expire+ | TTL expiration |
|
|
25
|
+
#
|
|
26
|
+
# Every metric is tagged with +method:<name>+ and +class:<name>+.
|
|
27
|
+
# Client errors are rescued and warned rather than raised.
|
|
5
28
|
module StatsD
|
|
29
|
+
# @api private
|
|
6
30
|
METRIC_NAMES = {
|
|
7
31
|
on_hit: "safe_memoize.hit",
|
|
8
32
|
on_miss: "safe_memoize.miss",
|
|
@@ -11,6 +35,13 @@ module SafeMemoize
|
|
|
11
35
|
on_store: "safe_memoize.store"
|
|
12
36
|
}.freeze
|
|
13
37
|
|
|
38
|
+
# Dispatches a lifecycle event to the StatsD client.
|
|
39
|
+
#
|
|
40
|
+
# @param client [Object] a StatsD-compatible client responding to +#increment+
|
|
41
|
+
# @param hook_type [Symbol] one of the keys in {METRIC_NAMES}
|
|
42
|
+
# @param cache_key [Array] the internal cache key (first element is the method name)
|
|
43
|
+
# @param class_name [String, nil]
|
|
44
|
+
# @return [void]
|
|
14
45
|
def self.dispatch(client, hook_type, cache_key, class_name)
|
|
15
46
|
metric = METRIC_NAMES[hook_type]
|
|
16
47
|
return unless metric
|