rails_named_cache 0.1.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 +7 -0
- data/CHANGELOG.md +24 -0
- data/LICENSE +21 -0
- data/README.md +364 -0
- data/lib/generators/rails_named_cache/install/install_generator.rb +22 -0
- data/lib/generators/rails_named_cache/install/templates/rails_named_cache.rb +38 -0
- data/lib/rails_named_cache/configuration.rb +74 -0
- data/lib/rails_named_cache/rails_ext.rb +25 -0
- data/lib/rails_named_cache/railtie.rb +20 -0
- data/lib/rails_named_cache/registry.rb +75 -0
- data/lib/rails_named_cache/version.rb +5 -0
- data/lib/rails_named_cache.rb +144 -0
- metadata +121 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '079d7672d3eb5ac855a1fd954bde9831ab34bef4f529cec6d7b17b393eb57629'
|
|
4
|
+
data.tar.gz: 64193c678525fc4c28f358c8a60fe6c85d9236620025e66ffb317888650c00fa
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b0ed82b9bad41e7607610f538d8df8a5d33bdf90e6eebf3ef7d19c1981aed08ad08a75eb798f64eda542a5a9bbcb38136f0974df901bdedc1bfd44a76f57ca3d
|
|
7
|
+
data.tar.gz: 6cc671bb1a3a633813db29c0eb29104b40fb6b1bc94ff70fcf917fcba44b045e513c25b9a88a48ddb453d4b861cd8258f1c72d2ff253795b3ec9970676ccbe74
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-07-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `Rails.cache(:name)` returns a named `ActiveSupport::Cache::Store`; `Rails.cache` is unchanged.
|
|
14
|
+
- `config.named_cache_stores` configuration, accepting store instances or anything
|
|
15
|
+
`ActiveSupport::Cache.lookup_store` understands.
|
|
16
|
+
- Thread-safe `RailsNamedCache::Registry` with O(1) lookups.
|
|
17
|
+
- `RailsNamedCache::UnknownStoreError`, which lists the available stores and never falls back.
|
|
18
|
+
`Rails.cache(nil)` raises `ConfigurationError` instead of returning the default store.
|
|
19
|
+
- `rails generate rails_named_cache:install` generator, creating an initializer with a 1 MB
|
|
20
|
+
`MemoryStore` registered as `:memory`.
|
|
21
|
+
- Support for Rails 7.0 through 8.1 on Ruby 3.3 and 3.4.
|
|
22
|
+
|
|
23
|
+
[Unreleased]: https://github.com/igorkasyanchuk/rails_named_cache/compare/v0.1.0...HEAD
|
|
24
|
+
[0.1.0]: https://github.com/igorkasyanchuk/rails_named_cache/releases/tag/v0.1.0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Kasyanchuk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# rails_named_cache
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/rails_named_cache)
|
|
4
|
+
[](https://github.com/igorkasyanchuk/rails_named_cache/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Multiple named cache stores for Rails, through `Rails.cache(:name)`.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
Rails.cache.fetch("users") { User.all } # default store, unchanged
|
|
11
|
+
Rails.cache(:memory).fetch("countries") { ... } # in-process, ~3 µs per read
|
|
12
|
+
Rails.cache(:redis).fetch("session") { ... } # shared across servers
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Why:**
|
|
16
|
+
|
|
17
|
+
- **Speed.** A named `MemoryStore` read is ~3 µs — no socket, no pool checkout, no wire protocol
|
|
18
|
+
([numbers](#why-memorystore-is-so-fast)).
|
|
19
|
+
- **Isolation.** Per-domain stores, so page-fragment churn can no longer evict the pricing table.
|
|
20
|
+
`Rails.cache(:pricing).clear` touches nothing else.
|
|
21
|
+
- **No new API.** `Rails.cache(:name)` returns the configured store itself — no wrapper, no proxy.
|
|
22
|
+
Stores live in `config/` instead of global constants; `Rails.cache` is untouched.
|
|
23
|
+
|
|
24
|
+
**Contents:** [Install](#installation) · [Configuration](#configuration) · [Usage](#usage) ·
|
|
25
|
+
[Expiration](#expiration) · [L1/L2](#l1l2-memory-in-front-of-redis) ·
|
|
26
|
+
[MemoryStore speed](#why-memorystore-is-so-fast) · [Testing](#testing) ·
|
|
27
|
+
[Advanced](#advanced-configuration) · [FAQ](#faq) · [Be careful](#be-careful) ·
|
|
28
|
+
[Compatibility](#compatibility)
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle add rails_named_cache
|
|
34
|
+
rails generate rails_named_cache:install # optional: a commented initializer to start from
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Ruby >= 3.3, Rails >= 7.0. Runtime deps are `activesupport`, `railties`, `concurrent-ruby`.
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
One setting. Values are any `ActiveSupport::Cache::Store`:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
# config/initializers/rails_named_cache.rb
|
|
45
|
+
Rails.application.configure do
|
|
46
|
+
config.named_cache_stores = {
|
|
47
|
+
memory: ActiveSupport::Cache::MemoryStore.new(size: 1.megabyte),
|
|
48
|
+
pricing: ActiveSupport::Cache::MemoryStore.new(size: 8.megabytes),
|
|
49
|
+
redis: ActiveSupport::Cache::RedisCacheStore.new(url: ENV.fetch("REDIS_URL")),
|
|
50
|
+
disk: ActiveSupport::Cache::FileStore.new(Rails.root.join("tmp/cache")),
|
|
51
|
+
null: ActiveSupport::Cache::NullStore.new
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Rails shorthand works too, resolved through `ActiveSupport::Cache.lookup_store`:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
config.named_cache_stores = {
|
|
60
|
+
memory: :memory_store,
|
|
61
|
+
redis: [:redis_cache_store, { url: ENV.fetch("REDIS_URL") }],
|
|
62
|
+
disk: [:file_store, "tmp/cache"]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Works from `config/application.rb` or any environment file too. An app that never sets it behaves
|
|
67
|
+
exactly as before. Per-environment stores, engines, boot ordering, error semantics and the registry
|
|
68
|
+
API are in [Advanced configuration](#advanced-configuration).
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
Rails.cache.fetch("key") { value } # default store, unchanged
|
|
74
|
+
Rails.cache(:memory).fetch("countries") { Country.all.to_a }
|
|
75
|
+
Rails.cache(:pricing).write("product/1", price)
|
|
76
|
+
Rails.cache(:null).fetch("anything") { computed } # never caches
|
|
77
|
+
|
|
78
|
+
store = Rails.cache(:memory) # a plain ActiveSupport::Cache::Store — full API, nothing wrapped
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Unknown and `nil` names raise instead of falling back to another store:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
Rails.cache(:unknown)
|
|
85
|
+
# RailsNamedCache::UnknownStoreError: Unknown named cache store :unknown
|
|
86
|
+
#
|
|
87
|
+
# Available stores:
|
|
88
|
+
# :memory
|
|
89
|
+
# :pricing
|
|
90
|
+
|
|
91
|
+
Rails.cache(nil)
|
|
92
|
+
# RailsNamedCache::ConfigurationError: Cache store name must be a non-empty Symbol or String, got nil
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`UnknownStoreError` exposes `#name` and `#available`.
|
|
96
|
+
|
|
97
|
+
## Expiration
|
|
98
|
+
|
|
99
|
+
Per-store defaults, so each domain gets its own policy:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
config.named_cache_stores = {
|
|
103
|
+
memory: ActiveSupport::Cache::MemoryStore.new(size: 1.megabyte, expires_in: 10.seconds),
|
|
104
|
+
pricing: [:redis_cache_store, { url: ENV.fetch("REDIS_URL"), expires_in: 1.hour }],
|
|
105
|
+
geocode: [:redis_cache_store, { url: ENV.fetch("REDIS_URL"), expires_in: 30.days }]
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Per-call overrides are ordinary ActiveSupport:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Rails.cache(:memory).fetch("flags", expires_in: 1.second) { Flag.enabled.to_a }
|
|
113
|
+
Rails.cache(:pricing).write("product/#{id}", price, expires_in: 30.seconds)
|
|
114
|
+
Rails.cache(:pricing).fetch("rate", expires_in: 1.hour, race_condition_ttl: 10.seconds) { fetch_rate }
|
|
115
|
+
Rails.cache(:pricing).fetch("report", expires_at: Date.tomorrow.midnight) { build_report }
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
No `expires_in` on a store means entries live until overwritten, deleted, or evicted by the backend.
|
|
119
|
+
|
|
120
|
+
## L1/L2: memory in front of Redis
|
|
121
|
+
|
|
122
|
+
Not a gem feature — two named stores and two nested `fetch` calls:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
# app/lib/layered_cache.rb
|
|
126
|
+
module LayeredCache
|
|
127
|
+
def self.fetch(key, l1_expires_in: 5.seconds, **options, &block)
|
|
128
|
+
Rails.cache(:memory).fetch(key, expires_in: l1_expires_in) do
|
|
129
|
+
Rails.cache(:redis).fetch(key, **options, &block)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.delete(key)
|
|
134
|
+
Rails.cache(:redis).delete(key)
|
|
135
|
+
Rails.cache(:memory).delete(key) # this process only
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
LayeredCache.fetch("countries", expires_in: 1.day) { Country.order(:name).to_a }
|
|
142
|
+
# L1 hit (< 5s): ~3 µs, no Redis. L1 miss: one Redis read, refills L1. Both miss: runs the block.
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**The L1 TTL is your staleness window.** An L1 entry cannot be invalidated from another process, so
|
|
146
|
+
`delete` clears Redis at once while other workers keep serving their copy until it expires. Keep it
|
|
147
|
+
at seconds.
|
|
148
|
+
|
|
149
|
+
## Why MemoryStore is so fast
|
|
150
|
+
|
|
151
|
+
| | Redis / Memcached | `MemoryStore` |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| Network round trip | yes | **no** |
|
|
154
|
+
| Connection pool checkout | yes | **no** |
|
|
155
|
+
| Wire protocol encode/decode | yes | **no** |
|
|
156
|
+
| Value handling | serialize to bytes | deep-copy in memory |
|
|
157
|
+
|
|
158
|
+
Reading a small hash 200,000 times, Ruby 3.4.5 on Apple silicon:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
MemoryStore: 362,918 reads/sec — 2.8 µs/read
|
|
162
|
+
FileStore: 51,202 reads/sec — 19.5 µs/read
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Redis adds a socket round trip on top of the same bookkeeping. What is left in `MemoryStore` is the
|
|
166
|
+
deep copy that keeps callers from mutating cached state (`dup` for strings, `Marshal` otherwise), so
|
|
167
|
+
keep values small and plain.
|
|
168
|
+
|
|
169
|
+
The cost: it is per-process and dies with the process. Use it for cheap-to-recompute, read-constantly
|
|
170
|
+
data; use a shared store for anything that must agree across processes.
|
|
171
|
+
|
|
172
|
+
## Testing
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# disable caching for one domain, restore after
|
|
176
|
+
config.around(:each, :no_pricing_cache) do |example|
|
|
177
|
+
original = Rails.cache(:pricing)
|
|
178
|
+
RailsNamedCache.register(:pricing, ActiveSupport::Cache::NullStore.new)
|
|
179
|
+
example.run
|
|
180
|
+
RailsNamedCache.register(:pricing, original)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
config.before { Rails.cache(:pricing).clear } # or clear one store between examples
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Advanced configuration
|
|
187
|
+
|
|
188
|
+
### Boot order
|
|
189
|
+
|
|
190
|
+
Stores are registered after `config/initializers` run and before eager loading, so eager-loaded
|
|
191
|
+
code can call `Rails.cache(:name)`.
|
|
192
|
+
|
|
193
|
+
> **Not available inside `config/initializers`** — a lookup in an initializer body raises
|
|
194
|
+
> `UnknownStoreError`. Use `config.after_initialize`, `to_prepare`, or application code.
|
|
195
|
+
|
|
196
|
+
Per-environment stores are plain Rails configuration:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
# config/environments/test.rb
|
|
200
|
+
config.named_cache_stores = { pricing: ActiveSupport::Cache::NullStore.new }
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Isolating stores that share a backend
|
|
204
|
+
|
|
205
|
+
Two stores on the same Redis, Memcached or directory share one keyspace — different objects, same
|
|
206
|
+
keys:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
Rails.cache(:short).write("user/1", "a")
|
|
210
|
+
Rails.cache(:long).read("user/1") # => "a" — not isolated
|
|
211
|
+
Rails.cache(:long).clear # also wipes :short
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Give each a `namespace`. That isolates the keys and makes the store identifiable in logs and APM —
|
|
215
|
+
the `cache_read.active_support` payload carries `namespace`, otherwise both report only
|
|
216
|
+
`store: "ActiveSupport::Cache::RedisCacheStore"`:
|
|
217
|
+
|
|
218
|
+
```ruby
|
|
219
|
+
config.named_cache_stores = {
|
|
220
|
+
short: [:redis_cache_store, { url: ENV.fetch("REDIS_URL"), namespace: "short", expires_in: 1.minute }],
|
|
221
|
+
long: [:redis_cache_store, { url: ENV.fetch("REDIS_URL"), namespace: "long", expires_in: 1.day }]
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Separate `MemoryStore` instances need no namespace.
|
|
226
|
+
|
|
227
|
+
### Engines
|
|
228
|
+
|
|
229
|
+
Do not set `config.named_cache_stores` from an engine — railtie config is shared, so it replaces the
|
|
230
|
+
host app's stores. Register instead:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
initializer "my_engine.caches", after: "rails_named_cache.register_stores" do
|
|
234
|
+
RailsNamedCache.register(:my_engine, ActiveSupport::Cache::MemoryStore.new)
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Configuration errors fail at boot
|
|
239
|
+
|
|
240
|
+
Nothing is registered if any entry is invalid:
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
config.named_cache_stores = { pricing: nil }
|
|
244
|
+
# ConfigurationError: Named cache store :pricing is nil
|
|
245
|
+
|
|
246
|
+
config.named_cache_stores = { "pricing" => a, pricing: b }
|
|
247
|
+
# ConfigurationError: Duplicate named cache store :pricing
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
`nil` is rejected because `ActiveSupport::Cache.lookup_store(nil)` returns a fresh `MemoryStore` —
|
|
251
|
+
a typo would become a cache that quietly loses data. Names are non-empty symbols or strings, and
|
|
252
|
+
`"memory"` and `:memory` are the same store.
|
|
253
|
+
|
|
254
|
+
### Registry API
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
RailsNamedCache.register(:memory, ActiveSupport::Cache::MemoryStore.new)
|
|
258
|
+
RailsNamedCache.fetch(:memory) # => the store (UnknownStoreError if missing)
|
|
259
|
+
RailsNamedCache.registered?(:redis) # => false
|
|
260
|
+
RailsNamedCache.names # => [:memory]
|
|
261
|
+
RailsNamedCache.registry.to_h # => { memory: #<MemoryStore> }
|
|
262
|
+
RailsNamedCache.reset! # empties the registry (tests)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Registration is by name, so stores registered by hand from an initializer survive boot unless
|
|
266
|
+
`config.named_cache_stores` names the same store.
|
|
267
|
+
|
|
268
|
+
## FAQ
|
|
269
|
+
|
|
270
|
+
**Why not global constants?**
|
|
271
|
+
Configuration ends up outside `config/`, tests stub constants, and nothing lists which caches exist.
|
|
272
|
+
|
|
273
|
+
**Forking servers (Puma workers, Unicorn, `preload_app!`)?**
|
|
274
|
+
Same caveat as `config.cache_store`: stores are built at boot, so with preloading they exist before
|
|
275
|
+
the fork. Pass connection-based stores as configuration (`[:redis_cache_store, { url: ... }]`)
|
|
276
|
+
rather than a live client — Rails connects lazily per worker, while a pre-connected socket shared
|
|
277
|
+
across forks corrupts.
|
|
278
|
+
|
|
279
|
+
**Do named stores get the per-request local cache?**
|
|
280
|
+
No. Rails inserts `LocalCache::Middleware` for the default `Rails.cache` only, so two identical
|
|
281
|
+
reads in one request hit the backend twice. Nothing leaks — the local cache is inert — and you can
|
|
282
|
+
opt in with `Rails.cache(:redis).with_local_cache { ... }`.
|
|
283
|
+
|
|
284
|
+
**Overhead?**
|
|
285
|
+
One `Concurrent::Map` read per lookup, lock-free. Stores are instantiated once at boot.
|
|
286
|
+
|
|
287
|
+
**Outside Rails?**
|
|
288
|
+
`RailsNamedCache.register` / `.fetch` work anywhere; `Rails.cache(:name)` and
|
|
289
|
+
`config.named_cache_stores` need an application. Requiring the gem always loads `rails/railtie`, so
|
|
290
|
+
load order never matters.
|
|
291
|
+
|
|
292
|
+
**Built-in L1/L2?**
|
|
293
|
+
No — [five lines](#l1l2-memory-in-front-of-redis) in app code, and a built-in version would have to
|
|
294
|
+
guess your staleness window. `Rails.cache(:memory, fallback: :redis)` stays possible later without
|
|
295
|
+
breaking the API.
|
|
296
|
+
|
|
297
|
+
## Be careful
|
|
298
|
+
|
|
299
|
+
Named stores make it easy to add caches, which makes it easy to add these:
|
|
300
|
+
|
|
301
|
+
- **Memory multiplies.** `MemoryStore` defaults to `size: 32.megabytes` when you omit it — five
|
|
302
|
+
named stores is up to 160 MB *per process*, times your Puma workers, times your servers. Set
|
|
303
|
+
`size:` on every `MemoryStore` explicitly and size it against the container limit, not the host.
|
|
304
|
+
- **A `MemoryStore` is one lock.** A `Monitor` guards reads *and* writes, so all threads in the
|
|
305
|
+
process serialize on it; the ~3 µs above is single-threaded. Pruning also runs inside that lock
|
|
306
|
+
(bounded by `max_prune_time`, default 2 s), so an oversized store can stall every thread. Prefer
|
|
307
|
+
several small stores over one huge hot one.
|
|
308
|
+
- **TTL does not reclaim memory.** Expired entries are dropped when read or on `cleanup`; eviction
|
|
309
|
+
happens on write once the store exceeds `size`, pruning back to 75%. A store full of expired
|
|
310
|
+
entries nobody reads still occupies RAM.
|
|
311
|
+
- **No cross-process invalidation.** `Rails.cache(:memory).delete(key)` and `.clear` affect the
|
|
312
|
+
calling process only — a console or rake task cannot flush the running workers. Anything that
|
|
313
|
+
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).
|
|
316
|
+
- **No per-request local cache.** Repeated reads of the same key in one request hit a named store's
|
|
317
|
+
backend every time. On a Redis-backed store, wrap the work in `with_local_cache` or put a
|
|
318
|
+
[memory layer in front](#l1l2-memory-in-front-of-redis).
|
|
319
|
+
- **Cache plain data, not objects.** Storing ActiveRecord instances or anything class-bound in a
|
|
320
|
+
long-lived in-process store outlives code reloading in development and keeps stale classes alive.
|
|
321
|
+
Arrays, hashes, strings and numbers stay cheap to deep-copy too.
|
|
322
|
+
- **Never take a store name from user input.** `Rails.cache(params[:store])` raises, and the error
|
|
323
|
+
message lists every registered store name.
|
|
324
|
+
- **Do not create a store per model.** Splitting one working set across many small caches lowers
|
|
325
|
+
the overall hit rate, and every configured store is built at boot whether anything reads it or
|
|
326
|
+
not. Add a store when a value genuinely needs a different backend, size or TTL — otherwise
|
|
327
|
+
`Rails.cache` is still the right answer.
|
|
328
|
+
- **Pass connection-based stores as configuration**, not as a live client object, or a preloading
|
|
329
|
+
forking server will share one socket across workers.
|
|
330
|
+
|
|
331
|
+
## Compatibility
|
|
332
|
+
|
|
333
|
+
| | |
|
|
334
|
+
| --- | --- |
|
|
335
|
+
| Ruby | 3.3, 3.4, 4.0 |
|
|
336
|
+
| Rails | 7.0, 7.1, 7.2, 8.0, 8.1 |
|
|
337
|
+
|
|
338
|
+
Every combination runs in CI. Note the Ruby floor: an older Rails is only supported on a modern
|
|
339
|
+
Ruby, so a Rails 7.0 app still on Ruby 3.1 cannot install this gem.
|
|
340
|
+
|
|
341
|
+
Rails 6.0 and 6.1 work but are neither supported nor tested — the runtime uses no API newer than
|
|
342
|
+
Rails 6.0, so relaxing the constraint locally is enough.
|
|
343
|
+
|
|
344
|
+
## Development
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
bundle exec rake # specs + rubocop
|
|
348
|
+
|
|
349
|
+
BUNDLE_GEMFILE=gemfiles/rails_7.0.gemfile bundle exec rspec # one Rails version
|
|
350
|
+
ls gemfiles/*.gemfile # rails_7.0 … rails_8.1
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The suite boots a real (tiny) Rails app in a tmpdir, so boot wiring is covered end to end. Design
|
|
354
|
+
rules live in [AGENTS.md](https://github.com/igorkasyanchuk/rails_named_cache/blob/main/AGENTS.md).
|
|
355
|
+
|
|
356
|
+
## Contributing
|
|
357
|
+
|
|
358
|
+
Bug reports and pull requests welcome at
|
|
359
|
+
<https://github.com/igorkasyanchuk/rails_named_cache>. Keep `bundle exec rake` green and add a spec
|
|
360
|
+
for anything that changes behaviour.
|
|
361
|
+
|
|
362
|
+
## License
|
|
363
|
+
|
|
364
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RailsNamedCache
|
|
6
|
+
module Generators
|
|
7
|
+
# Creates +config/initializers/rails_named_cache.rb+ with a default
|
|
8
|
+
# 1 MB MemoryStore registered as +:memory+.
|
|
9
|
+
#
|
|
10
|
+
# rails generate rails_named_cache:install
|
|
11
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Creates config/initializers/rails_named_cache.rb with a default memory store."
|
|
15
|
+
|
|
16
|
+
# @return [void]
|
|
17
|
+
def copy_initializer
|
|
18
|
+
template "rails_named_cache.rb", "config/initializers/rails_named_cache.rb"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Named cache stores, read back with Rails.cache(:name):
|
|
4
|
+
#
|
|
5
|
+
# Rails.cache(:memory).fetch("key") { expensive }
|
|
6
|
+
# Rails.cache(:memory).fetch("key", expires_in: 5.minutes) { expensive }
|
|
7
|
+
#
|
|
8
|
+
# Each store gets its own backend, size limit and default TTL. A store without
|
|
9
|
+
# expires_in keeps entries until they are overwritten, deleted or evicted.
|
|
10
|
+
# Rails.cache and config.cache_store are untouched.
|
|
11
|
+
|
|
12
|
+
Rails.application.configure do
|
|
13
|
+
config.named_cache_stores = {
|
|
14
|
+
# In-process and microsecond-fast, but per-process: every Puma worker has its
|
|
15
|
+
# own copy and it dies with the process. Nothing invalidates it from outside,
|
|
16
|
+
# so shorten the TTL for anything that changes and must not go stale.
|
|
17
|
+
memory: ActiveSupport::Cache::MemoryStore.new(size: 1.megabyte, expires_in: 1.day),
|
|
18
|
+
|
|
19
|
+
# Shared by every process and server. namespace: is required if another named
|
|
20
|
+
# store points at the same Redis — otherwise they share one keyspace and
|
|
21
|
+
# clearing either one wipes both.
|
|
22
|
+
# redis: ActiveSupport::Cache::RedisCacheStore.new(
|
|
23
|
+
# url: ENV.fetch("REDIS_URL"),
|
|
24
|
+
# namespace: "pricing",
|
|
25
|
+
# expires_in: 1.hour
|
|
26
|
+
# ),
|
|
27
|
+
|
|
28
|
+
# On disk: survives restarts, local to one machine. Suits large payloads that
|
|
29
|
+
# are cheap to store and expensive to rebuild.
|
|
30
|
+
# disk: ActiveSupport::Cache::FileStore.new(
|
|
31
|
+
# Rails.root.join("tmp/cache/reports"),
|
|
32
|
+
# expires_in: 1.day
|
|
33
|
+
# ),
|
|
34
|
+
|
|
35
|
+
# Caches nothing. Useful per environment, e.g. in config/environments/test.rb.
|
|
36
|
+
# null: ActiveSupport::Cache::NullStore.new
|
|
37
|
+
}
|
|
38
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsNamedCache
|
|
4
|
+
# Turns the raw +config.named_cache_stores+ value into cache store instances.
|
|
5
|
+
#
|
|
6
|
+
# Values may be:
|
|
7
|
+
#
|
|
8
|
+
# * an +ActiveSupport::Cache::Store+ instance (used as-is)
|
|
9
|
+
# * anything +ActiveSupport::Cache.lookup_store+ understands, e.g.
|
|
10
|
+
# +:memory_store+ or +[:redis_cache_store, { url: ENV["REDIS_URL"] }]+
|
|
11
|
+
#
|
|
12
|
+
# Configuration only resolves and validates; it never stores anything. The
|
|
13
|
+
# {Registry} owns state.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# RailsNamedCache::Configuration.new(
|
|
17
|
+
# memory: ActiveSupport::Cache::MemoryStore.new(size: 1.megabyte),
|
|
18
|
+
# redis: [:redis_cache_store, { url: ENV.fetch("REDIS_URL") }]
|
|
19
|
+
# ).resolved_stores
|
|
20
|
+
# # => { memory: #<MemoryStore>, redis: #<RedisCacheStore> }
|
|
21
|
+
class Configuration
|
|
22
|
+
# @return [Hash] the raw configured value
|
|
23
|
+
attr_reader :stores
|
|
24
|
+
|
|
25
|
+
# @param stores [Hash{Symbol=>Object}, nil] raw +config.named_cache_stores+
|
|
26
|
+
# @raise [ConfigurationError] if +stores+ is not a Hash
|
|
27
|
+
def initialize(stores = {})
|
|
28
|
+
stores ||= {}
|
|
29
|
+
|
|
30
|
+
unless stores.is_a?(Hash)
|
|
31
|
+
raise ConfigurationError,
|
|
32
|
+
"config.named_cache_stores must be a Hash of name => cache store, got #{stores.class}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@stores = stores
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Hash{Symbol=>ActiveSupport::Cache::Store}] name => store instance
|
|
39
|
+
# @raise [ConfigurationError] for duplicate names or values that cannot become a cache store
|
|
40
|
+
def resolved_stores
|
|
41
|
+
stores.each_with_object({}) do |(name, value), resolved|
|
|
42
|
+
key = RailsNamedCache.normalize_name(name)
|
|
43
|
+
|
|
44
|
+
# "memory" and :memory are the same store; silently keeping the last one
|
|
45
|
+
# would hide a typo in config.
|
|
46
|
+
if resolved.key?(key)
|
|
47
|
+
raise ConfigurationError,
|
|
48
|
+
"Duplicate named cache store #{key.inspect} in config.named_cache_stores"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
resolved[key] = resolve(key, value)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# @param name [Symbol]
|
|
58
|
+
# @param value [Object]
|
|
59
|
+
# @return [ActiveSupport::Cache::Store]
|
|
60
|
+
def resolve(name, value)
|
|
61
|
+
return value if value.is_a?(ActiveSupport::Cache::Store)
|
|
62
|
+
|
|
63
|
+
# nil would make lookup_store silently hand back a MemoryStore.
|
|
64
|
+
raise ConfigurationError, "Named cache store #{name.inspect} is nil" if value.nil?
|
|
65
|
+
|
|
66
|
+
store = ActiveSupport::Cache.lookup_store(value)
|
|
67
|
+
return store if store.is_a?(ActiveSupport::Cache::Store)
|
|
68
|
+
|
|
69
|
+
raise ConfigurationError,
|
|
70
|
+
"Named cache store #{name.inspect} must be an ActiveSupport::Cache::Store, " \
|
|
71
|
+
"got #{value.class}"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsNamedCache
|
|
4
|
+
# Prepended onto the +Rails+ singleton class so +Rails.cache+ takes an
|
|
5
|
+
# optional store name.
|
|
6
|
+
#
|
|
7
|
+
# This is the only monkey patch in the gem.
|
|
8
|
+
#
|
|
9
|
+
# Rails.cache # => the default Rails cache, untouched
|
|
10
|
+
# Rails.cache(:memory) # => the store registered as :memory
|
|
11
|
+
module RailsExt
|
|
12
|
+
# Splat rather than a +nil+ default, so +Rails.cache(name)+ with a +nil+
|
|
13
|
+
# name raises instead of quietly reading the default store.
|
|
14
|
+
#
|
|
15
|
+
# @param name [Array<Symbol, String>] zero arguments, or one registered store name
|
|
16
|
+
# @return [ActiveSupport::Cache::Store]
|
|
17
|
+
# @raise [ConfigurationError] if +name+ is present but not a non-empty Symbol or String
|
|
18
|
+
# @raise [UnknownStoreError] if +name+ was never registered
|
|
19
|
+
def cache(*name)
|
|
20
|
+
return super() if name.empty?
|
|
21
|
+
|
|
22
|
+
RailsNamedCache.fetch(*name)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/railtie"
|
|
4
|
+
|
|
5
|
+
module RailsNamedCache
|
|
6
|
+
# Wires the gem into the Rails boot process.
|
|
7
|
+
#
|
|
8
|
+
# Adds +config.named_cache_stores+ (default +{}+) and fills the {Registry}
|
|
9
|
+
# from it once, after +config/initializers+ have run and before eager loading,
|
|
10
|
+
# so eager-loaded code can already call +Rails.cache(:name)+.
|
|
11
|
+
class Railtie < ::Rails::Railtie
|
|
12
|
+
config.named_cache_stores = {}
|
|
13
|
+
|
|
14
|
+
initializer "rails_named_cache.register_stores", after: :load_config_initializers do |app|
|
|
15
|
+
# Registration is by name, so manual RailsNamedCache.register calls made
|
|
16
|
+
# from config/initializers survive unless config names the same store.
|
|
17
|
+
RailsNamedCache.load_configuration(app.config.named_cache_stores)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/map"
|
|
4
|
+
|
|
5
|
+
module RailsNamedCache
|
|
6
|
+
# Thread-safe store of named +ActiveSupport::Cache::Store+ instances.
|
|
7
|
+
#
|
|
8
|
+
# Lookups are O(1) and lock-free; writes happen once during boot.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# registry = RailsNamedCache::Registry.new
|
|
12
|
+
# registry.register(:memory, ActiveSupport::Cache::MemoryStore.new)
|
|
13
|
+
# registry.fetch(:memory) # => #<ActiveSupport::Cache::MemoryStore>
|
|
14
|
+
class Registry
|
|
15
|
+
def initialize
|
|
16
|
+
@stores = Concurrent::Map.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Registers +store+ under +name+, replacing any previous entry.
|
|
20
|
+
#
|
|
21
|
+
# @param name [Symbol, String] name used by +Rails.cache(name)+
|
|
22
|
+
# @param store [ActiveSupport::Cache::Store] the store instance
|
|
23
|
+
# @return [ActiveSupport::Cache::Store] the registered store
|
|
24
|
+
# @raise [ConfigurationError] if +name+ is blank or +store+ is not a cache store
|
|
25
|
+
def register(name, store)
|
|
26
|
+
key = RailsNamedCache.normalize_name(name)
|
|
27
|
+
|
|
28
|
+
unless store.is_a?(ActiveSupport::Cache::Store)
|
|
29
|
+
raise ConfigurationError,
|
|
30
|
+
"Named cache store #{key.inspect} must be an ActiveSupport::Cache::Store, " \
|
|
31
|
+
"got #{store.class}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@stores[key] = store
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @param name [Symbol, String] registered store name
|
|
38
|
+
# @return [ActiveSupport::Cache::Store]
|
|
39
|
+
# @raise [ConfigurationError] if +name+ is not a non-empty Symbol or String
|
|
40
|
+
# @raise [UnknownStoreError] if nothing is registered under +name+
|
|
41
|
+
def fetch(name)
|
|
42
|
+
key = RailsNamedCache.normalize_name(name)
|
|
43
|
+
@stores.fetch(key) { raise UnknownStoreError.new(key, names) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param name [Symbol, String]
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
# @raise [ConfigurationError] if +name+ is not a non-empty Symbol or String
|
|
49
|
+
def registered?(name)
|
|
50
|
+
@stores.key?(RailsNamedCache.normalize_name(name))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Array<Symbol>] every registered name
|
|
54
|
+
def names
|
|
55
|
+
@stores.keys
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Integer] number of registered stores
|
|
59
|
+
def size
|
|
60
|
+
@stores.size
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Removes every registered store. Intended for tests and reboots.
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
def clear
|
|
67
|
+
@stores.clear
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Hash{Symbol=>ActiveSupport::Cache::Store}] snapshot of the registry
|
|
71
|
+
def to_h
|
|
72
|
+
@stores.each_pair.to_h
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
require "active_support/cache"
|
|
5
|
+
|
|
6
|
+
require_relative "rails_named_cache/version"
|
|
7
|
+
require_relative "rails_named_cache/registry"
|
|
8
|
+
require_relative "rails_named_cache/configuration"
|
|
9
|
+
require_relative "rails_named_cache/rails_ext"
|
|
10
|
+
|
|
11
|
+
# Named +ActiveSupport::Cache::Store+ instances for Rails applications.
|
|
12
|
+
#
|
|
13
|
+
# Register stores through +config.named_cache_stores+ and read them back with
|
|
14
|
+
# +Rails.cache(:name)+.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# Rails.cache.fetch("users") { User.all } # default Rails cache
|
|
18
|
+
# Rails.cache(:memory).fetch("countries") { ... } # named store
|
|
19
|
+
module RailsNamedCache
|
|
20
|
+
# Base class for every error raised by this gem.
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
|
|
23
|
+
# Raised when +config.named_cache_stores+ holds something that cannot become
|
|
24
|
+
# a cache store.
|
|
25
|
+
class ConfigurationError < Error; end
|
|
26
|
+
|
|
27
|
+
# Raised when a named cache store is looked up but was never registered.
|
|
28
|
+
#
|
|
29
|
+
# Rails.cache(:nope)
|
|
30
|
+
# # => RailsNamedCache::UnknownStoreError:
|
|
31
|
+
# # Unknown named cache store :nope
|
|
32
|
+
# #
|
|
33
|
+
# # Available stores:
|
|
34
|
+
# # :memory
|
|
35
|
+
class UnknownStoreError < Error
|
|
36
|
+
# @return [Symbol] the requested name
|
|
37
|
+
attr_reader :name
|
|
38
|
+
|
|
39
|
+
# @return [Array<Symbol>] the registered names
|
|
40
|
+
attr_reader :available
|
|
41
|
+
|
|
42
|
+
# @param name [Symbol] requested store name
|
|
43
|
+
# @param available [Array<Symbol>] registered store names
|
|
44
|
+
def initialize(name, available)
|
|
45
|
+
@name = name
|
|
46
|
+
# Sorted so the message is deterministic regardless of registration order.
|
|
47
|
+
@available = available.sort
|
|
48
|
+
listed = @available.empty? ? "(none registered)" : @available.map(&:inspect).join("\n")
|
|
49
|
+
super("Unknown named cache store #{name.inspect}\n\nAvailable stores:\n#{listed}")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The process-wide registry.
|
|
54
|
+
#
|
|
55
|
+
# @return [Registry]
|
|
56
|
+
REGISTRY = Registry.new
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
# @return [Registry] the process-wide registry
|
|
60
|
+
def registry
|
|
61
|
+
REGISTRY
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Registers a cache store under +name+.
|
|
65
|
+
#
|
|
66
|
+
# @param name [Symbol, String]
|
|
67
|
+
# @param store [ActiveSupport::Cache::Store]
|
|
68
|
+
# @return [ActiveSupport::Cache::Store]
|
|
69
|
+
def register(name, store)
|
|
70
|
+
registry.register(name, store)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @param name [Symbol, String]
|
|
74
|
+
# @return [ActiveSupport::Cache::Store]
|
|
75
|
+
# @raise [UnknownStoreError]
|
|
76
|
+
def fetch(name)
|
|
77
|
+
registry.fetch(name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @param name [Symbol, String]
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def registered?(name)
|
|
83
|
+
registry.registered?(name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [Array<Symbol>] every registered name
|
|
87
|
+
def names
|
|
88
|
+
registry.names
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Registers every store described by +config.named_cache_stores+.
|
|
92
|
+
#
|
|
93
|
+
# @param stores [Hash{Symbol=>Object}, nil] raw configuration value
|
|
94
|
+
# @return [Array<Symbol>] the names that were registered
|
|
95
|
+
def load_configuration(stores)
|
|
96
|
+
Configuration.new(stores).resolved_stores.each do |name, store|
|
|
97
|
+
register(name, store)
|
|
98
|
+
end.keys
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Teaches +Rails.cache+ to accept an optional name. Idempotent, and a no-op
|
|
102
|
+
# when the gem is used outside Rails.
|
|
103
|
+
#
|
|
104
|
+
# @return [void]
|
|
105
|
+
def install!
|
|
106
|
+
return if !defined?(::Rails) || ::Rails.singleton_class.include?(RailsExt)
|
|
107
|
+
|
|
108
|
+
::Rails.singleton_class.prepend(RailsExt)
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Empties the registry. Intended for tests.
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
115
|
+
def reset!
|
|
116
|
+
registry.clear
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Coerces a store name to a Symbol.
|
|
120
|
+
#
|
|
121
|
+
# Shared by {Registry} and {Configuration} so a bad name fails the same way
|
|
122
|
+
# whether it came from configuration or from a direct call.
|
|
123
|
+
#
|
|
124
|
+
# @api private
|
|
125
|
+
# @param name [Symbol, String]
|
|
126
|
+
# @return [Symbol]
|
|
127
|
+
# @raise [ConfigurationError] if +name+ is not a non-empty Symbol or String
|
|
128
|
+
def normalize_name(name)
|
|
129
|
+
unless (name.is_a?(Symbol) || name.is_a?(String)) && !name.to_s.empty?
|
|
130
|
+
raise ConfigurationError,
|
|
131
|
+
"Cache store name must be a non-empty Symbol or String, got #{name.inspect}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
name.to_sym
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Unconditional: railties is a runtime dependency, and guarding this on
|
|
140
|
+
# defined?(Rails::Railtie) turns the gem into a silent no-op whenever it is
|
|
141
|
+
# required before Rails.
|
|
142
|
+
require_relative "rails_named_cache/railtie"
|
|
143
|
+
|
|
144
|
+
RailsNamedCache.install!
|
metadata
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rails_named_cache
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Igor Kasyanchuk
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: concurrent-ruby
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.1'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2.0'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '1.1'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '2.0'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: railties
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '7.0'
|
|
59
|
+
- - "<"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '9.0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '7.0'
|
|
69
|
+
- - "<"
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '9.0'
|
|
72
|
+
description: |
|
|
73
|
+
rails_named_cache lets a Rails application register several
|
|
74
|
+
ActiveSupport::Cache::Store instances by name and read them back with
|
|
75
|
+
Rails.cache(:memory), Rails.cache(:redis), Rails.cache(:pricing) and so on.
|
|
76
|
+
Rails.cache keeps working exactly as before, stores are never wrapped, and
|
|
77
|
+
no new caching API is introduced.
|
|
78
|
+
email:
|
|
79
|
+
- igorkasyanchuk@gmail.com
|
|
80
|
+
executables: []
|
|
81
|
+
extensions: []
|
|
82
|
+
extra_rdoc_files: []
|
|
83
|
+
files:
|
|
84
|
+
- CHANGELOG.md
|
|
85
|
+
- LICENSE
|
|
86
|
+
- README.md
|
|
87
|
+
- lib/generators/rails_named_cache/install/install_generator.rb
|
|
88
|
+
- lib/generators/rails_named_cache/install/templates/rails_named_cache.rb
|
|
89
|
+
- lib/rails_named_cache.rb
|
|
90
|
+
- lib/rails_named_cache/configuration.rb
|
|
91
|
+
- lib/rails_named_cache/rails_ext.rb
|
|
92
|
+
- lib/rails_named_cache/railtie.rb
|
|
93
|
+
- lib/rails_named_cache/registry.rb
|
|
94
|
+
- lib/rails_named_cache/version.rb
|
|
95
|
+
homepage: https://github.com/igorkasyanchuk/rails_named_cache
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata:
|
|
99
|
+
homepage_uri: https://github.com/igorkasyanchuk/rails_named_cache
|
|
100
|
+
source_code_uri: https://github.com/igorkasyanchuk/rails_named_cache
|
|
101
|
+
changelog_uri: https://github.com/igorkasyanchuk/rails_named_cache/blob/main/CHANGELOG.md
|
|
102
|
+
bug_tracker_uri: https://github.com/igorkasyanchuk/rails_named_cache/issues
|
|
103
|
+
rubygems_mfa_required: 'true'
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: 3.3.0
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.7.2
|
|
119
|
+
specification_version: 4
|
|
120
|
+
summary: Multiple named cache stores for Rails, through Rails.cache(:name).
|
|
121
|
+
test_files: []
|