cache_sweeper 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/CODE_OF_CONDUCT.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +586 -0
- data/lib/cache_sweeper/async_worker.rb +121 -0
- data/lib/cache_sweeper/base.rb +34 -0
- data/lib/cache_sweeper/dsl.rb +32 -0
- data/lib/cache_sweeper/flush_middleware.rb +152 -0
- data/lib/cache_sweeper/loader.rb +368 -0
- data/lib/cache_sweeper/logger.rb +104 -0
- data/lib/cache_sweeper/railtie.rb +13 -0
- data/lib/cache_sweeper/version.rb +3 -0
- data/lib/cache_sweeper.rb +108 -0
- data/lib/tasks/cache_sweeper.rake +4 -0
- metadata +104 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 392da6714d4f37e50e0defdcde8bb441ea64409dc61e05bb041086048d317146
|
4
|
+
data.tar.gz: ef6299ad81c1828237b3c3b66c4d6b331df3af6f86d54d2d71fa066a7e5c5fde
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8d8aa522bf13d117ec228c7c4028fef58953d2dc7b2e002a5b51f273938df6e9ea7f09c8422dee558efb239e01187b63eaa3064a1f8d4623450d1cf94d07123d
|
7
|
+
data.tar.gz: 8f230984e6fcdf49af9aaa33cac2714d88b6ab1111d8881b0031801db5a53977649fed23123060cbee08545eb4b3634a03996460011cd9a76415544a8f0432cc
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
In the interest of fostering an open and welcoming environment, we as
|
6
|
+
contributors and maintainers pledge to making participation in our project and
|
7
|
+
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
+
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
+
orientation.
|
11
|
+
|
12
|
+
## Our Standards
|
13
|
+
|
14
|
+
Examples of behavior that contributes to creating a positive environment
|
15
|
+
include:
|
16
|
+
|
17
|
+
* Using welcoming and inclusive language
|
18
|
+
* Being respectful of differing viewpoints and experiences
|
19
|
+
* Gracefully accepting constructive criticism
|
20
|
+
* Focusing on what is best for the community
|
21
|
+
* Showing empathy towards other community members
|
22
|
+
|
23
|
+
Examples of unacceptable behavior by participants include:
|
24
|
+
|
25
|
+
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
+
advances
|
27
|
+
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
+
* Public or private harassment
|
29
|
+
* Publishing others' private information, such as a physical or electronic
|
30
|
+
address, without explicit permission
|
31
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
+
professional setting
|
33
|
+
|
34
|
+
## Our Responsibilities
|
35
|
+
|
36
|
+
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
+
behavior and are expected to take appropriate and fair corrective action in
|
38
|
+
response to any instances of unacceptable behavior.
|
39
|
+
|
40
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
+
threatening, offensive, or harmful.
|
45
|
+
|
46
|
+
## Scope
|
47
|
+
|
48
|
+
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
+
when an individual is representing the project or its community. Examples of
|
50
|
+
representing a project or community include using an official project e-mail
|
51
|
+
address, posting via an official social media account, or acting as an appointed
|
52
|
+
representative at an online or offline event. Representation of a project may be
|
53
|
+
further defined and clarified by project maintainers.
|
54
|
+
|
55
|
+
## Enforcement
|
56
|
+
|
57
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
+
reported by contacting the project team at rafayqayyum786@gmail.com. All
|
59
|
+
complaints will be reviewed and investigated and will result in a response that
|
60
|
+
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
+
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
+
Further details of specific enforcement policies may be posted separately.
|
63
|
+
|
64
|
+
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
+
faith may face temporary or permanent repercussions as determined by other
|
66
|
+
members of the project's leadership.
|
67
|
+
|
68
|
+
## Attribution
|
69
|
+
|
70
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
+
available at [https://contributor-covenant.org/version/1/4][version]
|
72
|
+
|
73
|
+
[homepage]: https://contributor-covenant.org
|
74
|
+
[version]: https://contributor-covenant.org/version/1/4/
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Rafay Qayyum
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,586 @@
|
|
1
|
+
# CacheSweeper
|
2
|
+
|
3
|
+
A flexible, rule-based cache invalidation gem for Rails applications. CacheSweeper enables you to define cache invalidation logic in dedicated sweeper classes, keeping your models clean and your cache logic organized. It supports batching, async jobs via Sidekiq, and association-aware cache sweeping with comprehensive logging.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Features](#features)
|
8
|
+
- [Installation](#installation)
|
9
|
+
- [Quick Start](#quick-start)
|
10
|
+
- [Configuration](#configuration)
|
11
|
+
- [Usage Examples](#usage-examples)
|
12
|
+
- [API Reference](#api-reference)
|
13
|
+
- [Logging](#logging)
|
14
|
+
- [Middleware](#middleware)
|
15
|
+
- [Testing](#testing)
|
16
|
+
- [Troubleshooting](#troubleshooting)
|
17
|
+
- [Contributing](#contributing)
|
18
|
+
- [License](#license)
|
19
|
+
|
20
|
+
## Features
|
21
|
+
|
22
|
+
- **Rule-based cache invalidation**: Define what changes should trigger cache clearing using a simple DSL
|
23
|
+
- **Flexible triggering**: Choose between instant cache deletion or request-level batching
|
24
|
+
- **Async processing**: Offload cache deletion to Sidekiq for scalability
|
25
|
+
- **Association support**: Invalidate cache for associated models when their attributes change
|
26
|
+
- **Multi-level configuration**: Control behavior globally, per-sweeper, or per-rule
|
27
|
+
- **Comprehensive logging**: Detailed logging for debugging with configurable levels
|
28
|
+
- **Performance monitoring**: Built-in performance logging for cache operations and timing
|
29
|
+
- **Error tracking**: Comprehensive error logging with context and stack traces
|
30
|
+
- **Clean model code**: Keep cache logic out of your models
|
31
|
+
- **Thread-safe**: Uses RequestStore for reliable multi-threaded operation
|
32
|
+
- **Efficient batch deletion**: Uses `Rails.cache.delete_multi` for optimal performance
|
33
|
+
- **Configurable batch sizes**: Control how many keys are deleted in each batch
|
34
|
+
|
35
|
+
## Installation
|
36
|
+
|
37
|
+
Add this line to your application's Gemfile:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
gem 'cache_sweeper', path: 'path/to/cache_sweeper'
|
41
|
+
```
|
42
|
+
|
43
|
+
Then run:
|
44
|
+
|
45
|
+
```sh
|
46
|
+
bundle install
|
47
|
+
```
|
48
|
+
|
49
|
+
### Dependencies
|
50
|
+
|
51
|
+
- **Rails**: 5.0+ (tested with Rails 6.x and 7.x)
|
52
|
+
- **Sidekiq**: Required for async cache deletion
|
53
|
+
- **RequestStore**: For thread-safe request-level storage
|
54
|
+
|
55
|
+
Add Sidekiq to your Gemfile:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
gem 'sidekiq'
|
59
|
+
gem 'request_store'
|
60
|
+
```
|
61
|
+
|
62
|
+
## Quick Start
|
63
|
+
|
64
|
+
### 1. Configure the Gem
|
65
|
+
|
66
|
+
Create `config/initializers/cache_sweeper.rb`:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
# Logging configuration
|
70
|
+
CacheSweeper.logger = Rails.logger
|
71
|
+
CacheSweeper.log_level = :info # :debug, :info, :warn, :error
|
72
|
+
|
73
|
+
# Global cache invalidation configuration (optional)
|
74
|
+
CacheSweeper.trigger = :request # :instant or :request
|
75
|
+
CacheSweeper.mode = :async # :async or :inline
|
76
|
+
CacheSweeper.queue = :low # Sidekiq queue name
|
77
|
+
CacheSweeper.sidekiq_options = { retry: false }
|
78
|
+
CacheSweeper.delete_multi_batch_size = 100 # Batch size for efficient cache deletion
|
79
|
+
```
|
80
|
+
|
81
|
+
### 2. Add Middleware (if using request-level batching)
|
82
|
+
|
83
|
+
Add to `config/application.rb`:
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
config.middleware.use CacheSweeperFlushMiddleware
|
87
|
+
```
|
88
|
+
|
89
|
+
### 3. Create Your First Sweeper
|
90
|
+
|
91
|
+
Create `app/cache_sweepers/product_sweeper.rb`:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class ProductSweeper < CacheSweeper::Base
|
95
|
+
# Configure this sweeper's behavior
|
96
|
+
sweeper_options trigger: :request, mode: :async, queue: :products
|
97
|
+
|
98
|
+
# Clear cache when name or price changes
|
99
|
+
watch attributes: [:name, :price], keys: ->(product) { ["product:#{product.id}"] }
|
100
|
+
|
101
|
+
# Clear cache when product is created, updated, or destroyed
|
102
|
+
watch attributes: [:name, :price], keys: ->(product) { ["products:index", "products:featured"] }
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
### 4. Start Sidekiq
|
107
|
+
|
108
|
+
```sh
|
109
|
+
bundle exec sidekiq
|
110
|
+
```
|
111
|
+
|
112
|
+
That's it! Your cache will now be automatically invalidated when products change.
|
113
|
+
|
114
|
+
## Configuration
|
115
|
+
|
116
|
+
### Global Configuration
|
117
|
+
|
118
|
+
Configure in `config/initializers/cache_sweeper.rb`:
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
# Logging configuration
|
122
|
+
CacheSweeper.logger = Rails.logger
|
123
|
+
CacheSweeper.log_level = :info # :debug, :info, :warn, :error
|
124
|
+
|
125
|
+
# Cache invalidation configuration (optional - has sensible defaults)
|
126
|
+
CacheSweeper.trigger = :request # :instant or :request
|
127
|
+
CacheSweeper.mode = :async # :async or :inline
|
128
|
+
CacheSweeper.queue = :low # Sidekiq queue name
|
129
|
+
CacheSweeper.sidekiq_options = { retry: false }
|
130
|
+
CacheSweeper.delete_multi_batch_size = 100 # Batch size for efficient cache deletion
|
131
|
+
```
|
132
|
+
|
133
|
+
### Sweeper-Level Configuration
|
134
|
+
|
135
|
+
Use the `sweeper_options` DSL for clean sweeper configuration:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
class OrderSweeper < CacheSweeper::Base
|
139
|
+
sweeper_options trigger: :request, mode: :async, queue: :orders
|
140
|
+
# ... watch rules
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
### Rule-Level Configuration
|
145
|
+
|
146
|
+
Override configuration for specific rules:
|
147
|
+
|
148
|
+
```ruby
|
149
|
+
class MixedSweeper < CacheSweeper::Base
|
150
|
+
# Instant deletion for critical data
|
151
|
+
watch attributes: [:name], keys: ->(obj) { ["instant:#{obj.id}"] },
|
152
|
+
trigger: :instant, mode: :inline
|
153
|
+
|
154
|
+
# Async processing for less critical data
|
155
|
+
watch attributes: [:description], keys: ->(obj) { ["async:#{obj.id}"] },
|
156
|
+
trigger: :request, mode: :async, queue: :background
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
### Configuration Precedence
|
161
|
+
|
162
|
+
Configuration is resolved in this order (highest to lowest priority):
|
163
|
+
|
164
|
+
1. **Rule-level** - Options passed to individual `watch` calls
|
165
|
+
2. **Sweeper-level** - Configuration set on the sweeper class
|
166
|
+
3. **Global-level** - Default configuration set globally
|
167
|
+
|
168
|
+
### Configuration Options
|
169
|
+
|
170
|
+
- **`trigger`**: `:instant` (delete immediately) or `:request` (batch until end of request)
|
171
|
+
- **`mode`**: `:async` (use Sidekiq) or `:inline` (synchronous)
|
172
|
+
- **`queue`**: Sidekiq queue name (e.g., `:low`, `:high`, `:background`)
|
173
|
+
- **`sidekiq_options`**: Hash of Sidekiq options (e.g., `{ retry: false, backtrace: true }`)
|
174
|
+
- **`delete_multi_batch_size`**: Number of keys to delete in each batch (default: 100)
|
175
|
+
|
176
|
+
## Usage Examples
|
177
|
+
|
178
|
+
### Basic Sweeper
|
179
|
+
|
180
|
+
```ruby
|
181
|
+
# app/cache_sweepers/product_sweeper.rb
|
182
|
+
class ProductSweeper < CacheSweeper::Base
|
183
|
+
watch attributes: [:name, :price], keys: ->(product) { ["product:#{product.id}"] }
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
### Association Sweeper
|
188
|
+
|
189
|
+
```ruby
|
190
|
+
# app/cache_sweepers/package_sweeper.rb
|
191
|
+
class PackageSweeper < CacheSweeper::Base
|
192
|
+
# Clear cache when package name changes
|
193
|
+
watch attributes: [:name], keys: ->(package) { ["package:#{package.id}"] }
|
194
|
+
|
195
|
+
# Clear cache when associated products change
|
196
|
+
watch :products, attributes: [:name], keys: ->(product) {
|
197
|
+
product.packages.map { |pkg| "package:#{pkg.id}" }
|
198
|
+
}
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
### Conditional Cache Invalidation
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
# app/cache_sweepers/user_sweeper.rb
|
206
|
+
class UserSweeper < CacheSweeper::Base
|
207
|
+
# Clear cache when user profile changes
|
208
|
+
watch attributes: [:name, :email], keys: ->(user) { ["user:#{user.id}", "user:#{user.id}:profile"] }
|
209
|
+
|
210
|
+
# Clear cache with custom condition
|
211
|
+
watch attributes: [:last_login_at], keys: ->(user) { ["users:active"] },
|
212
|
+
if: ->(user) { user.last_login_at_changed? && user.last_login_at > 1.day.ago }
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
### Mixed Configuration Sweeper
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
# app/cache_sweepers/order_sweeper.rb
|
220
|
+
class OrderSweeper < CacheSweeper::Base
|
221
|
+
# Default configuration for this sweeper
|
222
|
+
sweeper_options trigger: :request, mode: :async, queue: :orders
|
223
|
+
|
224
|
+
# Critical data - instant deletion
|
225
|
+
watch attributes: [:status], keys: ->(order) { ["order:#{order.id}"] },
|
226
|
+
trigger: :instant, mode: :inline
|
227
|
+
|
228
|
+
# Less critical data - async processing
|
229
|
+
watch attributes: [:notes], keys: ->(order) { ["order:#{order.id}:notes"] },
|
230
|
+
trigger: :request, mode: :async, queue: :background
|
231
|
+
|
232
|
+
# Association changes
|
233
|
+
watch :order_items, attributes: [:quantity, :price], keys: ->(order_item) {
|
234
|
+
["order:#{order_item.order_id}", "order:#{order_item.order_id}:total"]
|
235
|
+
}
|
236
|
+
end
|
237
|
+
```
|
238
|
+
|
239
|
+
### Custom Callback Events
|
240
|
+
|
241
|
+
```ruby
|
242
|
+
# app/cache_sweepers/notification_sweeper.rb
|
243
|
+
class NotificationSweeper < CacheSweeper::Base
|
244
|
+
# Only clear cache on create and destroy, not update
|
245
|
+
watch attributes: [:message], keys: ->(notification) { ["notifications:count"] },
|
246
|
+
on: [:create, :destroy]
|
247
|
+
|
248
|
+
# Use before_commit instead of after_commit
|
249
|
+
watch attributes: [:read_at], keys: ->(notification) { ["user:#{notification.user_id}:unread_count"] },
|
250
|
+
callback: :before_commit
|
251
|
+
end
|
252
|
+
```
|
253
|
+
|
254
|
+
## API Reference
|
255
|
+
|
256
|
+
### Sweeper DSL
|
257
|
+
|
258
|
+
#### `watch(association = nil, **options)`
|
259
|
+
|
260
|
+
Define cache invalidation rules.
|
261
|
+
|
262
|
+
**Parameters:**
|
263
|
+
|
264
|
+
- **`association`** (optional): Association name to watch (e.g., `:products`, `:order_items`)
|
265
|
+
- **`attributes`**: Array of attributes to watch for changes (e.g., `[:name, :price]`)
|
266
|
+
- **`keys`**: Proc or array of cache keys to invalidate
|
267
|
+
- **`if`**: Proc or method name for conditional invalidation
|
268
|
+
- **`trigger`**: `:instant` or `:request` (per rule)
|
269
|
+
- **`mode`**: `:async` or `:inline` (per rule)
|
270
|
+
- **`queue`**: Sidekiq queue name (per rule)
|
271
|
+
- **`sidekiq_options`**: Hash of Sidekiq options (per rule)
|
272
|
+
- **`callback`**: Callback type (`:after_commit`, `:before_commit`, etc.)
|
273
|
+
- **`on`**: Events to watch (`[:create, :update, :destroy]`)
|
274
|
+
|
275
|
+
**Examples:**
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
# Basic usage
|
279
|
+
watch attributes: [:name], keys: ->(obj) { ["key:#{obj.id}"] }
|
280
|
+
|
281
|
+
# Association watching
|
282
|
+
watch :products, attributes: [:name], keys: ->(product) { ["product:#{product.id}"] }
|
283
|
+
|
284
|
+
# Conditional invalidation
|
285
|
+
watch attributes: [:status], keys: ->(obj) { ["key"] },
|
286
|
+
if: ->(obj) { obj.status == 'published' }
|
287
|
+
|
288
|
+
# Custom events
|
289
|
+
watch attributes: [:name], keys: ->(obj) { ["key"] },
|
290
|
+
on: [:create, :destroy]
|
291
|
+
```
|
292
|
+
|
293
|
+
#### `sweeper_options(**options)`
|
294
|
+
|
295
|
+
Configure sweeper-level behavior.
|
296
|
+
|
297
|
+
**Parameters:**
|
298
|
+
|
299
|
+
- **`trigger`**: `:instant` or `:request`
|
300
|
+
- **`mode`**: `:async` or `:inline`
|
301
|
+
- **`queue`**: Sidekiq queue name
|
302
|
+
- **`sidekiq_options`**: Hash of Sidekiq options
|
303
|
+
|
304
|
+
**Example:**
|
305
|
+
|
306
|
+
```ruby
|
307
|
+
class MySweeper < CacheSweeper::Base
|
308
|
+
sweeper_options trigger: :request, mode: :async, queue: :low
|
309
|
+
end
|
310
|
+
```
|
311
|
+
|
312
|
+
### Global Configuration
|
313
|
+
|
314
|
+
#### `CacheSweeper.logger = logger`
|
315
|
+
|
316
|
+
Set the logger for cache actions.
|
317
|
+
|
318
|
+
#### `CacheSweeper.log_level = level`
|
319
|
+
|
320
|
+
Set minimum log level (`:debug`, `:info`, `:warn`, `:error`).
|
321
|
+
|
322
|
+
#### Global Configuration Attributes
|
323
|
+
|
324
|
+
Configure global cache invalidation behavior using direct attribute assignment:
|
325
|
+
|
326
|
+
**Example:**
|
327
|
+
|
328
|
+
```ruby
|
329
|
+
CacheSweeper.trigger = :request
|
330
|
+
CacheSweeper.mode = :async
|
331
|
+
CacheSweeper.queue = :low
|
332
|
+
CacheSweeper.sidekiq_options = { retry: false }
|
333
|
+
```
|
334
|
+
|
335
|
+
## Logging
|
336
|
+
|
337
|
+
The gem provides comprehensive logging to help debug cache invalidation issues.
|
338
|
+
|
339
|
+
### Log Levels
|
340
|
+
|
341
|
+
- **`:debug`** - All logging enabled (initialization, rule execution, performance, cache operations, async jobs, middleware)
|
342
|
+
- **`:info`** - Important events (initialization, cache operations, async jobs, middleware)
|
343
|
+
- **`:warn`** - Warnings and errors only
|
344
|
+
- **`:error`** - Errors only
|
345
|
+
|
346
|
+
### Default Log Levels
|
347
|
+
|
348
|
+
- **Development**: `:debug` (all logging enabled)
|
349
|
+
- **Production**: `:warn` (warnings and errors only)
|
350
|
+
- **Other environments**: `:info`
|
351
|
+
|
352
|
+
### Log Output
|
353
|
+
|
354
|
+
Log output includes:
|
355
|
+
|
356
|
+
- **Initialization**: Sweeper loading and model attachment
|
357
|
+
- **Rule execution**: Which rules are triggered, condition evaluation, cache key generation
|
358
|
+
- **Performance**: Timing for cache operations and rule execution
|
359
|
+
- **Cache operations**: Cache key invalidation details
|
360
|
+
- **Async jobs**: Job scheduling and execution status
|
361
|
+
- **Middleware**: Request-level batching and flushing
|
362
|
+
- **Errors**: Detailed error information with context and stack traces
|
363
|
+
|
364
|
+
### Example Log Output
|
365
|
+
|
366
|
+
```
|
367
|
+
[CacheSweeper] [2024-01-15 10:30:45.123] [INFO] Initialization: Processing sweeper: ProductSweeper
|
368
|
+
[CacheSweeper] [2024-01-15 10:30:45.124] [DEBUG] Rule execution: ProductSweeper -> Product#123
|
369
|
+
[CacheSweeper] [2024-01-15 10:30:45.125] [INFO] Cache operations: Deleted instantly: product:123
|
370
|
+
[CacheSweeper] [2024-01-15 10:30:45.126] [DEBUG] Performance: cache_invalidation took 2.456ms
|
371
|
+
```
|
372
|
+
|
373
|
+
### Debugging Configuration
|
374
|
+
|
375
|
+
```ruby
|
376
|
+
# Enable all logging for debugging
|
377
|
+
CacheSweeper.logger = Rails.logger
|
378
|
+
CacheSweeper.log_level = :debug # Shows everything
|
379
|
+
|
380
|
+
# Or use different levels
|
381
|
+
CacheSweeper.log_level = :info # Shows important events only
|
382
|
+
CacheSweeper.log_level = :warn # Shows warnings and errors only
|
383
|
+
CacheSweeper.log_level = :error # Shows errors only
|
384
|
+
```
|
385
|
+
|
386
|
+
## Middleware
|
387
|
+
|
388
|
+
The `CacheSweeperFlushMiddleware` handles request-level batching. It automatically flushes all pending cache keys at the end of each request.
|
389
|
+
|
390
|
+
### Setup
|
391
|
+
|
392
|
+
Add to `config/application.rb`:
|
393
|
+
|
394
|
+
```ruby
|
395
|
+
config.middleware.use CacheSweeperFlushMiddleware
|
396
|
+
```
|
397
|
+
|
398
|
+
### How It Works
|
399
|
+
|
400
|
+
1. When `trigger: :request` is used, cache keys are batched during the request
|
401
|
+
2. At the end of the request, the middleware flushes all pending keys
|
402
|
+
3. Keys are processed according to their `mode` setting (`:async` or `:inline`)
|
403
|
+
|
404
|
+
### Middleware Order
|
405
|
+
|
406
|
+
Place the middleware after other middleware that might affect caching:
|
407
|
+
|
408
|
+
```ruby
|
409
|
+
# config/application.rb
|
410
|
+
config.middleware.use SomeOtherMiddleware
|
411
|
+
config.middleware.use CacheSweeperFlushMiddleware
|
412
|
+
```
|
413
|
+
|
414
|
+
## Testing
|
415
|
+
|
416
|
+
### Basic Testing
|
417
|
+
|
418
|
+
You can test sweepers using standard Rails/ActiveRecord test frameworks:
|
419
|
+
|
420
|
+
```ruby
|
421
|
+
# test/sweepers/product_sweeper_test.rb
|
422
|
+
class ProductSweeperTest < ActiveSupport::TestCase
|
423
|
+
test "clears cache when product name changes" do
|
424
|
+
product = Product.create!(name: "Original Name")
|
425
|
+
|
426
|
+
# Mock cache
|
427
|
+
Rails.cache.expects(:delete).with("product:#{product.id}")
|
428
|
+
|
429
|
+
product.update!(name: "New Name")
|
430
|
+
end
|
431
|
+
end
|
432
|
+
```
|
433
|
+
|
434
|
+
### Testing Async Jobs
|
435
|
+
|
436
|
+
For async jobs, ensure Sidekiq is running or stub the worker:
|
437
|
+
|
438
|
+
```ruby
|
439
|
+
# test/sweepers/async_sweeper_test.rb
|
440
|
+
class AsyncSweeperTest < ActiveSupport::TestCase
|
441
|
+
test "schedules async job for cache deletion" do
|
442
|
+
# Stub Sidekiq worker
|
443
|
+
CacheSweeper::AsyncWorker.expects(:perform_async).with(["key1", "key2"])
|
444
|
+
|
445
|
+
# Trigger the change
|
446
|
+
product = Product.create!(name: "Test Product")
|
447
|
+
end
|
448
|
+
end
|
449
|
+
```
|
450
|
+
|
451
|
+
### Testing with Sidekiq
|
452
|
+
|
453
|
+
For integration tests with Sidekiq:
|
454
|
+
|
455
|
+
```ruby
|
456
|
+
# test/integration/cache_sweeper_integration_test.rb
|
457
|
+
class CacheSweeperIntegrationTest < ActionDispatch::IntegrationTest
|
458
|
+
test "async cache deletion works end-to-end" do
|
459
|
+
# Ensure Sidekiq is running
|
460
|
+
Sidekiq::Testing.inline! do
|
461
|
+
product = Product.create!(name: "Test Product")
|
462
|
+
# Cache should be cleared synchronously
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
```
|
467
|
+
|
468
|
+
## Troubleshooting
|
469
|
+
|
470
|
+
### Common Issues
|
471
|
+
|
472
|
+
#### Sidekiq Not Running
|
473
|
+
|
474
|
+
**Problem**: Async cache deletion doesn't work.
|
475
|
+
|
476
|
+
**Solution**: Ensure Sidekiq is running:
|
477
|
+
|
478
|
+
```sh
|
479
|
+
bundle exec sidekiq
|
480
|
+
```
|
481
|
+
|
482
|
+
#### Sweepers Not Loading
|
483
|
+
|
484
|
+
**Problem**: Sweepers aren't being loaded or attached to models.
|
485
|
+
|
486
|
+
**Solution**:
|
487
|
+
1. Ensure sweepers are in `app/cache_sweepers/` directory
|
488
|
+
2. Ensure sweepers inherit from `CacheSweeper::Base`
|
489
|
+
3. Check that the sweeper files are named `*_sweeper.rb`
|
490
|
+
|
491
|
+
#### Cache Keys Not Being Cleared
|
492
|
+
|
493
|
+
**Problem**: Cache keys aren't being invalidated when models change.
|
494
|
+
|
495
|
+
**Solution**:
|
496
|
+
1. Enable debug logging: `CacheSweeper.log_level = :debug`
|
497
|
+
2. Check that the correct attributes are being watched
|
498
|
+
3. Verify cache key generation logic
|
499
|
+
4. Ensure the model callbacks are being triggered
|
500
|
+
|
501
|
+
#### Middleware Not Flushing
|
502
|
+
|
503
|
+
**Problem**: Request-level batching isn't flushing at the end of requests.
|
504
|
+
|
505
|
+
**Solution**:
|
506
|
+
1. Ensure `CacheSweeperFlushMiddleware` is added to the middleware stack
|
507
|
+
2. Check middleware order in `config/application.rb`
|
508
|
+
3. Verify that `trigger: :request` is being used
|
509
|
+
|
510
|
+
### Debugging Steps
|
511
|
+
|
512
|
+
1. **Enable comprehensive logging**:
|
513
|
+
```ruby
|
514
|
+
CacheSweeper.logger = Rails.logger
|
515
|
+
CacheSweeper.log_level = :debug
|
516
|
+
```
|
517
|
+
|
518
|
+
2. **Check sweeper loading**:
|
519
|
+
```ruby
|
520
|
+
# In Rails console
|
521
|
+
CacheSweeper::Base.descendants
|
522
|
+
```
|
523
|
+
|
524
|
+
3. **Verify model callbacks**:
|
525
|
+
```ruby
|
526
|
+
# In Rails console
|
527
|
+
Product._commit_callbacks.map(&:filter)
|
528
|
+
```
|
529
|
+
|
530
|
+
4. **Test cache key generation**:
|
531
|
+
```ruby
|
532
|
+
# In Rails console
|
533
|
+
product = Product.first
|
534
|
+
sweeper = ProductSweeper.new
|
535
|
+
# Test your key generation logic
|
536
|
+
```
|
537
|
+
|
538
|
+
### Performance Considerations
|
539
|
+
|
540
|
+
- **Use `:instant` trigger** for critical cache that must be cleared immediately
|
541
|
+
- **Use `:request` trigger** for less critical cache to reduce database load
|
542
|
+
- **Use `:async` mode** for high-volume applications to avoid blocking requests
|
543
|
+
- **Use `:inline` mode** for low-volume applications or when immediate consistency is required
|
544
|
+
- **Monitor Sidekiq queue sizes** to ensure async jobs are being processed
|
545
|
+
- **Optimize batch sizes**: Adjust `delete_multi_batch_size` based on your cache store's performance
|
546
|
+
- **Redis**: 100-500 keys per batch works well
|
547
|
+
- **Memcached**: 50-200 keys per batch is optimal
|
548
|
+
- **File store**: 10-50 keys per batch to avoid I/O bottlenecks
|
549
|
+
|
550
|
+
### Memory Usage
|
551
|
+
|
552
|
+
- Request-level batching stores cache keys in memory during the request
|
553
|
+
- For high-volume applications, consider using `:instant` trigger to avoid memory buildup
|
554
|
+
- Monitor RequestStore memory usage in production
|
555
|
+
|
556
|
+
## Contributing
|
557
|
+
|
558
|
+
1. Fork the repository
|
559
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
560
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
561
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
562
|
+
5. Create a new Pull Request
|
563
|
+
|
564
|
+
### Development Setup
|
565
|
+
|
566
|
+
1. Clone the repository
|
567
|
+
2. Run `bundle install`
|
568
|
+
3. Run tests with `bundle exec rspec`
|
569
|
+
4. Run linting with `bundle exec rubocop`
|
570
|
+
|
571
|
+
### Code Style
|
572
|
+
|
573
|
+
- Follow Ruby style guidelines
|
574
|
+
- Write tests for new features
|
575
|
+
- Update documentation for API changes
|
576
|
+
- Use meaningful commit messages
|
577
|
+
|
578
|
+
## License
|
579
|
+
|
580
|
+
MIT License. See [LICENSE.txt](LICENSE.txt) for details.
|
581
|
+
|
582
|
+
## Links
|
583
|
+
|
584
|
+
- [GitHub Repository](https://github.com/rafayqayyum/cache_sweeper)
|
585
|
+
- [MIT License](https://opensource.org/licenses/MIT)
|
586
|
+
- [RubyGems](https://rubygems.org/gems/cache_sweeper) (when published)
|