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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 601e9576e81fb4405ca36fe64b4a98ffd4d91f4d599fd77c9f96eec3d3547c18
4
- data.tar.gz: a726a516a2cfafb77d404df75276fc7ede6f2534f1f43d56dc0209d6bd805f11
3
+ metadata.gz: d75f242cecde072c5db6fe680a47fcb3b98165a4d12cf46738d64b0c0956c7ef
4
+ data.tar.gz: 86c3c5b676cb94ea1427cbed332f8fc97b2dac235ce22c031511a8ad1014d8b8
5
5
  SHA512:
6
- metadata.gz: 902ed8597f65c59c98f5148923246db46fbd5a97a44bda6bf108c589cd647720d37c75ae67b60f840af1abf40872cf7028d2f9905484d9fa7f980fba8cf90645
7
- data.tar.gz: 40f535b5e0e95de053886976858cc2bead62cb8b44246a3e4dcd2ce271b7538dafa7ff0f893bee72b364e29b1a2e98524cd67415545f809dfbe8f99b5413b8f5
6
+ metadata.gz: a4997e8c87c2cb1b974eeac87489b5ad6344adef9dd632d0c1f80429f4aae49edbe76ca4b70557d201a309575f8369a9f361207556c7bb6758630adf6240d174
7
+ data.tar.gz: 623d1edd5b5a304e7839133d9bc4ae9fc9d8f1d67ec76d893df77c6ff506e6c5322b37c657eac5f42d9cbe617f689def250b4974f150f0a0d668348f00766709
data/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [![CI](https://github.com/speedshop/cache_stache/actions/workflows/ci.yml/badge.svg)](https://github.com/speedshop/cache_stache/actions/workflows/ci.yml)
4
4
 
5
- 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.
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
- A higher hit rate means your app finds data in the cache more often. This means fewer slow calls to your database or other services. CacheStache helps you see this rate over time and spot problems.
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 a Password (Optional)
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
- # Falls back to ENV["REDIS_URL"] if not set
68
- config.redis_url = ENV.fetch("CACHE_STACHE_REDIS_URL", ENV["REDIS_URL"])
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
- | `redis_url` | `ENV["CACHE_STACHE_REDIS_URL"]` or `ENV["REDIS_URL"]` | Redis connection URL |
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 | Not yet active |
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.
@@ -26,7 +26,7 @@ module CacheStache
26
26
  def initialize(config = CacheStache.configuration)
27
27
  @config = config
28
28
  @pool = ConnectionPool.new(size: @config.redis_pool_size) do
29
- Redis.new(url: @config.redis_url)
29
+ @config.build_redis
30
30
  end
31
31
  end
32
32
 
@@ -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
- :redis_url, :redis_pool_size, :use_rack_after_reply, :max_buckets
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
- @redis_url = ENV.fetch("CACHE_STACHE_REDIS_URL") { ENV.fetch("REDIS_URL", "redis://localhost:6379/0") }
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..]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CacheStache
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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
- # Falls back to ENV["REDIS_URL"] if not set
9
- config.redis_url = ENV.fetch("CACHE_STACHE_REDIS_URL", ENV["REDIS_URL"])
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
@@ -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.redis_url = CACHE_STACHE_TEST_REDIS_URL
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.redis_url = CACHE_STACHE_TEST_REDIS_URL
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.redis_url).to eq(default_redis_url) }
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.redis_url = "redis://localhost:6379/0"
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 "requires redis_url" do
141
- config.redis_url = nil
142
- expect { config.validate! }.to raise_error(CacheStache::Error, /redis_url must be configured/)
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.1.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