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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '079d7672d3eb5ac855a1fd954bde9831ab34bef4f529cec6d7b17b393eb57629'
4
- data.tar.gz: 64193c678525fc4c28f358c8a60fe6c85d9236620025e66ffb317888650c00fa
3
+ metadata.gz: 7421605a4a8c9d7fb63fec7fb51a2376cf7fd8e58a0d7b30bf56a8a8f29d2509
4
+ data.tar.gz: 4c48647f995d27409ff4f6319e18f11b80557ea7d8bd67a75c2643f2212747e2
5
5
  SHA512:
6
- metadata.gz: b0ed82b9bad41e7607610f538d8df8a5d33bdf90e6eebf3ef7d19c1981aed08ad08a75eb798f64eda542a5a9bbcb38136f0974df901bdedc1bfd44a76f57ca3d
7
- data.tar.gz: 6cc671bb1a3a633813db29c0eb29104b40fb6b1bc94ff70fcf917fcba44b045e513c25b9a88a48ddb453d4b861cd8258f1c72d2ff253795b3ec9970676ccbe74
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.1.0...HEAD
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
- config.before { Rails.cache(:pricing).clear } # or clear one store between examples
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. See
315
- [Isolating stores that share a backend](#isolating-stores-that-share-a-backend).
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsNamedCache
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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.1.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