support_table_cache 1.1.3 → 1.1.5
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/AGENTS.md +74 -0
- data/ARCHITECTURE.md +386 -0
- data/CHANGELOG.md +20 -0
- data/README.md +21 -3
- data/VERSION +1 -1
- data/lib/support_table_cache/associations.rb +1 -1
- data/lib/support_table_cache/fiber_locals.rb +53 -0
- data/lib/support_table_cache/find_by_override.rb +5 -3
- data/lib/support_table_cache/memory_cache.rb +26 -0
- data/lib/support_table_cache/relation_override.rb +15 -4
- data/lib/support_table_cache.rb +48 -43
- metadata +6 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9fe5a9190c32271b431faed20e0b6ddbeb195cd05c927cae94c03b5c7d20bc8a
|
|
4
|
+
data.tar.gz: 8f8bc59c1a0eb6240712e50fb34da28f45d071e1c6b59e28d647d8bcf7e17896
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c833468a94fed3f16efd45022c56cbf19c755d6f0e2f48a2d93b8b92c2d946bd46f246170a623f2edeade892aa017851d581794fc7505c4efd5c5bce3fdf29b9
|
|
7
|
+
data.tar.gz: d4307c2d653184fe890e8aa8af0c548ef340d817141fddcb69e8a083ffcd235fa7d0d124b98e6ece0dd99e5a8fc549db1a8a986acf571aada17cb6f0619c67e1
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Copilot Instructions for support_table_cache
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
This is a Ruby gem that adds transparent caching to ActiveRecord support/lookup tables. It intercepts `find_by` queries and `belongs_to` associations to cache small, rarely-changing reference tables (statuses, types, categories) without code changes.
|
|
6
|
+
|
|
7
|
+
**Core principle**: Cache entries keyed by unique attribute combinations, auto-invalidated on record changes via `after_commit` callbacks.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
- **lib/support_table_cache.rb**: Main module with `cache_by` DSL, cache configuration, and invalidation logic
|
|
12
|
+
- **lib/support_table_cache/find_by_override.rb**: Prepends to model class to intercept `find_by` calls
|
|
13
|
+
- **lib/support_table_cache/relation_override.rb**: Prepends to `ActiveRecord::Relation` to handle scoped queries (e.g., `where(group: 'x').find_by(name: 'y')`)
|
|
14
|
+
- **lib/support_table_cache/associations.rb**: Extends `belongs_to` with `cache_belongs_to` to cache foreign key lookups
|
|
15
|
+
- **lib/support_table_cache/memory_cache.rb**: In-process cache implementation (use `support_table_cache = :memory`)
|
|
16
|
+
|
|
17
|
+
See [ARCHITECTURE.md](../ARCHITECTURE.md) for detailed flow diagrams showing cache key generation, invalidation, and association caching sequences.
|
|
18
|
+
|
|
19
|
+
## Key Patterns
|
|
20
|
+
|
|
21
|
+
### Model Configuration
|
|
22
|
+
Models use `cache_by` to declare unique keys that can be cached. Support composite keys and case-insensitivity:
|
|
23
|
+
```ruby
|
|
24
|
+
cache_by :name, case_sensitive: false
|
|
25
|
+
cache_by [:group, :code]
|
|
26
|
+
cache_by :name, where: {deleted_at: nil} # For default scopes
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Cache Key Structure
|
|
30
|
+
Cache keys are `[ClassName, {attr1: val1, attr2: val2}]` arrays with sorted attribute names. Case-insensitive values are downcased before keying.
|
|
31
|
+
|
|
32
|
+
### Module Prepending Pattern
|
|
33
|
+
Uses `prepend` to wrap ActiveRecord methods (`find_by`) rather than monkey-patching. This allows `super` to call original behavior on cache misses or when caching disabled.
|
|
34
|
+
|
|
35
|
+
## Testing
|
|
36
|
+
|
|
37
|
+
- **Multi-version testing**: Uses Appraisal gem to test against ActiveRecord 5.0-8.0 (see [Appraisals](../Appraisals))
|
|
38
|
+
- **Run tests**: `bundle exec rspec` (default rake task) or `bundle exec appraisal rspec` for all versions
|
|
39
|
+
- **Test setup**: In-memory SQLite database created in [spec/spec_helper.rb](../spec/spec_helper.rb) with test tables
|
|
40
|
+
- **Test isolation**: Tests wrapped with `SupportTableCache.testing!` in RSpec `config.before` to prevent cache pollution
|
|
41
|
+
|
|
42
|
+
### Code Style
|
|
43
|
+
Use **standardrb** for linting. Run `standardrb --fix` before committing. CI enforces this on ActiveRecord 8.0 matrix entry.
|
|
44
|
+
|
|
45
|
+
## Common Operations
|
|
46
|
+
|
|
47
|
+
### Adding Cache Support to Models
|
|
48
|
+
1. Include `SupportTableCache` in model class
|
|
49
|
+
2. Call `cache_by` with unique key attributes
|
|
50
|
+
3. Optionally set `self.support_table_cache_ttl = 5.minutes`
|
|
51
|
+
4. For associations: include `SupportTableCache::Associations` in parent model, then `cache_belongs_to :association_name`
|
|
52
|
+
|
|
53
|
+
### Cache Invalidation
|
|
54
|
+
Automatic via `after_commit` callback that clears all cache key variations (both old and new attribute values on updates). No manual invalidation needed unless using in-memory cache across processes.
|
|
55
|
+
|
|
56
|
+
### Debugging Cache Behavior
|
|
57
|
+
- Use `fetch_by` instead of `find_by` to raise error if query won't hit cache
|
|
58
|
+
- Disable caching in block: `Model.disable_cache { ... }` or globally `SupportTableCache.disable { ... }`
|
|
59
|
+
- Check if caching enabled: inspect `support_table_cache_by_attributes` class attribute
|
|
60
|
+
|
|
61
|
+
## Development Workflow
|
|
62
|
+
|
|
63
|
+
1. **Running specs locally**: `bundle exec rspec` (uses Ruby 3.3+ and ActiveRecord 8.0 from Gemfile)
|
|
64
|
+
2. **Testing specific AR version**: `bundle exec appraisal activerecord_7 rspec`
|
|
65
|
+
3. **Generating all gemfiles**: `bundle exec appraisal generate`
|
|
66
|
+
4. **Lint before commit**: `standardrb --fix`
|
|
67
|
+
5. **Release**: Only from `main` branch (enforced by `Rakefile` pre-release check)
|
|
68
|
+
|
|
69
|
+
## Important Constraints
|
|
70
|
+
|
|
71
|
+
- **Target models**: Only for small tables (few hundred rows max)
|
|
72
|
+
- **Unique keys only**: `cache_by` attributes must define unique constraints
|
|
73
|
+
- **No runtime scopes**: Cannot use `cache_belongs_to` with scoped associations (checked at configuration time)
|
|
74
|
+
- **In-memory cache caveat**: Per-process, not invalidated across processes—only use for truly static data or with TTL
|
data/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Support Table Cache Architecture
|
|
2
|
+
|
|
3
|
+
This document describes the architecture and design of the `support_table_cache` gem, which provides automatic caching for ActiveRecord support table models.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Support Table Cache is designed to optimize database queries for small lookup tables (support tables) that have:
|
|
8
|
+
- Unique keys (e.g., unique `name` attribute)
|
|
9
|
+
- Limited number of entries (a few hundred at most)
|
|
10
|
+
- Rarely updated data but frequently queried
|
|
11
|
+
- Used for data normalization (lookup tables)
|
|
12
|
+
|
|
13
|
+
The gem automatically caches records when using `find_by` methods and `belongs_to` associations, eliminating redundant database queries.
|
|
14
|
+
|
|
15
|
+
## High-Level Architecture
|
|
16
|
+
|
|
17
|
+
```mermaid
|
|
18
|
+
flowchart TB
|
|
19
|
+
subgraph "Application Layer"
|
|
20
|
+
App["Application Code"]
|
|
21
|
+
Models["ActiveRecord Models"]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
subgraph "Support Table Cache Gem"
|
|
25
|
+
STC["SupportTableCache Module"]
|
|
26
|
+
Assoc["Associations Module"]
|
|
27
|
+
FindBy["FindByOverride"]
|
|
28
|
+
RelOverride["RelationOverride"]
|
|
29
|
+
MemCache["MemoryCache"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
subgraph "Cache Layer"
|
|
33
|
+
RC["Rails.cache / Custom Cache"]
|
|
34
|
+
MC["In-Memory Cache"]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
subgraph "Database Layer"
|
|
38
|
+
DB["PostgreSQL/MySQL/SQLite"]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
App --> Models
|
|
42
|
+
Models --> STC
|
|
43
|
+
STC --> FindBy
|
|
44
|
+
STC --> Assoc
|
|
45
|
+
STC --> RelOverride
|
|
46
|
+
STC --> MemCache
|
|
47
|
+
|
|
48
|
+
FindBy --> RC
|
|
49
|
+
FindBy --> MC
|
|
50
|
+
Assoc --> RC
|
|
51
|
+
Assoc --> MC
|
|
52
|
+
RelOverride --> RC
|
|
53
|
+
RelOverride --> MC
|
|
54
|
+
|
|
55
|
+
FindBy -.-> DB
|
|
56
|
+
Assoc -.-> DB
|
|
57
|
+
RelOverride -.-> DB
|
|
58
|
+
|
|
59
|
+
RC -.-> DB
|
|
60
|
+
MC -.-> DB
|
|
61
|
+
|
|
62
|
+
classDef appLayer fill:#e1f5fe
|
|
63
|
+
classDef cacheLayer fill:#fff3e0
|
|
64
|
+
classDef dbLayer fill:#f3e5f5
|
|
65
|
+
classDef gemLayer fill:#e8f5e8
|
|
66
|
+
|
|
67
|
+
class App,Models appLayer
|
|
68
|
+
class STC,Assoc,FindBy,RelOverride,MemCache gemLayer
|
|
69
|
+
class RC,MC cacheLayer
|
|
70
|
+
class DB dbLayer
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Core Components
|
|
74
|
+
|
|
75
|
+
### 1. SupportTableCache Module
|
|
76
|
+
|
|
77
|
+
The main module that provides caching functionality to ActiveRecord models.
|
|
78
|
+
|
|
79
|
+
```mermaid
|
|
80
|
+
flowchart LR
|
|
81
|
+
subgraph "SupportTableCache Module"
|
|
82
|
+
CM["Class Methods"]
|
|
83
|
+
IM["Instance Methods"]
|
|
84
|
+
CC["Cache Configuration"]
|
|
85
|
+
CCE["Cache Control & Expiry"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
subgraph "Class Methods"
|
|
89
|
+
CB["cache_by()"]
|
|
90
|
+
DC["disable_cache()"]
|
|
91
|
+
EC["enable_cache()"]
|
|
92
|
+
LC["load_cache()"]
|
|
93
|
+
SC["support_table_cache="]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
subgraph "Instance Methods"
|
|
97
|
+
UC["uncache()"]
|
|
98
|
+
ClearCache["support_table_clear_cache_entries()"]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
CM --> CB
|
|
102
|
+
CM --> DC
|
|
103
|
+
CM --> EC
|
|
104
|
+
CM --> LC
|
|
105
|
+
CM --> SC
|
|
106
|
+
|
|
107
|
+
IM --> UC
|
|
108
|
+
IM --> ClearCache
|
|
109
|
+
|
|
110
|
+
CB --> CC
|
|
111
|
+
SC --> CC
|
|
112
|
+
UC --> CCE
|
|
113
|
+
ClearCache --> CCE
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 2. Cache Key Generation Flow
|
|
117
|
+
|
|
118
|
+
```mermaid
|
|
119
|
+
sequenceDiagram
|
|
120
|
+
participant App as Application
|
|
121
|
+
participant Model as Model.find_by
|
|
122
|
+
participant Override as FindByOverride
|
|
123
|
+
participant Cache as Cache Store
|
|
124
|
+
participant DB as Database
|
|
125
|
+
|
|
126
|
+
App->>Model: Model.find_by(name: "example")
|
|
127
|
+
Model->>Override: Intercept find_by call
|
|
128
|
+
|
|
129
|
+
Override->>Override: Extract attributes from query
|
|
130
|
+
Override->>Override: Check cache_by_attributes config
|
|
131
|
+
Override->>Override: Generate cache key from attributes
|
|
132
|
+
|
|
133
|
+
alt Cache Hit
|
|
134
|
+
Override->>Cache: fetch(cache_key)
|
|
135
|
+
Cache-->>Override: Return cached record
|
|
136
|
+
Override-->>App: Return record
|
|
137
|
+
else Cache Miss
|
|
138
|
+
Override->>Cache: fetch(cache_key) with block
|
|
139
|
+
Cache->>DB: Execute original find_by query
|
|
140
|
+
DB-->>Cache: Return record from DB
|
|
141
|
+
Cache->>Cache: Store record with TTL
|
|
142
|
+
Cache-->>Override: Return record
|
|
143
|
+
Override-->>App: Return record
|
|
144
|
+
end
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 3. Cache Key Structure
|
|
148
|
+
|
|
149
|
+
```mermaid
|
|
150
|
+
flowchart TD
|
|
151
|
+
Attrs["Query Attributes<br/>{name: 'active', type: 'primary'}"]
|
|
152
|
+
|
|
153
|
+
subgraph "Key Generation Process"
|
|
154
|
+
Sort["Sort attribute names<br/>['name', 'type']"]
|
|
155
|
+
CaseCheck["Apply case sensitivity<br/>name: 'active' → 'active'<br/>type: 'primary' → 'primary'"]
|
|
156
|
+
KeyGen["Generate cache key<br/>['ModelName', {name: 'active', type: 'primary'}]"]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
Attrs --> Sort
|
|
160
|
+
Sort --> CaseCheck
|
|
161
|
+
CaseCheck --> KeyGen
|
|
162
|
+
|
|
163
|
+
KeyGen --> CacheStore["Cache Store<br/>Key: ['Status', {name: 'active', type: 'primary'}]<br/>Value: #<Status id: 1, name: 'active'>"]
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 4. Association Caching Flow
|
|
167
|
+
|
|
168
|
+
```mermaid
|
|
169
|
+
sequenceDiagram
|
|
170
|
+
participant App as Application
|
|
171
|
+
participant Parent as Parent Model
|
|
172
|
+
participant Assoc as Association Reader
|
|
173
|
+
participant Cache as Cache Store
|
|
174
|
+
participant Child as Child Model
|
|
175
|
+
participant DB as Database
|
|
176
|
+
|
|
177
|
+
App->>Parent: parent.status
|
|
178
|
+
Parent->>Assoc: Call association reader
|
|
179
|
+
|
|
180
|
+
Assoc->>Assoc: Extract foreign key value
|
|
181
|
+
Assoc->>Assoc: Build cache key from foreign key
|
|
182
|
+
|
|
183
|
+
alt Cache Hit
|
|
184
|
+
Assoc->>Cache: fetch(cache_key)
|
|
185
|
+
Cache-->>Assoc: Return cached record
|
|
186
|
+
Assoc-->>App: Return associated record
|
|
187
|
+
else Cache Miss
|
|
188
|
+
Assoc->>Cache: fetch(cache_key) with block
|
|
189
|
+
Cache->>Child: Load association normally
|
|
190
|
+
Child->>DB: Query database
|
|
191
|
+
DB-->>Child: Return record
|
|
192
|
+
Child-->>Cache: Return record
|
|
193
|
+
Cache->>Cache: Store record with TTL
|
|
194
|
+
Cache-->>Assoc: Return record
|
|
195
|
+
Assoc-->>App: Return associated record
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 5. Cache Invalidation Strategy
|
|
200
|
+
|
|
201
|
+
```mermaid
|
|
202
|
+
flowchart TD
|
|
203
|
+
subgraph "Record Lifecycle Events"
|
|
204
|
+
Create["Record Created"]
|
|
205
|
+
Update["Record Updated"]
|
|
206
|
+
Delete["Record Deleted"]
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
subgraph "Cache Invalidation Process"
|
|
210
|
+
Hook["after_commit callback"]
|
|
211
|
+
ExtractKeys["Extract all cacheable<br/>attribute combinations"]
|
|
212
|
+
BuildKeys["Build cache keys for<br/>before & after states"]
|
|
213
|
+
ClearCache["Delete cache entries"]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
subgraph "Cache Keys Cleared"
|
|
217
|
+
BeforeKeys["Keys with old values"]
|
|
218
|
+
AfterKeys["Keys with new values"]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
Create --> Hook
|
|
222
|
+
Update --> Hook
|
|
223
|
+
Delete --> Hook
|
|
224
|
+
|
|
225
|
+
Hook --> ExtractKeys
|
|
226
|
+
ExtractKeys --> BuildKeys
|
|
227
|
+
BuildKeys --> ClearCache
|
|
228
|
+
|
|
229
|
+
ClearCache --> BeforeKeys
|
|
230
|
+
ClearCache --> AfterKeys
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 6. Cache Implementation Types
|
|
234
|
+
|
|
235
|
+
```mermaid
|
|
236
|
+
flowchart LR
|
|
237
|
+
subgraph "Cache Types"
|
|
238
|
+
subgraph "External Cache"
|
|
239
|
+
RC["Rails.cache<br/>(Redis/Memcached)"]
|
|
240
|
+
CC["Custom Cache Store"]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
subgraph "In-Memory Cache"
|
|
244
|
+
MC["MemoryCache<br/>(Process-local)"]
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
subgraph "Configuration"
|
|
249
|
+
Global["Global Cache<br/>SupportTableCache.cache = store"]
|
|
250
|
+
PerClass["Per-Class Cache<br/>Model.support_table_cache = :memory"]
|
|
251
|
+
Testing["Test Mode<br/>SupportTableCache.testing!"]
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
Global --> RC
|
|
255
|
+
Global --> CC
|
|
256
|
+
PerClass --> MC
|
|
257
|
+
Testing --> MC
|
|
258
|
+
|
|
259
|
+
subgraph "Trade-offs"
|
|
260
|
+
ExtPros["✓ Shared across processes<br/>✓ Automatic invalidation<br/>✓ TTL support"]
|
|
261
|
+
ExtCons["✗ Network overhead<br/>✗ Serialization cost"]
|
|
262
|
+
|
|
263
|
+
MemPros["✓ Ultra-fast access<br/>✓ No network overhead<br/>✓ No serialization"]
|
|
264
|
+
MemCons["✗ Per-process storage<br/>✗ Manual invalidation<br/>✗ Memory usage"]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
RC -.-> ExtPros
|
|
268
|
+
CC -.-> ExtPros
|
|
269
|
+
RC -.-> ExtCons
|
|
270
|
+
CC -.-> ExtCons
|
|
271
|
+
|
|
272
|
+
MC -.-> MemPros
|
|
273
|
+
MC -.-> MemCons
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 7. Testing Integration
|
|
277
|
+
|
|
278
|
+
```mermaid
|
|
279
|
+
flowchart TD
|
|
280
|
+
subgraph "Test Execution"
|
|
281
|
+
TestStart["Test Begins"]
|
|
282
|
+
TestCode["Test Code Execution"]
|
|
283
|
+
TestEnd["Test Ends"]
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
subgraph "Cache Isolation"
|
|
287
|
+
TestCache["Isolated Test Cache<br/>(MemoryCache per test)"]
|
|
288
|
+
CleanSlate["Clean State<br/>(No cache pollution)"]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
TestStart --> TestCache
|
|
292
|
+
TestCache --> TestCode
|
|
293
|
+
TestCode --> CleanSlate
|
|
294
|
+
CleanSlate --> TestEnd
|
|
295
|
+
|
|
296
|
+
subgraph "Integration Pattern"
|
|
297
|
+
RSpecWrap["RSpec around hook<br/>SupportTableCache.testing!"]
|
|
298
|
+
MiniTestWrap["MiniTest around hook<br/>SupportTableCache.testing!"]
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
RSpecWrap -.-> TestCache
|
|
302
|
+
MiniTestWrap -.-> TestCache
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Configuration Patterns
|
|
306
|
+
|
|
307
|
+
### Model Setup
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
class Status < ApplicationRecord
|
|
311
|
+
include SupportTableCache
|
|
312
|
+
|
|
313
|
+
# Cache by single unique attribute
|
|
314
|
+
cache_by :name, case_sensitive: false
|
|
315
|
+
|
|
316
|
+
# Cache by composite unique key
|
|
317
|
+
cache_by [:group, :name]
|
|
318
|
+
|
|
319
|
+
# Cache by id (for associations)
|
|
320
|
+
cache_by :id
|
|
321
|
+
|
|
322
|
+
# Optional: Set TTL for cache entries
|
|
323
|
+
self.support_table_cache_ttl = 5.minutes
|
|
324
|
+
|
|
325
|
+
# Optional: Use in-memory cache
|
|
326
|
+
self.support_table_cache = :memory
|
|
327
|
+
end
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Association Setup
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
class Order < ApplicationRecord
|
|
334
|
+
include SupportTableCache::Associations
|
|
335
|
+
|
|
336
|
+
belongs_to :status
|
|
337
|
+
cache_belongs_to :status
|
|
338
|
+
end
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Performance Benefits
|
|
342
|
+
|
|
343
|
+
### Query Elimination
|
|
344
|
+
|
|
345
|
+
```mermaid
|
|
346
|
+
sequenceDiagram
|
|
347
|
+
participant App as Application
|
|
348
|
+
participant Cache as Cache
|
|
349
|
+
participant DB as Database
|
|
350
|
+
|
|
351
|
+
Note over App,DB: Without Cache
|
|
352
|
+
App->>DB: Status.find_by(name: 'active')
|
|
353
|
+
DB-->>App: Record
|
|
354
|
+
App->>DB: Status.find_by(name: 'active')
|
|
355
|
+
DB-->>App: Same Record (redundant query)
|
|
356
|
+
App->>DB: Status.find_by(name: 'active')
|
|
357
|
+
DB-->>App: Same Record (redundant query)
|
|
358
|
+
|
|
359
|
+
Note over App,DB: With Cache
|
|
360
|
+
App->>Cache: Status.find_by(name: 'active')
|
|
361
|
+
Cache->>DB: First query only
|
|
362
|
+
DB-->>Cache: Record
|
|
363
|
+
Cache-->>App: Record
|
|
364
|
+
App->>Cache: Status.find_by(name: 'active')
|
|
365
|
+
Cache-->>App: Record (from cache)
|
|
366
|
+
App->>Cache: Status.find_by(name: 'active')
|
|
367
|
+
Cache-->>App: Record (from cache)
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## Design Principles
|
|
371
|
+
|
|
372
|
+
1. **Transparent Integration**: No code changes required beyond configuration
|
|
373
|
+
2. **Selective Caching**: Only caches queries that match configured unique keys
|
|
374
|
+
3. **Automatic Invalidation**: Cache entries are cleared when records change
|
|
375
|
+
4. **Flexible Cache Backends**: Supports various cache stores including in-memory
|
|
376
|
+
5. **Test Isolation**: Provides testing utilities to prevent cache pollution
|
|
377
|
+
6. **Performance Optimization**: Minimizes database queries for frequently accessed lookup data
|
|
378
|
+
|
|
379
|
+
## Use Cases
|
|
380
|
+
|
|
381
|
+
- **Status/Type Tables**: Small enums stored in database tables
|
|
382
|
+
- **Configuration Tables**: Application settings and parameters
|
|
383
|
+
- **Reference Data**: Countries, states, categories, etc.
|
|
384
|
+
- **Lookup Tables**: Any small, rarely-changing reference data
|
|
385
|
+
|
|
386
|
+
This architecture enables significant performance improvements for applications that heavily query small support tables while maintaining data consistency and providing flexible caching options.
|
data/CHANGELOG.md
CHANGED
|
@@ -4,24 +4,41 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## 1.1.5
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Replaced thread local variables with fiber local variables to prevent the possibility of behavior from leaking across fibers when disabling the cache in a block.
|
|
12
|
+
- Allow setting the cache to an in-memory cache by setting `support_table_cache` to `true`.
|
|
13
|
+
|
|
14
|
+
## 1.1.4
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed issue where using `find_by` on a `has_many` relation would not take the scope of the relation into account when looking up the cached record. Now chaining a `find_by` onto a `has_many` relation will correctly bypass the cache and directly query the database.
|
|
19
|
+
|
|
7
20
|
## 1.1.3
|
|
8
21
|
|
|
9
22
|
### Fixed
|
|
23
|
+
|
|
10
24
|
- Avoid calling methods that require a database connection when setting up belongs to caching.
|
|
11
25
|
|
|
12
26
|
## 1.1.2
|
|
13
27
|
|
|
14
28
|
### Fixed
|
|
29
|
+
|
|
15
30
|
- Do not cache records where only some of the columns have been loaded with a call to `select`.
|
|
16
31
|
|
|
17
32
|
## 1.1.1
|
|
18
33
|
|
|
19
34
|
### Fixed
|
|
35
|
+
|
|
20
36
|
- Fixed disabled and disable_cache methods to yield a block to match the documentation.
|
|
21
37
|
|
|
22
38
|
## 1.1.0
|
|
23
39
|
|
|
24
40
|
### Added
|
|
41
|
+
|
|
25
42
|
- Added fetch_by and fetch_by! methods that can verify the result will be cacheable.
|
|
26
43
|
- Allow configuring cache storage on a per class basis.
|
|
27
44
|
- Allow disabling caching on per class basis.
|
|
@@ -30,15 +47,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
30
47
|
- Added test mode to intialize new caches within a test block.
|
|
31
48
|
|
|
32
49
|
### Changed
|
|
50
|
+
|
|
33
51
|
- Changed fiber local variables used for disabling the cache to thread local variables.
|
|
34
52
|
- Using find_by! on a relation will now use the cache.
|
|
35
53
|
|
|
36
54
|
## 1.0.1
|
|
37
55
|
|
|
38
56
|
### Added
|
|
57
|
+
|
|
39
58
|
- Preserve scope on relations terminated with a `find_by`.
|
|
40
59
|
|
|
41
60
|
## 1.0.0
|
|
42
61
|
|
|
43
62
|
### Added
|
|
63
|
+
|
|
44
64
|
- Add SupportTableCache concern to enable automatic caching on models when calling `find_by` with unique key parameters.
|
data/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bdurand/support_table_cache/actions/workflows/continuous_integration.yml)
|
|
4
4
|
[](https://github.com/testdouble/standard)
|
|
5
|
+
[](https://badge.fury.io/rb/support_table_cache)
|
|
5
6
|
|
|
6
7
|
This gem adds caching for ActiveRecord support table models. These models have a unique key (i.e. a unique `name` attribute, etc.) and a limited number of entries (a few hundred at most). These are often models added to normalize the data structure and are also known as lookup tables.
|
|
7
8
|
|
|
@@ -18,6 +19,18 @@ end
|
|
|
18
19
|
|
|
19
20
|
With this gem, you can avoid the database query associated with the `find_by` call. You don't need to alter your code in any way other than to include `SupportTableCache` in your model and telling it the attributes that comprise a unique key, which can be used for caching.
|
|
20
21
|
|
|
22
|
+
## Table of Contents
|
|
23
|
+
|
|
24
|
+
- [Usage](#usage)
|
|
25
|
+
- [Setting the Cache](#setting-the-cache)
|
|
26
|
+
- [Disabling Caching](#disabling-caching)
|
|
27
|
+
- [Caching Belongs to Associations](#caching-belongs-to-associations)
|
|
28
|
+
- [Testing](#testing)
|
|
29
|
+
- [Companion Gems](#companion-gems)
|
|
30
|
+
- [Installation](#installation)
|
|
31
|
+
- [Contributing](#contributing)
|
|
32
|
+
- [License](#license)
|
|
33
|
+
|
|
21
34
|
## Usage
|
|
22
35
|
|
|
23
36
|
To use the gem, you need to include it in you models and then specify which attributes can be used for caching with the `cache_by` method. A caching attribute must be a unique key on the model. For a composite unique key, you can specify an array of attributes. If any of the attributes are case-insensitive strings, you need to specify that as well.
|
|
@@ -114,6 +127,9 @@ end
|
|
|
114
127
|
|
|
115
128
|
You can include `SupportTableCache::Associations` in your `ApplicationRecord` class to make association caching available on all models.
|
|
116
129
|
|
|
130
|
+
> [!NOTE]
|
|
131
|
+
> You still need to set up the target model to cache by the primary key used by the belongs to association. Otherwise the association will not be cached.
|
|
132
|
+
|
|
117
133
|
### Testing
|
|
118
134
|
|
|
119
135
|
Caching may interfere with tests by allowing data created in one test to leak into subsequent tests. You can resolve this by wrapping your tests with the `SupportTableCache.testing!` method.
|
|
@@ -131,14 +147,16 @@ class MiniTest::Spec
|
|
|
131
147
|
around do |tests|
|
|
132
148
|
SupportTableCache.testing!(&tests)
|
|
133
149
|
end
|
|
134
|
-
|
|
135
|
-
|
|
150
|
+
end
|
|
136
151
|
```
|
|
137
152
|
|
|
138
|
-
###
|
|
153
|
+
### Companion Gems
|
|
139
154
|
|
|
140
155
|
You can use the companion [support_table_data gem](https://github.com/bdurand/support_table_data) to provide functionality for loading static data into your support tables as well as adding helper functions to make looking up specific rows much easier.
|
|
141
156
|
|
|
157
|
+
> [!TIP]
|
|
158
|
+
> The [support_table](https://github.com/bdurand/support_table) gem combines both gems in a drop in solution for Rails applications.
|
|
159
|
+
|
|
142
160
|
## Installation
|
|
143
161
|
|
|
144
162
|
Add this line to your application's Gemfile:
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.5
|
|
@@ -14,7 +14,7 @@ module SupportTableCache
|
|
|
14
14
|
#
|
|
15
15
|
# @param association_name [Symbol, String] The association name to cache.
|
|
16
16
|
# @return [void]
|
|
17
|
-
# @raise ArgumentError if the association is not defined or if it has a runtime scope.
|
|
17
|
+
# @raise [ArgumentError] if the association is not defined or if it has a runtime scope.
|
|
18
18
|
def cache_belongs_to(association_name)
|
|
19
19
|
reflection = reflections[association_name.to_s]
|
|
20
20
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SupportTableCache
|
|
4
|
+
# Utility class for managing fiber-local variables. This implementation
|
|
5
|
+
# does not pollute the global namespace.
|
|
6
|
+
class FiberLocals
|
|
7
|
+
def initialize
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
@locals = {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def [](key)
|
|
13
|
+
fiber_locals = nil
|
|
14
|
+
@mutex.synchronize do
|
|
15
|
+
fiber_locals = @locals[Fiber.current.object_id]
|
|
16
|
+
end
|
|
17
|
+
return nil if fiber_locals.nil?
|
|
18
|
+
|
|
19
|
+
fiber_locals[key]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def with(key, value)
|
|
23
|
+
fiber_id = Fiber.current.object_id
|
|
24
|
+
fiber_locals = nil
|
|
25
|
+
previous_value = nil
|
|
26
|
+
inited_vars = false
|
|
27
|
+
|
|
28
|
+
begin
|
|
29
|
+
@mutex.synchronize do
|
|
30
|
+
fiber_locals = @locals[fiber_id]
|
|
31
|
+
if fiber_locals.nil?
|
|
32
|
+
fiber_locals = {}
|
|
33
|
+
@locals[fiber_id] = fiber_locals
|
|
34
|
+
inited_vars = true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
previous_value = fiber_locals[key]
|
|
39
|
+
fiber_locals[key] = value
|
|
40
|
+
|
|
41
|
+
yield
|
|
42
|
+
ensure
|
|
43
|
+
if inited_vars
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
@locals.delete(fiber_id)
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
fiber_locals[key] = previous_value
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -39,7 +39,8 @@ module SupportTableCache
|
|
|
39
39
|
# Same as find_by, but performs a safety check to confirm the query will hit the cache.
|
|
40
40
|
#
|
|
41
41
|
# @param attributes [Hash] Attributes to find the record by.
|
|
42
|
-
# @
|
|
42
|
+
# @return [ActiveRecord::Base, nil] The found record or nil if not found.
|
|
43
|
+
# @raise [ArgumentError] if the query cannot use the cache.
|
|
43
44
|
def fetch_by(attributes)
|
|
44
45
|
find_by_attribute_names = support_table_find_by_attribute_names(attributes)
|
|
45
46
|
unless support_table_cache_by_attributes.any? { |attribute_names, _ci, _where| attribute_names == find_by_attribute_names }
|
|
@@ -51,8 +52,9 @@ module SupportTableCache
|
|
|
51
52
|
# Same as find_by!, but performs a safety check to confirm the query will hit the cache.
|
|
52
53
|
#
|
|
53
54
|
# @param attributes [Hash] Attributes to find the record by.
|
|
54
|
-
# @
|
|
55
|
-
# @raise
|
|
55
|
+
# @return [ActiveRecord::Base] The found record.
|
|
56
|
+
# @raise [ArgumentError] if the query cannot use the cache.
|
|
57
|
+
# @raise [ActiveRecord::RecordNotFound] if no record is found.
|
|
56
58
|
def fetch_by!(attributes)
|
|
57
59
|
value = fetch_by(attributes)
|
|
58
60
|
if value.nil?
|
|
@@ -8,11 +8,20 @@ module SupportTableCache
|
|
|
8
8
|
# This cache will not store nil values. This is to prevent the cache from filling up with
|
|
9
9
|
# cache misses because there is no purging mechanism.
|
|
10
10
|
class MemoryCache
|
|
11
|
+
# Create a new memory cache.
|
|
12
|
+
#
|
|
13
|
+
# @return [SupportTableCache::MemoryCache]
|
|
11
14
|
def initialize
|
|
12
15
|
@cache = {}
|
|
13
16
|
@mutex = Mutex.new
|
|
14
17
|
end
|
|
15
18
|
|
|
19
|
+
# Fetch a value from the cache. If the key is not found or has expired, yields to get a new value.
|
|
20
|
+
#
|
|
21
|
+
# @param key [Object] The cache key.
|
|
22
|
+
# @param expires_in [Integer, nil] Time in seconds until the cached value expires.
|
|
23
|
+
# @yield Block to execute to get a new value if the key is not cached.
|
|
24
|
+
# @return [Object, nil] The cached value or the result of the block, or nil if no value is found.
|
|
16
25
|
def fetch(key, expires_in: nil)
|
|
17
26
|
serialized_value, expire_at = @cache[key]
|
|
18
27
|
if serialized_value.nil? || (expire_at && expire_at < Process.clock_gettime(Process::CLOCK_MONOTONIC))
|
|
@@ -24,10 +33,20 @@ module SupportTableCache
|
|
|
24
33
|
Marshal.load(serialized_value)
|
|
25
34
|
end
|
|
26
35
|
|
|
36
|
+
# Read a value from the cache.
|
|
37
|
+
#
|
|
38
|
+
# @param key [Object] The cache key.
|
|
39
|
+
# @return [Object, nil] The cached value or nil if not found.
|
|
27
40
|
def read(key)
|
|
28
41
|
fetch(key)
|
|
29
42
|
end
|
|
30
43
|
|
|
44
|
+
# Write a value to the cache.
|
|
45
|
+
#
|
|
46
|
+
# @param key [Object] The cache key.
|
|
47
|
+
# @param value [Object] The value to cache. Nil values are not cached.
|
|
48
|
+
# @param expires_in [Integer, nil] Time in seconds until the cached value expires.
|
|
49
|
+
# @return [void]
|
|
31
50
|
def write(key, value, expires_in: nil)
|
|
32
51
|
return if value.nil?
|
|
33
52
|
|
|
@@ -42,10 +61,17 @@ module SupportTableCache
|
|
|
42
61
|
end
|
|
43
62
|
end
|
|
44
63
|
|
|
64
|
+
# Delete a value from the cache.
|
|
65
|
+
#
|
|
66
|
+
# @param key [Object] The cache key.
|
|
67
|
+
# @return [void]
|
|
45
68
|
def delete(key)
|
|
46
69
|
@cache.delete(key)
|
|
47
70
|
end
|
|
48
71
|
|
|
72
|
+
# Clear all values from the cache.
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
49
75
|
def clear
|
|
50
76
|
@cache.clear
|
|
51
77
|
end
|
|
@@ -5,12 +5,18 @@ module SupportTableCache
|
|
|
5
5
|
|
|
6
6
|
module RelationOverride
|
|
7
7
|
# Override for the find_by method that looks in the cache first.
|
|
8
|
+
#
|
|
9
|
+
# @param args [Array<Object>] Arguments passed to find_by.
|
|
10
|
+
# @return [ActiveRecord::Base, nil] The found record or nil if not found.
|
|
8
11
|
def find_by(*args)
|
|
9
12
|
return super unless klass.include?(SupportTableCache)
|
|
10
13
|
|
|
11
14
|
cache = klass.send(:current_support_table_cache)
|
|
12
15
|
return super unless cache
|
|
13
16
|
|
|
17
|
+
# Skip caching for has_many or has_many :through associations
|
|
18
|
+
return super if is_a?(ActiveRecord::Associations::CollectionProxy)
|
|
19
|
+
|
|
14
20
|
return super if select_values.present?
|
|
15
21
|
|
|
16
22
|
cache_key = nil
|
|
@@ -43,7 +49,10 @@ module SupportTableCache
|
|
|
43
49
|
end
|
|
44
50
|
|
|
45
51
|
# Override for the find_by! method that looks in the cache first.
|
|
46
|
-
#
|
|
52
|
+
#
|
|
53
|
+
# @param args [Array<Object>] Arguments passed to find_by!.
|
|
54
|
+
# @return [ActiveRecord::Base] The found record.
|
|
55
|
+
# @raise [ActiveRecord::RecordNotFound] if no record is found.
|
|
47
56
|
def find_by!(*args)
|
|
48
57
|
value = find_by(*args)
|
|
49
58
|
unless value
|
|
@@ -55,7 +64,8 @@ module SupportTableCache
|
|
|
55
64
|
# Same as find_by, but performs a safety check to confirm the query will hit the cache.
|
|
56
65
|
#
|
|
57
66
|
# @param attributes [Hash] Attributes to find the record by.
|
|
58
|
-
# @
|
|
67
|
+
# @return [ActiveRecord::Base, nil] The found record or nil if not found.
|
|
68
|
+
# @raise [ArgumentError] if the query cannot use the cache.
|
|
59
69
|
def fetch_by(attributes)
|
|
60
70
|
find_by_attribute_names = support_table_find_by_attribute_names(attributes)
|
|
61
71
|
unless klass.support_table_cache_by_attributes.any? { |attribute_names, _ci| attribute_names == find_by_attribute_names }
|
|
@@ -67,8 +77,9 @@ module SupportTableCache
|
|
|
67
77
|
# Same as find_by!, but performs a safety check to confirm the query will hit the cache.
|
|
68
78
|
#
|
|
69
79
|
# @param attributes [Hash] Attributes to find the record by.
|
|
70
|
-
# @
|
|
71
|
-
# @raise
|
|
80
|
+
# @return [ActiveRecord::Base] The found record.
|
|
81
|
+
# @raise [ArgumentError] if the query cannot use the cache.
|
|
82
|
+
# @raise [ActiveRecord::RecordNotFound] if no record is found.
|
|
72
83
|
def fetch_by!(attributes)
|
|
73
84
|
value = fetch_by(attributes)
|
|
74
85
|
if value.nil?
|
data/lib/support_table_cache.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "support_table_cache/associations"
|
|
4
|
+
require_relative "support_table_cache/fiber_locals"
|
|
4
5
|
require_relative "support_table_cache/find_by_override"
|
|
5
6
|
require_relative "support_table_cache/relation_override"
|
|
6
7
|
require_relative "support_table_cache/memory_cache"
|
|
@@ -10,6 +11,13 @@ require_relative "support_table_cache/memory_cache"
|
|
|
10
11
|
module SupportTableCache
|
|
11
12
|
extend ActiveSupport::Concern
|
|
12
13
|
|
|
14
|
+
NOT_SET = Object.new.freeze
|
|
15
|
+
private_constant :NOT_SET
|
|
16
|
+
|
|
17
|
+
@fiber_locals = FiberLocals.new
|
|
18
|
+
@cache = NOT_SET
|
|
19
|
+
@disabled = false
|
|
20
|
+
|
|
13
21
|
included do
|
|
14
22
|
# @api private Used to store the list of attribute names used for caching.
|
|
15
23
|
class_attribute :support_table_cache_by_attributes, instance_accessor: false
|
|
@@ -39,22 +47,17 @@ module SupportTableCache
|
|
|
39
47
|
# for a class will always take precedence over the global setting.
|
|
40
48
|
#
|
|
41
49
|
# @param disabled [Boolean] Caching will be disabled if this is true and enabled if false.
|
|
42
|
-
# @
|
|
50
|
+
# @yield Executes the provided block with caching disabled or enabled.
|
|
51
|
+
# @return [Object] The return value of the block.
|
|
43
52
|
def disable_cache(disabled = true, &block)
|
|
44
|
-
|
|
45
|
-
save_val = Thread.current.thread_variable_get(varname)
|
|
46
|
-
begin
|
|
47
|
-
Thread.current.thread_variable_set(varname, !!disabled)
|
|
48
|
-
yield
|
|
49
|
-
ensure
|
|
50
|
-
Thread.current.thread_variable_set(varname, save_val)
|
|
51
|
-
end
|
|
53
|
+
SupportTableCache.with_fiber_local("support_table_cache_disabled:#{name}", !!disabled, &block)
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
# Enable the caching behavior for this class within the block. The enabled setting
|
|
55
57
|
# for a class will always take precedence over the global setting.
|
|
56
58
|
#
|
|
57
|
-
# @
|
|
59
|
+
# @yield Executes the provided block with caching enabled.
|
|
60
|
+
# @return [Object] The return value of the block.
|
|
58
61
|
def enable_cache(&block)
|
|
59
62
|
disable_cache(false, &block)
|
|
60
63
|
end
|
|
@@ -69,7 +72,7 @@ module SupportTableCache
|
|
|
69
72
|
|
|
70
73
|
find_each do |record|
|
|
71
74
|
support_table_cache_by_attributes.each do |attribute_names, case_sensitive|
|
|
72
|
-
attributes = record.attributes.
|
|
75
|
+
attributes = record.attributes.slice(*attribute_names)
|
|
73
76
|
cache_key = SupportTableCache.cache_key(self, attributes, attribute_names, case_sensitive)
|
|
74
77
|
cache.fetch(cache_key, expires_in: support_table_cache_ttl) { record }
|
|
75
78
|
end
|
|
@@ -79,26 +82,26 @@ module SupportTableCache
|
|
|
79
82
|
# Set a class-specific cache to use in lieu of the global cache.
|
|
80
83
|
#
|
|
81
84
|
# @param cache [ActiveSupport::Cache::Store, Symbol] The cache instance to use. You can also
|
|
82
|
-
# specify the value :memory to use an optimized in-memory cache.
|
|
85
|
+
# specify the value :memory or true to use an optimized in-memory cache.
|
|
83
86
|
# @return [void]
|
|
84
87
|
def support_table_cache=(cache)
|
|
85
|
-
cache = MemoryCache.new if cache == :memory
|
|
88
|
+
cache = MemoryCache.new if cache == :memory || cache == true
|
|
86
89
|
self.support_table_cache_impl = cache
|
|
87
90
|
end
|
|
88
91
|
|
|
89
92
|
protected
|
|
90
93
|
|
|
91
94
|
# Specify which attributes can be used for looking up records in the cache. Each value must
|
|
92
|
-
# define a unique key
|
|
95
|
+
# define a unique key. Multiple unique keys can be specified.
|
|
93
96
|
#
|
|
94
97
|
# If multiple attributes are used to make up a unique key, then they should be passed in as an array.
|
|
95
98
|
#
|
|
96
99
|
# If you need to remove caching setup in a superclass, you can pass in the value false to reset
|
|
97
100
|
# cache behavior on the class.
|
|
98
101
|
#
|
|
99
|
-
# @param attributes [String, Symbol, Array<String, Symbol>,
|
|
100
|
-
# @param case_sensitive [Boolean] Indicate if strings should treated as case insensitive in the key.
|
|
101
|
-
# @param where [Hash] A hash representing a hard coded set of attributes that must match a query in order
|
|
102
|
+
# @param attributes [String, Symbol, Array<String, Symbol>, false] Attributes that make up a unique key.
|
|
103
|
+
# @param case_sensitive [Boolean] Indicate if strings should be treated as case insensitive in the key.
|
|
104
|
+
# @param where [Hash, nil] A hash representing a hard coded set of attributes that must match a query in order
|
|
102
105
|
# to cache the result. If a model has a default scope, then this value should be set to match the
|
|
103
106
|
# where clause in that scope.
|
|
104
107
|
# @return [void]
|
|
@@ -125,7 +128,7 @@ module SupportTableCache
|
|
|
125
128
|
private
|
|
126
129
|
|
|
127
130
|
def support_table_cache_disabled?
|
|
128
|
-
current_block_value =
|
|
131
|
+
current_block_value = SupportTableCache.fiber_local_value("support_table_cache_disabled:#{name}")
|
|
129
132
|
if current_block_value.nil?
|
|
130
133
|
SupportTableCache.disabled?
|
|
131
134
|
else
|
|
@@ -144,16 +147,11 @@ module SupportTableCache
|
|
|
144
147
|
# disabled for that block. If no block is specified, then caching is disabled globally.
|
|
145
148
|
#
|
|
146
149
|
# @param disabled [Boolean] Caching will be disabled if this is true and enabled if false.
|
|
147
|
-
# @
|
|
150
|
+
# @yield Executes the provided block with caching disabled or enabled (if block is given).
|
|
151
|
+
# @return [Object, nil] The return value of the block if a block is given, nil otherwise.
|
|
148
152
|
def disable(disabled = true, &block)
|
|
149
153
|
if block
|
|
150
|
-
|
|
151
|
-
begin
|
|
152
|
-
Thread.current.thread_variable_set(:support_table_cache_disabled, !!disabled)
|
|
153
|
-
yield
|
|
154
|
-
ensure
|
|
155
|
-
Thread.current.thread_variable_set(:support_table_cache_disabled, save_val)
|
|
156
|
-
end
|
|
154
|
+
SupportTableCache.with_fiber_local("support_table_cache_disabled", !!disabled, &block)
|
|
157
155
|
else
|
|
158
156
|
@disabled = !!disabled
|
|
159
157
|
end
|
|
@@ -162,7 +160,8 @@ module SupportTableCache
|
|
|
162
160
|
# Enable the caching behavior for all classes. If a block is specified, then caching is only
|
|
163
161
|
# enabled for that block. If no block is specified, then caching is enabled globally.
|
|
164
162
|
#
|
|
165
|
-
# @
|
|
163
|
+
# @yield Executes the provided block with caching enabled (if block is given).
|
|
164
|
+
# @return [Object, nil] The return value of the block if a block is given, nil otherwise.
|
|
166
165
|
def enable(&block)
|
|
167
166
|
disable(false, &block)
|
|
168
167
|
end
|
|
@@ -170,9 +169,9 @@ module SupportTableCache
|
|
|
170
169
|
# Return true if caching has been disabled.
|
|
171
170
|
# @return [Boolean]
|
|
172
171
|
def disabled?
|
|
173
|
-
block_value =
|
|
172
|
+
block_value = SupportTableCache.fiber_local_value("support_table_cache_disabled")
|
|
174
173
|
if block_value.nil?
|
|
175
|
-
|
|
174
|
+
!!@disabled
|
|
176
175
|
else
|
|
177
176
|
block_value
|
|
178
177
|
end
|
|
@@ -193,7 +192,7 @@ module SupportTableCache
|
|
|
193
192
|
def cache
|
|
194
193
|
if testing_cache
|
|
195
194
|
testing_cache
|
|
196
|
-
elsif
|
|
195
|
+
elsif @cache != NOT_SET
|
|
197
196
|
@cache
|
|
198
197
|
elsif defined?(Rails.cache)
|
|
199
198
|
Rails.cache
|
|
@@ -204,27 +203,25 @@ module SupportTableCache
|
|
|
204
203
|
# can use this to wrap your test methods so that cached values from one test don't show up
|
|
205
204
|
# in subsequent tests.
|
|
206
205
|
#
|
|
207
|
-
# @
|
|
206
|
+
# @yield Executes the provided block in test mode.
|
|
207
|
+
# @return [Object] The return value of the block.
|
|
208
208
|
def testing!(&block)
|
|
209
|
-
save_val =
|
|
209
|
+
save_val = SupportTableCache.fiber_local_value("support_table_cache_test_cache")
|
|
210
210
|
if save_val.nil?
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
begin
|
|
211
|
+
SupportTableCache.with_fiber_local("support_table_cache_test_cache", MemoryCache.new, &block)
|
|
212
|
+
else
|
|
214
213
|
yield
|
|
215
|
-
ensure
|
|
216
|
-
Thread.current.thread_variable_set(:support_table_cache_test_cache, save_val)
|
|
217
214
|
end
|
|
218
215
|
end
|
|
219
216
|
|
|
220
217
|
# Get the current test mode cache. This will only return a value inside of a `testing!` block.
|
|
221
218
|
#
|
|
222
|
-
# @return [SupportTableCache::MemoryCache]
|
|
219
|
+
# @return [SupportTableCache::MemoryCache, nil] The test cache or nil if not in test mode.
|
|
223
220
|
# @api private
|
|
224
221
|
def testing_cache
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
return nil if @cache.nil?
|
|
223
|
+
|
|
224
|
+
SupportTableCache.fiber_local_value("support_table_cache_test_cache")
|
|
228
225
|
end
|
|
229
226
|
|
|
230
227
|
# Generate a consistent cache key for a set of attributes. It will return nil if the attributes
|
|
@@ -232,9 +229,9 @@ module SupportTableCache
|
|
|
232
229
|
#
|
|
233
230
|
# @param klass [Class] The class that is being cached.
|
|
234
231
|
# @param attributes [Hash] The attributes used to find a record.
|
|
235
|
-
# @param key_attribute_names [Array] List of attributes that can be used as a key in the cache.
|
|
232
|
+
# @param key_attribute_names [Array<String>] List of attributes that can be used as a key in the cache.
|
|
236
233
|
# @param case_sensitive [Boolean] Indicator if string values are case-sensitive in the cache key.
|
|
237
|
-
# @return [String]
|
|
234
|
+
# @return [Array(String, Hash), nil] A two-element array with the class name and attributes hash, or nil if not cacheable.
|
|
238
235
|
# @api private
|
|
239
236
|
def cache_key(klass, attributes, key_attribute_names, case_sensitive)
|
|
240
237
|
return nil if attributes.blank? || key_attribute_names.blank?
|
|
@@ -253,6 +250,14 @@ module SupportTableCache
|
|
|
253
250
|
|
|
254
251
|
[klass.name, sorted_attributes]
|
|
255
252
|
end
|
|
253
|
+
|
|
254
|
+
def fiber_local_value(varname)
|
|
255
|
+
@fiber_locals[varname]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def with_fiber_local(varname, value, &block)
|
|
259
|
+
@fiber_locals.with(varname, value, &block)
|
|
260
|
+
end
|
|
256
261
|
end
|
|
257
262
|
|
|
258
263
|
# Remove the cache entry for this record.
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: support_table_cache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brian Durand
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -38,19 +37,21 @@ dependencies:
|
|
|
38
37
|
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '0'
|
|
41
|
-
description:
|
|
42
40
|
email:
|
|
43
41
|
- bbdurand@gmail.com
|
|
44
42
|
executables: []
|
|
45
43
|
extensions: []
|
|
46
44
|
extra_rdoc_files: []
|
|
47
45
|
files:
|
|
46
|
+
- AGENTS.md
|
|
47
|
+
- ARCHITECTURE.md
|
|
48
48
|
- CHANGELOG.md
|
|
49
49
|
- MIT-LICENSE
|
|
50
50
|
- README.md
|
|
51
51
|
- VERSION
|
|
52
52
|
- lib/support_table_cache.rb
|
|
53
53
|
- lib/support_table_cache/associations.rb
|
|
54
|
+
- lib/support_table_cache/fiber_locals.rb
|
|
54
55
|
- lib/support_table_cache/find_by_override.rb
|
|
55
56
|
- lib/support_table_cache/memory_cache.rb
|
|
56
57
|
- lib/support_table_cache/relation_override.rb
|
|
@@ -59,7 +60,6 @@ homepage: https://github.com/bdurand/support_table_cache
|
|
|
59
60
|
licenses:
|
|
60
61
|
- MIT
|
|
61
62
|
metadata: {}
|
|
62
|
-
post_install_message:
|
|
63
63
|
rdoc_options: []
|
|
64
64
|
require_paths:
|
|
65
65
|
- lib
|
|
@@ -74,8 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '0'
|
|
76
76
|
requirements: []
|
|
77
|
-
rubygems_version:
|
|
78
|
-
signing_key:
|
|
77
|
+
rubygems_version: 4.0.3
|
|
79
78
|
specification_version: 4
|
|
80
79
|
summary: Automatic ActiveRecord caching for small support tables.
|
|
81
80
|
test_files: []
|