cache_stache 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -94
- data/lib/cache_stache/cache_client.rb +1 -1
- data/lib/cache_stache/configuration.rb +32 -3
- data/lib/cache_stache/version.rb +1 -1
- data/lib/generators/cache_stache/templates/cache_stache.rb +9 -3
- data/spec/cache_stache_helper.rb +3 -2
- data/spec/unit/configuration_spec.rb +48 -5
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d75f242cecde072c5db6fe680a47fcb3b98165a4d12cf46738d64b0c0956c7ef
|
|
4
|
+
data.tar.gz: 86c3c5b676cb94ea1427cbed332f8fc97b2dac235ce22c031511a8ad1014d8b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4997e8c87c2cb1b974eeac87489b5ad6344adef9dd632d0c1f80429f4aae49edbe76ca4b70557d201a309575f8369a9f361207556c7bb6758630adf6240d174
|
|
7
|
+
data.tar.gz: 623d1edd5b5a304e7839133d9bc4ae9fc9d8f1d67ec76d893df77c6ff506e6c5322b37c657eac5f42d9cbe617f689def250b4974f150f0a0d668348f00766709
|
data/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/speedshop/cache_stache/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Have you ever had to work with a Redis cache provider which doesn't provide hitrate stats? It's a bummer. Use this gem!
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
CacheStache tracks cache hit rates for Rails apps. It counts how often your cache has data (hits) and how often it does not (misses). You can view these counts on a web page.
|
|
8
8
|
|
|
9
9
|
## Features
|
|
10
10
|
|
|
@@ -42,7 +42,7 @@ A higher hit rate means your app finds data in the cache more often. This means
|
|
|
42
42
|
|
|
43
43
|
4. Restart Rails and go to `/cache-stache`.
|
|
44
44
|
|
|
45
|
-
### Add
|
|
45
|
+
### Add Authentication
|
|
46
46
|
|
|
47
47
|
You can add a password to the web page:
|
|
48
48
|
|
|
@@ -63,9 +63,9 @@ All settings go in `config/initializers/cache_stache.rb`:
|
|
|
63
63
|
|
|
64
64
|
```ruby
|
|
65
65
|
CacheStache.configure do |config|
|
|
66
|
-
# Redis connection for storing cache metrics
|
|
67
|
-
#
|
|
68
|
-
config.
|
|
66
|
+
# Redis connection for storing cache metrics.
|
|
67
|
+
# Can be a String (URL), Proc, or Redis-compatible object.
|
|
68
|
+
config.redis = ENV.fetch("CACHE_STACHE_REDIS_URL", ENV["REDIS_URL"])
|
|
69
69
|
|
|
70
70
|
# Time bucket size
|
|
71
71
|
config.bucket_seconds = 5.minutes
|
|
@@ -99,12 +99,12 @@ end
|
|
|
99
99
|
|
|
100
100
|
| Setting | Default | What it does |
|
|
101
101
|
|---------|---------|--------------|
|
|
102
|
-
| `
|
|
102
|
+
| `redis` | `ENV["CACHE_STACHE_REDIS_URL"]` or `ENV["REDIS_URL"]` | Redis connection (String URL, Proc, or Redis object) |
|
|
103
103
|
| `redis_pool_size` | 5 | Size of the Redis connection pool |
|
|
104
104
|
| `bucket_seconds` | 5 minutes | Size of each time bucket |
|
|
105
105
|
| `retention_seconds` | 7 days | How long to keep data |
|
|
106
106
|
| `max_buckets` | 288 | Maximum number of buckets to query |
|
|
107
|
-
| `sample_rate` | 1.0 |
|
|
107
|
+
| `sample_rate` | 1.0 | Sample events |
|
|
108
108
|
| `enabled` | true | Turn tracking on or off |
|
|
109
109
|
| `use_rack_after_reply` | false | Wait to write until after response |
|
|
110
110
|
|
|
@@ -126,37 +126,6 @@ end
|
|
|
126
126
|
|
|
127
127
|
A cache key can match more than one keyspace.
|
|
128
128
|
|
|
129
|
-
## Web Page
|
|
130
|
-
|
|
131
|
-
The web page shows:
|
|
132
|
-
|
|
133
|
-
- Total hit rate
|
|
134
|
-
- Hit rate for each keyspace
|
|
135
|
-
- Current settings
|
|
136
|
-
- Size of stored data
|
|
137
|
-
|
|
138
|
-
Time windows: 5m, 15m, 1h (default), 6h, 1d, 1w.
|
|
139
|
-
|
|
140
|
-
Click a keyspace name to see more detail.
|
|
141
|
-
|
|
142
|
-
## How It Works
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
Rails.cache.fetch(...)
|
|
146
|
-
-> Rails sends an event
|
|
147
|
-
-> CacheStache counts it
|
|
148
|
-
-> CacheStache stores the count
|
|
149
|
-
-> Web page shows the counts
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
1. **Counting**: CacheStache listens for cache events. It skips its own cache calls.
|
|
153
|
-
|
|
154
|
-
2. **Buckets**: Times are rounded down to `bucket_seconds`. Each event adds to hit or miss counts.
|
|
155
|
-
|
|
156
|
-
3. **Storage**: Counts are stored with keys like `cache_stache:v1:production:1234567890`. Each bucket expires after `retention_seconds`.
|
|
157
|
-
|
|
158
|
-
4. **Reading**: The web page reads all buckets and adds them up.
|
|
159
|
-
|
|
160
129
|
## Query Stats in Code
|
|
161
130
|
|
|
162
131
|
You can get stats from Ruby code:
|
|
@@ -171,61 +140,7 @@ results[:overall][:misses] # => 210
|
|
|
171
140
|
results[:keyspaces][:profiles][:hit_rate_percent] # => 92.1
|
|
172
141
|
```
|
|
173
142
|
|
|
174
|
-
## Test Data
|
|
175
|
-
|
|
176
|
-
Make fake data with:
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
rails runner lib/cache_stache/bin/test_day_simulation.rb
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
This makes 24 hours of fake cache events. Then go to `/cache-stache` to see it.
|
|
183
|
-
|
|
184
143
|
## Limits
|
|
185
144
|
|
|
186
|
-
- The `sample_rate` setting does nothing yet.
|
|
187
145
|
- Only cache reads are tracked. Writes and deletes are not.
|
|
188
|
-
- If you have two cache stores of the same type, their events will be mixed.
|
|
189
|
-
|
|
190
|
-
## Files
|
|
191
|
-
|
|
192
|
-
```
|
|
193
|
-
lib/cache_stache/
|
|
194
|
-
├── app/ # Web page views and code
|
|
195
|
-
├── bin/ # Test scripts
|
|
196
|
-
├── config/ # Routes
|
|
197
|
-
├── lib/ # Gem/engine Ruby code
|
|
198
|
-
│ ├── cache_stache.rb
|
|
199
|
-
│ ├── cache_stache/
|
|
200
|
-
│ └── generators/
|
|
201
|
-
├── Gemfile # Standalone bundler entrypoint
|
|
202
|
-
├── cache_stache.gemspec
|
|
203
|
-
├── Rakefile
|
|
204
|
-
├── spec/ # Tests
|
|
205
|
-
├── tasks/ # Rake tasks
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
## Running Specs (Standalone)
|
|
209
|
-
|
|
210
|
-
CacheStache can be tested independently from the host Rails app:
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
cd lib/cache_stache
|
|
214
|
-
bundle install
|
|
215
|
-
bundle exec rspec
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
From the host app root, you can also run the engine suite without `cd`:
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
BUNDLE_GEMFILE=lib/cache_stache/Gemfile bundle exec rspec --options lib/cache_stache/.rspec lib/cache_stache/spec
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
## Run Tests
|
|
225
|
-
|
|
226
|
-
```bash
|
|
227
|
-
cd lib/cache_stache
|
|
228
|
-
bundle exec rspec
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Tests are in `lib/cache_stache/spec/`. They do not need Redis.
|
|
146
|
+
- If you have two cache stores of the same type (redis, memcached, etc), their events will be mixed.
|
|
@@ -5,8 +5,10 @@ require "active_support/core_ext/numeric/time"
|
|
|
5
5
|
|
|
6
6
|
module CacheStache
|
|
7
7
|
class Configuration
|
|
8
|
+
DEFAULT_REDIS_OPTIONS = {reconnect_attempts: 0}.freeze
|
|
9
|
+
|
|
8
10
|
attr_accessor :bucket_seconds, :retention_seconds, :sample_rate, :enabled,
|
|
9
|
-
:
|
|
11
|
+
:redis, :redis_pool_size, :use_rack_after_reply, :max_buckets
|
|
10
12
|
attr_reader :keyspaces
|
|
11
13
|
|
|
12
14
|
def initialize
|
|
@@ -15,13 +17,33 @@ module CacheStache
|
|
|
15
17
|
@sample_rate = 1.0
|
|
16
18
|
@enabled = rails_env != "test"
|
|
17
19
|
@use_rack_after_reply = false
|
|
18
|
-
@
|
|
20
|
+
@redis = ENV.fetch("CACHE_STACHE_REDIS_URL") { ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
|
|
19
21
|
@redis_pool_size = 5
|
|
20
22
|
@max_buckets = 288
|
|
21
23
|
@keyspaces = []
|
|
22
24
|
@keyspace_cache = {}
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
# Factory method to create a new Redis instance.
|
|
28
|
+
#
|
|
29
|
+
# Handles three options:
|
|
30
|
+
#
|
|
31
|
+
# Option Class Result
|
|
32
|
+
# :redis Proc -> redis.call
|
|
33
|
+
# :redis String -> Redis.new(url: redis)
|
|
34
|
+
# :redis Object -> redis (assumed to be a Redis-compatible client)
|
|
35
|
+
#
|
|
36
|
+
def build_redis
|
|
37
|
+
case redis
|
|
38
|
+
when Proc
|
|
39
|
+
redis.call
|
|
40
|
+
when String
|
|
41
|
+
::Redis.new(DEFAULT_REDIS_OPTIONS.merge(url: redis))
|
|
42
|
+
else
|
|
43
|
+
redis
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
25
47
|
def keyspace(name, &block)
|
|
26
48
|
ks = Keyspace.new(name)
|
|
27
49
|
builder = KeyspaceBuilder.new(ks)
|
|
@@ -43,8 +65,9 @@ module CacheStache
|
|
|
43
65
|
def validate!
|
|
44
66
|
raise Error, "bucket_seconds must be positive" unless bucket_seconds.to_i.positive?
|
|
45
67
|
raise Error, "retention_seconds must be positive" unless retention_seconds.to_i.positive?
|
|
68
|
+
raise Error, "redis must be configured" if redis.nil?
|
|
69
|
+
raise Error, "redis must be a Proc, String (URL), or Redis-compatible object" unless valid_redis_option?
|
|
46
70
|
raise Error, "redis_pool_size must be positive" unless redis_pool_size.to_i.positive?
|
|
47
|
-
raise Error, "redis_url must be configured" if redis_url.to_s.strip.empty?
|
|
48
71
|
raise Error, "sample_rate must be between 0 and 1" unless sample_rate&.between?(0, 1)
|
|
49
72
|
raise Error, "max_buckets must be positive" unless max_buckets.to_i.positive?
|
|
50
73
|
|
|
@@ -64,6 +87,12 @@ module CacheStache
|
|
|
64
87
|
|
|
65
88
|
private
|
|
66
89
|
|
|
90
|
+
def valid_redis_option?
|
|
91
|
+
return true if redis.is_a?(Proc)
|
|
92
|
+
return redis.to_s.strip.length > 0 if redis.is_a?(String)
|
|
93
|
+
true # Assume other objects are Redis-compatible clients
|
|
94
|
+
end
|
|
95
|
+
|
|
67
96
|
def key_digest(key)
|
|
68
97
|
# Use last 4 chars of a simple hash as cache key
|
|
69
98
|
Digest::MD5.hexdigest(key.to_s)[-4..]
|
data/lib/cache_stache/version.rb
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
# This file configures the CacheStache cache hit rate monitoring system.
|
|
5
5
|
|
|
6
6
|
CacheStache.configure do |config|
|
|
7
|
-
# Redis connection for storing cache metrics
|
|
8
|
-
#
|
|
9
|
-
|
|
7
|
+
# Redis connection for storing cache metrics.
|
|
8
|
+
# Can be a String (URL), Proc, or Redis-compatible object.
|
|
9
|
+
#
|
|
10
|
+
# Examples:
|
|
11
|
+
# config.redis = "redis://localhost:6379/0"
|
|
12
|
+
# config.redis = -> { Redis.new(url: ENV["REDIS_URL"]) }
|
|
13
|
+
# config.redis = ConnectionPool.new { Redis.new }
|
|
14
|
+
#
|
|
15
|
+
config.redis = ENV.fetch("CACHE_STACHE_REDIS_URL", ENV["REDIS_URL"])
|
|
10
16
|
|
|
11
17
|
# Size of time buckets for aggregation (default: 5 minutes)
|
|
12
18
|
config.bucket_seconds = 5.minutes
|
data/spec/cache_stache_helper.rb
CHANGED
|
@@ -50,11 +50,12 @@ module CacheStacheTestHelpers
|
|
|
50
50
|
# Build a test configuration with common defaults
|
|
51
51
|
def build_test_config(keyspaces: {}, **options)
|
|
52
52
|
CacheStache::Configuration.new.tap do |c|
|
|
53
|
-
c.
|
|
53
|
+
c.redis = CACHE_STACHE_TEST_REDIS_URL
|
|
54
54
|
c.bucket_seconds = options.fetch(:bucket_seconds, 300)
|
|
55
55
|
c.retention_seconds = options.fetch(:retention_seconds, 3600)
|
|
56
56
|
c.sample_rate = options.fetch(:sample_rate, 1.0)
|
|
57
57
|
c.use_rack_after_reply = options.fetch(:use_rack_after_reply, false)
|
|
58
|
+
c.enabled = options.fetch(:enabled, true)
|
|
58
59
|
|
|
59
60
|
keyspaces.each do |name, keyspace_config|
|
|
60
61
|
c.keyspace(name) do
|
|
@@ -86,7 +87,7 @@ RSpec.configure do |config|
|
|
|
86
87
|
config.before do
|
|
87
88
|
# Configure CacheStache to use test Redis
|
|
88
89
|
CacheStache.configure do |c|
|
|
89
|
-
c.
|
|
90
|
+
c.redis = CACHE_STACHE_TEST_REDIS_URL
|
|
90
91
|
c.redis_pool_size = 1
|
|
91
92
|
c.enabled = true
|
|
92
93
|
end
|
|
@@ -11,6 +11,7 @@ RSpec.describe CacheStache::Configuration do
|
|
|
11
11
|
before do
|
|
12
12
|
allow(ENV).to receive(:fetch).and_call_original
|
|
13
13
|
allow(ENV).to receive(:fetch).with("CACHE_STACHE_REDIS_URL").and_return(default_redis_url)
|
|
14
|
+
allow(ENV).to receive(:fetch).with("RAILS_ENV", "development").and_return("development")
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
it { expect(config.bucket_seconds).to eq(5.minutes.to_i) }
|
|
@@ -18,11 +19,38 @@ RSpec.describe CacheStache::Configuration do
|
|
|
18
19
|
it { expect(config.sample_rate).to eq(1.0) }
|
|
19
20
|
it { expect(config.enabled).to be(true) }
|
|
20
21
|
it { expect(config.use_rack_after_reply).to be(false) }
|
|
21
|
-
it { expect(config.
|
|
22
|
+
it { expect(config.redis).to eq(default_redis_url) }
|
|
22
23
|
it { expect(config.redis_pool_size).to eq(5) }
|
|
23
24
|
it { expect(config.keyspaces).to eq([]) }
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
describe "#build_redis" do
|
|
28
|
+
it "calls the proc when redis is a Proc" do
|
|
29
|
+
redis_instance = instance_double(Redis)
|
|
30
|
+
config.redis = -> { redis_instance }
|
|
31
|
+
|
|
32
|
+
expect(config.build_redis).to eq(redis_instance)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "creates a Redis instance when redis is a String URL" do
|
|
36
|
+
config.redis = "redis://localhost:6379/1"
|
|
37
|
+
|
|
38
|
+
expect(Redis).to receive(:new).with(
|
|
39
|
+
hash_including(url: "redis://localhost:6379/1")
|
|
40
|
+
).and_call_original
|
|
41
|
+
|
|
42
|
+
result = config.build_redis
|
|
43
|
+
expect(result).to be_a(Redis)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "returns the object directly when redis is an Object" do
|
|
47
|
+
redis_instance = instance_double(Redis)
|
|
48
|
+
config.redis = redis_instance
|
|
49
|
+
|
|
50
|
+
expect(config.build_redis).to eq(redis_instance)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
26
54
|
describe "#keyspace" do
|
|
27
55
|
it "adds a keyspace with the given name" do
|
|
28
56
|
config.keyspace(:views) do
|
|
@@ -134,12 +162,27 @@ RSpec.describe CacheStache::Configuration do
|
|
|
134
162
|
|
|
135
163
|
describe "#validate!" do
|
|
136
164
|
before do
|
|
137
|
-
config.
|
|
165
|
+
config.redis = "redis://localhost:6379/0"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "requires redis" do
|
|
169
|
+
config.redis = nil
|
|
170
|
+
expect { config.validate! }.to raise_error(CacheStache::Error, /redis must be configured/)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
it "rejects empty string for redis" do
|
|
174
|
+
config.redis = " "
|
|
175
|
+
expect { config.validate! }.to raise_error(CacheStache::Error, /redis must be a Proc, String/)
|
|
138
176
|
end
|
|
139
177
|
|
|
140
|
-
it "
|
|
141
|
-
config.
|
|
142
|
-
expect { config.validate! }.
|
|
178
|
+
it "accepts a Proc for redis" do
|
|
179
|
+
config.redis = -> { Redis.new }
|
|
180
|
+
expect { config.validate! }.not_to raise_error
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "accepts an Object for redis" do
|
|
184
|
+
config.redis = instance_double(Redis)
|
|
185
|
+
expect { config.validate! }.not_to raise_error
|
|
143
186
|
end
|
|
144
187
|
|
|
145
188
|
it "requires redis_pool_size to be positive" do
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cache_stache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CacheStache contributors
|
|
@@ -13,14 +13,14 @@ dependencies:
|
|
|
13
13
|
name: rails
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '7.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '7.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|