rails_named_cache 0.1.0 → 0.2.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 +11 -1
- data/README.md +95 -5
- data/lib/generators/rails_named_cache/install/templates/rails_named_cache.rb +7 -0
- data/lib/rails_named_cache/logging.rb +70 -0
- data/lib/rails_named_cache/railtie.rb +5 -0
- data/lib/rails_named_cache/version.rb +1 -1
- data/lib/rails_named_cache.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7421605a4a8c9d7fb63fec7fb51a2376cf7fd8e58a0d7b30bf56a8a8f29d2509
|
|
4
|
+
data.tar.gz: 4c48647f995d27409ff4f6319e18f11b80557ea7d8bd67a75c2643f2212747e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b3397d1941abb8a4a3b4b71f8e8f52234e58efd3929702b92226923950260268b321e73117497f139ce4c3a9734a28a81728daf97a212b377982568f7c7167de
|
|
7
|
+
data.tar.gz: e1d39f9714e606be99ec3dcc1c126b41c936efb9a8b521ac198de7ea9be7eb222e988d4597a8b8d6524d2f7e016c3504dbe4a8f5c5e18e2185644436b2a2ecc3
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,15 @@ All notable changes to this project are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-08-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Hit/miss logging: every `cache_read.active_support` notification is logged at `debug` level as
|
|
14
|
+
`Cache hit: <key>`. Configured with `config.named_cache_logger` — `Rails.logger`
|
|
15
|
+
by default, `false` to disable — and available outside Rails as
|
|
16
|
+
`RailsNamedCache::Logging.subscribe(logger)`.
|
|
17
|
+
|
|
9
18
|
## [0.1.0] - 2026-07-30
|
|
10
19
|
|
|
11
20
|
### Added
|
|
@@ -20,5 +29,6 @@ All notable changes to this project are documented here. The format follows
|
|
|
20
29
|
`MemoryStore` registered as `:memory`.
|
|
21
30
|
- Support for Rails 7.0 through 8.1 on Ruby 3.3 and 3.4.
|
|
22
31
|
|
|
23
|
-
[Unreleased]: https://github.com/igorkasyanchuk/rails_named_cache/compare/v0.
|
|
32
|
+
[Unreleased]: https://github.com/igorkasyanchuk/rails_named_cache/compare/v0.2.0...HEAD
|
|
33
|
+
[0.2.0]: https://github.com/igorkasyanchuk/rails_named_cache/compare/v0.1.0...v0.2.0
|
|
24
34
|
[0.1.0]: https://github.com/igorkasyanchuk/rails_named_cache/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -22,7 +22,7 @@ Rails.cache(:redis).fetch("session") { ... } # shared across servers
|
|
|
22
22
|
Stores live in `config/` instead of global constants; `Rails.cache` is untouched.
|
|
23
23
|
|
|
24
24
|
**Contents:** [Install](#installation) · [Configuration](#configuration) · [Usage](#usage) ·
|
|
25
|
-
[Expiration](#expiration) · [L1/L2](#l1l2-memory-in-front-of-redis) ·
|
|
25
|
+
[Expiration](#expiration) · [Logging](#hitmiss-logging) · [L1/L2](#l1l2-memory-in-front-of-redis) ·
|
|
26
26
|
[MemoryStore speed](#why-memorystore-is-so-fast) · [Testing](#testing) ·
|
|
27
27
|
[Advanced](#advanced-configuration) · [FAQ](#faq) · [Be careful](#be-careful) ·
|
|
28
28
|
[Compatibility](#compatibility)
|
|
@@ -117,6 +117,40 @@ Rails.cache(:pricing).fetch("report", expires_at: Date.tomorrow.midnight) { buil
|
|
|
117
117
|
|
|
118
118
|
No `expires_in` on a store means entries live until overwritten, deleted, or evicted by the backend.
|
|
119
119
|
|
|
120
|
+
## Hit/miss logging
|
|
121
|
+
|
|
122
|
+
Every cache read is logged to `Rails.logger` at `debug` level — nothing to configure. It lands in
|
|
123
|
+
the app log next to the rest of the request, `log/development.log` locally, and stays quiet in
|
|
124
|
+
production, where the level is `info`:
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
Cache hit: users/1
|
|
128
|
+
Cache miss: pricing:eur
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To turn it off, or to send it somewhere other than the app log:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
config.named_cache_logger = false # log nothing
|
|
135
|
+
config.named_cache_logger = Logger.new(Rails.root.join("log/cache.log")) # a separate file
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Two things to know:
|
|
139
|
+
|
|
140
|
+
- This is a subscriber on the `cache_read.active_support` notification ActiveSupport already
|
|
141
|
+
emits — no store is wrapped, and reads through the default `Rails.cache` are logged too.
|
|
142
|
+
- The line carries the key, not the store: ActiveSupport's payload has nothing that identifies
|
|
143
|
+
the instance a read came from. On Rails 7.2 and up the key is the normalized one, so give
|
|
144
|
+
stores a `namespace:` and its prefix tells them apart. Rails 7.0 and 7.1 report the key as the
|
|
145
|
+
caller wrote it, without the namespace.
|
|
146
|
+
|
|
147
|
+
Outside Rails, subscribe by hand:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
RailsNamedCache::Logging.subscribe(my_logger)
|
|
151
|
+
RailsNamedCache::Logging.unsubscribe
|
|
152
|
+
```
|
|
153
|
+
|
|
120
154
|
## L1/L2: memory in front of Redis
|
|
121
155
|
|
|
122
156
|
Not a gem feature — two named stores and two nested `fetch` calls:
|
|
@@ -171,18 +205,73 @@ data; use a shared store for anything that must agree across processes.
|
|
|
171
205
|
|
|
172
206
|
## Testing
|
|
173
207
|
|
|
208
|
+
**Same names and the same backends in test — only the target changes.** Every name the code calls
|
|
209
|
+
has to exist in every environment, since a missing one raises `UnknownStoreError` and never falls
|
|
210
|
+
back. Keep each store on the backend it uses in production, pointed at a test-only Redis database,
|
|
211
|
+
so a `:redis` store is always Redis and tests exercise real serialization and expiry:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
# config/initializers/rails_named_cache.rb
|
|
215
|
+
Rails.application.configure do
|
|
216
|
+
# a separate Redis database in test, so a suite can never touch development keys
|
|
217
|
+
redis_url = Rails.env.test? ? ENV.fetch("REDIS_TEST_URL") : ENV.fetch("REDIS_URL")
|
|
218
|
+
|
|
219
|
+
config.named_cache_stores = {
|
|
220
|
+
memory: ActiveSupport::Cache::MemoryStore.new(size: 1.megabyte, expires_in: 1.day),
|
|
221
|
+
pricing: [:redis_cache_store, { url: redis_url, namespace: "pricing" }],
|
|
222
|
+
redis: [:redis_cache_store, { url: redis_url, namespace: "app" }]
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
> **Namespace every Redis store.** `RedisCacheStore#clear` runs `FLUSHDB` when the store has no
|
|
228
|
+
> namespace, so the clear-between-examples hook below would wipe the whole database — including
|
|
229
|
+
> Sidekiq queues or a colleague's development data if the suite ever runs against a shared server.
|
|
230
|
+
> With a namespace, `clear` deletes only that store's keys.
|
|
231
|
+
|
|
232
|
+
To disable one domain's caching in test, register a `NullStore` for that name — see the per-example
|
|
233
|
+
swap below — rather than backing a `:redis` name with a memory store, which makes the suite pass
|
|
234
|
+
against behaviour production never runs.
|
|
235
|
+
|
|
236
|
+
> Initializers load *after* `config/environments/*.rb`, so an initializer that assigns
|
|
237
|
+
> `config.named_cache_stores` silently overwrites whatever `config/environments/test.rb` set. Pick
|
|
238
|
+
> one: branch on `Rails.env` in the initializer, as above, or delete the initializer and configure
|
|
239
|
+
> each environment file.
|
|
240
|
+
|
|
241
|
+
**Clear every store between examples**, so cached state cannot leak from one example to the next:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
config.before { RailsNamedCache.names.each { |name| Rails.cache(name).clear } }
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Do not call `RailsNamedCache.reset!` in an application suite — registration happens once at boot,
|
|
248
|
+
so an emptied registry stays empty and every later lookup raises. It exists for testing this gem.
|
|
249
|
+
|
|
250
|
+
**Swap one store for one example:**
|
|
251
|
+
|
|
174
252
|
```ruby
|
|
175
|
-
# disable caching for one domain, restore after
|
|
176
253
|
config.around(:each, :no_pricing_cache) do |example|
|
|
177
254
|
original = Rails.cache(:pricing)
|
|
178
255
|
RailsNamedCache.register(:pricing, ActiveSupport::Cache::NullStore.new)
|
|
179
256
|
example.run
|
|
180
257
|
RailsNamedCache.register(:pricing, original)
|
|
181
258
|
end
|
|
259
|
+
```
|
|
182
260
|
|
|
183
|
-
|
|
261
|
+
Re-registering is better than stubbing `Rails.cache`: a `receive(:cache).with(:pricing)` stub also
|
|
262
|
+
intercepts the no-argument calls that Rails itself makes.
|
|
263
|
+
|
|
264
|
+
**Assert against the store directly** — it is a real store, so no mocks are needed:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
it "caches the computed price" do
|
|
268
|
+
expect { PriceCalculator.call(product) }
|
|
269
|
+
.to change { Rails.cache(:pricing).exist?("product/#{product.id}") }.from(false).to(true)
|
|
270
|
+
end
|
|
184
271
|
```
|
|
185
272
|
|
|
273
|
+
Minitest is the same, from `setup` / `teardown` in `ActiveSupport::TestCase`.
|
|
274
|
+
|
|
186
275
|
## Advanced configuration
|
|
187
276
|
|
|
188
277
|
### Boot order
|
|
@@ -311,8 +400,9 @@ Named stores make it easy to add caches, which makes it easy to add these:
|
|
|
311
400
|
- **No cross-process invalidation.** `Rails.cache(:memory).delete(key)` and `.clear` affect the
|
|
312
401
|
calling process only — a console or rake task cannot flush the running workers. Anything that
|
|
313
402
|
needs a coordinated purge belongs in a shared backend.
|
|
314
|
-
- **Two stores on one backend share one keyspace**, and `clear` on either wipes both.
|
|
315
|
-
|
|
403
|
+
- **Two stores on one backend share one keyspace**, and `clear` on either wipes both. Worse, a
|
|
404
|
+
`RedisCacheStore` with no `namespace` implements `clear` as `FLUSHDB` — the whole database, not
|
|
405
|
+
just your keys. See [Isolating stores that share a backend](#isolating-stores-that-share-a-backend).
|
|
316
406
|
- **No per-request local cache.** Repeated reads of the same key in one request hit a named store's
|
|
317
407
|
backend every time. On a Redis-backed store, wrap the work in `with_local_cache` or put a
|
|
318
408
|
[memory layer in front](#l1l2-memory-in-front-of-redis).
|
|
@@ -35,4 +35,11 @@ Rails.application.configure do
|
|
|
35
35
|
# Caches nothing. Useful per environment, e.g. in config/environments/test.rb.
|
|
36
36
|
# null: ActiveSupport::Cache::NullStore.new
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
# Cache hits and misses go to the app log (Rails.logger) at debug level:
|
|
40
|
+
# visible in development, quiet in production. Turn it off, or send it to its
|
|
41
|
+
# own file, with:
|
|
42
|
+
#
|
|
43
|
+
# config.named_cache_logger = false
|
|
44
|
+
# config.named_cache_logger = Logger.new(Rails.root.join("log/cache.log"))
|
|
38
45
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module RailsNamedCache
|
|
6
|
+
# Logs cache hits and misses at +debug+ level.
|
|
7
|
+
#
|
|
8
|
+
# It subscribes to the +cache_read.active_support+ notification ActiveSupport
|
|
9
|
+
# already emits, so no store is wrapped and no cache API is added:
|
|
10
|
+
#
|
|
11
|
+
# Cache hit: users/1
|
|
12
|
+
# Cache miss: pricing:eur
|
|
13
|
+
#
|
|
14
|
+
# Every read is logged, named or not — the notification is global. On Rails
|
|
15
|
+
# 7.2 and up the key is the normalized one, so a store's +namespace:+ shows up
|
|
16
|
+
# as its prefix; earlier versions instrument the name the caller passed.
|
|
17
|
+
module Logging
|
|
18
|
+
# The only notification carrying +:hit+.
|
|
19
|
+
EVENT = "cache_read.active_support"
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
# @return [Object, nil] the notification subscriber, or +nil+ when not subscribed
|
|
23
|
+
attr_reader :subscriber
|
|
24
|
+
|
|
25
|
+
# Starts logging. Idempotent.
|
|
26
|
+
#
|
|
27
|
+
# @param logger [Logger, nil, false] +nil+ resolves +Rails.logger+ per event,
|
|
28
|
+
# +false+ disables logging entirely
|
|
29
|
+
# @return [Object, nil] the subscriber, or +nil+ when disabled or already subscribed
|
|
30
|
+
def subscribe(logger = nil)
|
|
31
|
+
return if logger == false || subscribed?
|
|
32
|
+
|
|
33
|
+
@logger = logger
|
|
34
|
+
@subscriber = ActiveSupport::Notifications.subscribe(EVENT) do |_name, _start, _finish, _id, payload|
|
|
35
|
+
log(payload)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Stops logging. Intended for tests and reboots.
|
|
40
|
+
#
|
|
41
|
+
# @return [void]
|
|
42
|
+
def unsubscribe
|
|
43
|
+
return unless @subscriber
|
|
44
|
+
|
|
45
|
+
ActiveSupport::Notifications.unsubscribe(@subscriber)
|
|
46
|
+
@subscriber = nil
|
|
47
|
+
@logger = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Boolean]
|
|
51
|
+
def subscribed?
|
|
52
|
+
!@subscriber.nil?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [Logger, nil] the configured logger, or +Rails.logger+
|
|
56
|
+
def logger
|
|
57
|
+
@logger || (::Rails.logger if defined?(::Rails))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# @param payload [Hash] the notification payload
|
|
63
|
+
# @return [void]
|
|
64
|
+
def log(payload)
|
|
65
|
+
# Block form: nothing is built when the logger is above debug level.
|
|
66
|
+
logger&.debug { "Cache #{payload[:hit] ? "hit" : "miss"}: #{payload[:key]}" }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -8,13 +8,18 @@ module RailsNamedCache
|
|
|
8
8
|
# Adds +config.named_cache_stores+ (default +{}+) and fills the {Registry}
|
|
9
9
|
# from it once, after +config/initializers+ have run and before eager loading,
|
|
10
10
|
# so eager-loaded code can already call +Rails.cache(:name)+.
|
|
11
|
+
#
|
|
12
|
+
# Also adds +config.named_cache_logger+ (default +nil+, meaning +Rails.logger+;
|
|
13
|
+
# set it to +false+ to log nothing) and subscribes {Logging} to it.
|
|
11
14
|
class Railtie < ::Rails::Railtie
|
|
12
15
|
config.named_cache_stores = {}
|
|
16
|
+
config.named_cache_logger = nil
|
|
13
17
|
|
|
14
18
|
initializer "rails_named_cache.register_stores", after: :load_config_initializers do |app|
|
|
15
19
|
# Registration is by name, so manual RailsNamedCache.register calls made
|
|
16
20
|
# from config/initializers survive unless config names the same store.
|
|
17
21
|
RailsNamedCache.load_configuration(app.config.named_cache_stores)
|
|
22
|
+
RailsNamedCache::Logging.subscribe(app.config.named_cache_logger)
|
|
18
23
|
end
|
|
19
24
|
end
|
|
20
25
|
end
|
data/lib/rails_named_cache.rb
CHANGED
|
@@ -6,6 +6,7 @@ require "active_support/cache"
|
|
|
6
6
|
require_relative "rails_named_cache/version"
|
|
7
7
|
require_relative "rails_named_cache/registry"
|
|
8
8
|
require_relative "rails_named_cache/configuration"
|
|
9
|
+
require_relative "rails_named_cache/logging"
|
|
9
10
|
require_relative "rails_named_cache/rails_ext"
|
|
10
11
|
|
|
11
12
|
# Named +ActiveSupport::Cache::Store+ instances for Rails applications.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_named_cache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Igor Kasyanchuk
|
|
@@ -88,6 +88,7 @@ files:
|
|
|
88
88
|
- lib/generators/rails_named_cache/install/templates/rails_named_cache.rb
|
|
89
89
|
- lib/rails_named_cache.rb
|
|
90
90
|
- lib/rails_named_cache/configuration.rb
|
|
91
|
+
- lib/rails_named_cache/logging.rb
|
|
91
92
|
- lib/rails_named_cache/rails_ext.rb
|
|
92
93
|
- lib/rails_named_cache/railtie.rb
|
|
93
94
|
- lib/rails_named_cache/registry.rb
|