cache_stache 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/README.md +231 -0
- data/app/assets/stylesheets/cache_stache/application.css +5 -0
- data/app/assets/stylesheets/cache_stache/pico.css +4 -0
- data/app/controllers/cache_stache/application_controller.rb +11 -0
- data/app/controllers/cache_stache/dashboard_controller.rb +32 -0
- data/app/helpers/cache_stache/application_helper.rb +37 -0
- data/app/views/cache_stache/dashboard/index.html.erb +154 -0
- data/app/views/cache_stache/dashboard/keyspace.html.erb +83 -0
- data/app/views/layouts/cache_stache/application.html.erb +14 -0
- data/config/routes.rb +6 -0
- data/lib/cache_stache/cache_client.rb +202 -0
- data/lib/cache_stache/configuration.rb +87 -0
- data/lib/cache_stache/engine.rb +17 -0
- data/lib/cache_stache/instrumentation.rb +142 -0
- data/lib/cache_stache/keyspace.rb +28 -0
- data/lib/cache_stache/rack_after_reply_middleware.rb +22 -0
- data/lib/cache_stache/railtie.rb +30 -0
- data/lib/cache_stache/stats_query.rb +89 -0
- data/lib/cache_stache/version.rb +5 -0
- data/lib/cache_stache/web.rb +69 -0
- data/lib/cache_stache/window_options.rb +34 -0
- data/lib/cache_stache.rb +37 -0
- data/lib/generators/cache_stache/install_generator.rb +21 -0
- data/lib/generators/cache_stache/templates/README +35 -0
- data/lib/generators/cache_stache/templates/cache_stache.rb +43 -0
- data/spec/cache_stache_helper.rb +148 -0
- data/spec/dummy_app/Rakefile +5 -0
- data/spec/dummy_app/app/assets/config/manifest.js +1 -0
- data/spec/dummy_app/config/application.rb +31 -0
- data/spec/dummy_app/config/boot.rb +3 -0
- data/spec/dummy_app/config/environment.rb +5 -0
- data/spec/dummy_app/config/routes.rb +7 -0
- data/spec/integration/dashboard_controller_spec.rb +94 -0
- data/spec/integration/full_cache_flow_spec.rb +202 -0
- data/spec/integration/instrumentation_spec.rb +259 -0
- data/spec/integration/rack_after_reply_spec.rb +47 -0
- data/spec/integration/rake_tasks_spec.rb +17 -0
- data/spec/spec_helper.rb +64 -0
- data/spec/unit/cache_client_spec.rb +278 -0
- data/spec/unit/configuration_spec.rb +209 -0
- data/spec/unit/keyspace_spec.rb +93 -0
- data/spec/unit/stats_query_spec.rb +367 -0
- data/tasks/cache_stache.rake +74 -0
- metadata +226 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/numeric/time"
|
|
4
|
+
|
|
5
|
+
module CacheStache
|
|
6
|
+
class StatsQuery
|
|
7
|
+
attr_reader :window, :resolution
|
|
8
|
+
|
|
9
|
+
def initialize(window: 1.hour, resolution: nil)
|
|
10
|
+
@window = window.to_i
|
|
11
|
+
@resolution = resolution
|
|
12
|
+
@config = CacheStache.configuration
|
|
13
|
+
@cache_client = CacheClient.new(@config)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def execute
|
|
17
|
+
to_ts = Time.current.to_i
|
|
18
|
+
from_ts = to_ts - window
|
|
19
|
+
|
|
20
|
+
buckets = @cache_client.fetch_buckets(from_ts, to_ts)
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
overall: calculate_overall_stats(buckets),
|
|
24
|
+
keyspaces: calculate_keyspace_stats(buckets),
|
|
25
|
+
buckets: buckets.map { |b| format_bucket(b) },
|
|
26
|
+
window_seconds: window,
|
|
27
|
+
bucket_count: buckets.size
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def calculate_overall_stats(buckets)
|
|
34
|
+
total_hits = 0.0
|
|
35
|
+
total_misses = 0.0
|
|
36
|
+
|
|
37
|
+
buckets.each do |bucket|
|
|
38
|
+
total_hits += bucket[:stats]["overall:hits"].to_f
|
|
39
|
+
total_misses += bucket[:stats]["overall:misses"].to_f
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
total_ops = total_hits + total_misses
|
|
43
|
+
hit_rate = total_ops.positive? ? (total_hits / total_ops * 100).round(2) : 0.0
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
hits: total_hits.round,
|
|
47
|
+
misses: total_misses.round,
|
|
48
|
+
total_operations: total_ops.round,
|
|
49
|
+
hit_rate_percent: hit_rate
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def calculate_keyspace_stats(buckets)
|
|
54
|
+
keyspace_data = {}
|
|
55
|
+
|
|
56
|
+
@config.keyspaces.each do |keyspace|
|
|
57
|
+
total_hits = 0.0
|
|
58
|
+
total_misses = 0.0
|
|
59
|
+
|
|
60
|
+
buckets.each do |bucket|
|
|
61
|
+
total_hits += bucket[:stats]["#{keyspace.name}:hits"].to_f
|
|
62
|
+
total_misses += bucket[:stats]["#{keyspace.name}:misses"].to_f
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
total_ops = total_hits + total_misses
|
|
66
|
+
hit_rate = total_ops.positive? ? (total_hits / total_ops * 100).round(2) : 0.0
|
|
67
|
+
|
|
68
|
+
keyspace_data[keyspace.name] = {
|
|
69
|
+
label: keyspace.label,
|
|
70
|
+
pattern: keyspace.pattern,
|
|
71
|
+
hits: total_hits.round,
|
|
72
|
+
misses: total_misses.round,
|
|
73
|
+
total_operations: total_ops.round,
|
|
74
|
+
hit_rate_percent: hit_rate
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
keyspace_data
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def format_bucket(bucket)
|
|
82
|
+
{
|
|
83
|
+
timestamp: bucket[:timestamp],
|
|
84
|
+
time: Time.at(bucket[:timestamp]).utc,
|
|
85
|
+
stats: bucket[:stats]
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "engine"
|
|
4
|
+
|
|
5
|
+
module CacheStache
|
|
6
|
+
class Web
|
|
7
|
+
class << self
|
|
8
|
+
def call(env)
|
|
9
|
+
ensure_routes_loaded!
|
|
10
|
+
|
|
11
|
+
# Build app only once and cache it
|
|
12
|
+
unless @app
|
|
13
|
+
# Capture middlewares outside the Rack::Builder context
|
|
14
|
+
mw = middlewares
|
|
15
|
+
|
|
16
|
+
@app = Rack::Builder.new do
|
|
17
|
+
mw.each { |middleware, args, block| use(middleware, *args, &block) }
|
|
18
|
+
run CacheStache::Engine
|
|
19
|
+
end.to_app
|
|
20
|
+
|
|
21
|
+
Rails.logger.info "[CacheStache::Web] Rack app built successfully"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@app.call(env)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def use(middleware, *args, &block)
|
|
28
|
+
middlewares << [middleware, args, block]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def middlewares
|
|
32
|
+
@middlewares ||= []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reset_middlewares!
|
|
36
|
+
@middlewares = []
|
|
37
|
+
@app = nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reset_routes!
|
|
41
|
+
@routes_loaded = false
|
|
42
|
+
@app = nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def ensure_routes_loaded!
|
|
48
|
+
return if @routes_loaded
|
|
49
|
+
|
|
50
|
+
routes_path = File.expand_path("../../config/routes.rb", __dir__)
|
|
51
|
+
load_engine_components
|
|
52
|
+
load routes_path
|
|
53
|
+
@routes_loaded = true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_engine_components
|
|
57
|
+
base_path = File.expand_path("../../app", __dir__)
|
|
58
|
+
|
|
59
|
+
Dir["#{base_path}/helpers/**/*.rb"].sort.each do |file|
|
|
60
|
+
load file
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Dir["#{base_path}/controllers/**/*.rb"].sort.each do |file|
|
|
64
|
+
load file
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CacheStache
|
|
4
|
+
module WindowOptions
|
|
5
|
+
WINDOWS = [
|
|
6
|
+
{param: "5m", aliases: ["5_minutes"], label: "5 minutes", duration: 5.minutes},
|
|
7
|
+
{param: "15m", aliases: ["15_minutes"], label: "15 minutes", duration: 15.minutes},
|
|
8
|
+
{param: "1h", aliases: ["1_hour"], label: "1 hour", duration: 1.hour, default: true},
|
|
9
|
+
{param: "6h", aliases: ["6_hours"], label: "6 hours", duration: 6.hours},
|
|
10
|
+
{param: "1d", aliases: ["1_day", "24h"], label: "1 day", duration: 1.day},
|
|
11
|
+
{param: "1w", aliases: ["1_week", "7d"], label: "1 week", duration: 1.week}
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
DEFAULT_WINDOW = WINDOWS.find { |w| w[:default] }
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def for_select
|
|
19
|
+
WINDOWS.map { |w| [w[:label], w[:param]] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def find(param)
|
|
23
|
+
WINDOWS.find { |w| w[:param] == param || w[:aliases].include?(param) } || DEFAULT_WINDOW
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def label_for(param)
|
|
27
|
+
find(param)[:label]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def duration_for(param)
|
|
31
|
+
find(param)[:duration]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/cache_stache.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/all"
|
|
4
|
+
|
|
5
|
+
require_relative "cache_stache/version"
|
|
6
|
+
require_relative "cache_stache/window_options"
|
|
7
|
+
require_relative "cache_stache/configuration"
|
|
8
|
+
require_relative "cache_stache/keyspace"
|
|
9
|
+
require_relative "cache_stache/cache_client"
|
|
10
|
+
require_relative "cache_stache/instrumentation"
|
|
11
|
+
require_relative "cache_stache/stats_query"
|
|
12
|
+
require_relative "cache_stache/rack_after_reply_middleware"
|
|
13
|
+
|
|
14
|
+
module CacheStache
|
|
15
|
+
class Error < StandardError; end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
attr_writer :configuration
|
|
19
|
+
|
|
20
|
+
def configuration
|
|
21
|
+
@configuration ||= Configuration.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configure
|
|
25
|
+
yield(configuration)
|
|
26
|
+
configuration.validate!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def reset_configuration!
|
|
30
|
+
@configuration = Configuration.new
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
require_relative "cache_stache/railtie"
|
|
36
|
+
require_relative "cache_stache/engine"
|
|
37
|
+
require_relative "cache_stache/web"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module CacheStache
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
desc "Creates a CacheStache initializer and mounts the dashboard"
|
|
11
|
+
|
|
12
|
+
def copy_initializer
|
|
13
|
+
template "cache_stache.rb", "config/initializers/cache_stache.rb"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show_readme
|
|
17
|
+
readme "README" if behavior == :invoke
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
===============================================================================
|
|
2
|
+
|
|
3
|
+
CacheStache has been installed!
|
|
4
|
+
|
|
5
|
+
Next steps:
|
|
6
|
+
|
|
7
|
+
1. Review and customize config/initializers/cache_stache.rb
|
|
8
|
+
|
|
9
|
+
2. Mount the dashboard in your routes.rb:
|
|
10
|
+
|
|
11
|
+
# config/routes.rb
|
|
12
|
+
require "cache_stache/web"
|
|
13
|
+
|
|
14
|
+
# Optional: Add authentication
|
|
15
|
+
CacheStache::Web.use Rack::Auth::Basic do |user, pass|
|
|
16
|
+
ActiveSupport::SecurityUtils.secure_compare(user, ENV["CACHE_STACHE_USER"]) &&
|
|
17
|
+
ActiveSupport::SecurityUtils.secure_compare(pass, ENV["CACHE_STACHE_PASS"])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
mount CacheStache::Web, at: "/cache-stache"
|
|
21
|
+
|
|
22
|
+
3. Set environment variables (if using auth):
|
|
23
|
+
CACHE_STACHE_USER=admin
|
|
24
|
+
CACHE_STACHE_PASS=secret
|
|
25
|
+
|
|
26
|
+
4. Start your Rails server and visit /cache-stache
|
|
27
|
+
|
|
28
|
+
5. (Optional) Add keyspaces to track specific cache patterns in the initializer
|
|
29
|
+
|
|
30
|
+
Rake tasks:
|
|
31
|
+
rake cache_stache:config # Show current configuration
|
|
32
|
+
rake cache_stache:stats # Show current stats
|
|
33
|
+
rake cache_stache:prune # Manually prune old data
|
|
34
|
+
|
|
35
|
+
===============================================================================
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CacheStache Configuration
|
|
4
|
+
# This file configures the CacheStache cache hit rate monitoring system.
|
|
5
|
+
|
|
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"])
|
|
10
|
+
|
|
11
|
+
# Size of time buckets for aggregation (default: 5 minutes)
|
|
12
|
+
config.bucket_seconds = 5.minutes
|
|
13
|
+
|
|
14
|
+
# How long to retain data (default: 7 days)
|
|
15
|
+
config.retention_seconds = 7.days
|
|
16
|
+
|
|
17
|
+
# Sample rate (0.0 to 1.0). Use < 1.0 for high-traffic apps (default: 1.0)
|
|
18
|
+
config.sample_rate = 1.0
|
|
19
|
+
|
|
20
|
+
# Enable/disable instrumentation (default: true)
|
|
21
|
+
# Set to false in test environment if desired
|
|
22
|
+
config.enabled = !Rails.env.test?
|
|
23
|
+
|
|
24
|
+
# Defer stats increments until after the response is sent, via Rack's
|
|
25
|
+
# `env["rack.after_reply"]` (supported by Puma and others).
|
|
26
|
+
# Default: false
|
|
27
|
+
config.use_rack_after_reply = false
|
|
28
|
+
|
|
29
|
+
# Define keyspaces to track specific cache key patterns
|
|
30
|
+
# Each keyspace uses a regex to match cache keys
|
|
31
|
+
#
|
|
32
|
+
# Example:
|
|
33
|
+
#
|
|
34
|
+
# config.keyspace :profiles do
|
|
35
|
+
# label "Profile Fragments"
|
|
36
|
+
# match /^profile:/
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# config.keyspace :search do
|
|
40
|
+
# label "Search Results"
|
|
41
|
+
# match %r{/search/}
|
|
42
|
+
# end
|
|
43
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spec_helper"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
# Initialize the Rails app (spec_helper loads it but doesn't initialize)
|
|
7
|
+
CacheStacheDummy::Application.initialize! unless CacheStacheDummy::Application.initialized?
|
|
8
|
+
|
|
9
|
+
# Load RSpec Rails integration (controller specs, etc.)
|
|
10
|
+
require "rspec/rails"
|
|
11
|
+
|
|
12
|
+
# Ensure the engine is loaded after Rails is initialized
|
|
13
|
+
require "cache_stache/engine"
|
|
14
|
+
load CacheStache::Engine.root.join("config/routes.rb")
|
|
15
|
+
|
|
16
|
+
# Test Redis configuration - uses database 15 to isolate from other Redis usage
|
|
17
|
+
CACHE_STACHE_TEST_REDIS_URL = ENV.fetch("CACHE_STACHE_TEST_REDIS_URL", "redis://localhost:6379/15")
|
|
18
|
+
|
|
19
|
+
module CacheStacheTestHelpers
|
|
20
|
+
# Helper to get the test Redis connection directly
|
|
21
|
+
def cache_stache_redis
|
|
22
|
+
@_cache_stache_redis ||= Redis.new(url: CACHE_STACHE_TEST_REDIS_URL)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Helper to get the current bucket key
|
|
26
|
+
def current_bucket_key(bucket_seconds: 300)
|
|
27
|
+
bucket_ts = (Time.current.to_i / bucket_seconds) * bucket_seconds
|
|
28
|
+
"cache_stache:v1:test:#{bucket_ts}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Helper to get stats from current bucket
|
|
32
|
+
def current_bucket_stats(bucket_seconds: 300)
|
|
33
|
+
cache_stache_redis.hgetall(current_bucket_key(bucket_seconds: bucket_seconds))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Helper to clear all CacheStache notification listeners
|
|
37
|
+
def clear_cache_stache_listeners
|
|
38
|
+
ActiveSupport::Notifications.notifier.listeners_for("cache_read.active_support").each do |listener|
|
|
39
|
+
ActiveSupport::Notifications.unsubscribe(listener)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Helper to flush CacheStache keys from Redis
|
|
44
|
+
def flush_cache_stache_redis
|
|
45
|
+
redis = Redis.new(url: CACHE_STACHE_TEST_REDIS_URL)
|
|
46
|
+
redis.scan_each(match: "cache_stache:*") { |key| redis.del(key) }
|
|
47
|
+
redis.close
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Build a test configuration with common defaults
|
|
51
|
+
def build_test_config(keyspaces: {}, **options)
|
|
52
|
+
CacheStache::Configuration.new.tap do |c|
|
|
53
|
+
c.redis_url = CACHE_STACHE_TEST_REDIS_URL
|
|
54
|
+
c.bucket_seconds = options.fetch(:bucket_seconds, 300)
|
|
55
|
+
c.retention_seconds = options.fetch(:retention_seconds, 3600)
|
|
56
|
+
c.sample_rate = options.fetch(:sample_rate, 1.0)
|
|
57
|
+
c.use_rack_after_reply = options.fetch(:use_rack_after_reply, false)
|
|
58
|
+
|
|
59
|
+
keyspaces.each do |name, keyspace_config|
|
|
60
|
+
c.keyspace(name) do
|
|
61
|
+
label keyspace_config[:label] if keyspace_config[:label]
|
|
62
|
+
match keyspace_config[:match]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
RSpec.configure do |config|
|
|
70
|
+
config.include CacheStacheTestHelpers
|
|
71
|
+
|
|
72
|
+
config.before(:suite) do
|
|
73
|
+
# Verify Redis is available before running tests
|
|
74
|
+
redis = Redis.new(url: CACHE_STACHE_TEST_REDIS_URL)
|
|
75
|
+
begin
|
|
76
|
+
redis.ping
|
|
77
|
+
rescue Redis::CannotConnectError => e
|
|
78
|
+
abort "CacheStache tests require a running Redis server. " \
|
|
79
|
+
"Please start Redis and try again.\n" \
|
|
80
|
+
"Connection error: #{e.message}"
|
|
81
|
+
ensure
|
|
82
|
+
redis.close
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
config.before do
|
|
87
|
+
# Configure CacheStache to use test Redis
|
|
88
|
+
CacheStache.configure do |c|
|
|
89
|
+
c.redis_url = CACHE_STACHE_TEST_REDIS_URL
|
|
90
|
+
c.redis_pool_size = 1
|
|
91
|
+
c.enabled = true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Create a fresh cache client for each test
|
|
95
|
+
cache_client = CacheStache::CacheClient.new(CacheStache.configuration)
|
|
96
|
+
Thread.current[:cache_stache_test_client] = cache_client
|
|
97
|
+
|
|
98
|
+
flush_cache_stache_redis
|
|
99
|
+
Rails.cache.clear
|
|
100
|
+
clear_cache_stache_listeners
|
|
101
|
+
CacheStache::Instrumentation.reset!
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
config.after do
|
|
105
|
+
Thread.current[:cache_stache_test_client] = nil
|
|
106
|
+
Rails.cache.clear
|
|
107
|
+
|
|
108
|
+
if defined?(@_cache_stache_redis) && @_cache_stache_redis
|
|
109
|
+
@_cache_stache_redis.close
|
|
110
|
+
@_cache_stache_redis = nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Shared context for tests that need instrumentation installed
|
|
116
|
+
RSpec.shared_context "with instrumentation" do
|
|
117
|
+
let(:config) do
|
|
118
|
+
build_test_config(
|
|
119
|
+
keyspaces: {
|
|
120
|
+
views: {label: "View Fragments", match: /^views\//},
|
|
121
|
+
models: {label: "Model Cache", match: /community/}
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
before do
|
|
127
|
+
allow(CacheStache).to receive(:configuration).and_return(config)
|
|
128
|
+
CacheStache::Instrumentation.install!
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Shared context for tests that need instrumentation with search keyspace
|
|
133
|
+
RSpec.shared_context "with instrumentation and search" do
|
|
134
|
+
let(:config) do
|
|
135
|
+
build_test_config(
|
|
136
|
+
keyspaces: {
|
|
137
|
+
views: {label: "View Fragments", match: /^views\//},
|
|
138
|
+
models: {label: "Model Cache", match: /community/},
|
|
139
|
+
search: {label: "Search Results", match: /search/}
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
before do
|
|
145
|
+
allow(CacheStache).to receive(:configuration).and_return(config)
|
|
146
|
+
CacheStache::Instrumentation.install!
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_directory ../stylesheets .css
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "boot"
|
|
4
|
+
|
|
5
|
+
require "pathname"
|
|
6
|
+
require "logger"
|
|
7
|
+
require "rails"
|
|
8
|
+
require "rails/engine"
|
|
9
|
+
require "active_support/all"
|
|
10
|
+
require "action_dispatch/railtie"
|
|
11
|
+
require "action_controller/railtie"
|
|
12
|
+
require "action_view/railtie"
|
|
13
|
+
require "sprockets/railtie"
|
|
14
|
+
|
|
15
|
+
require "cache_stache"
|
|
16
|
+
require "cache_stache/web"
|
|
17
|
+
require "cache_stache/engine"
|
|
18
|
+
require "cache_stache/railtie"
|
|
19
|
+
|
|
20
|
+
module CacheStacheDummy
|
|
21
|
+
class Application < Rails::Application
|
|
22
|
+
config.load_defaults Rails::VERSION::STRING.to_f
|
|
23
|
+
config.root = Pathname.new(File.expand_path("..", __dir__))
|
|
24
|
+
|
|
25
|
+
config.eager_load = false
|
|
26
|
+
config.cache_store = :memory_store
|
|
27
|
+
config.logger = Logger.new(nil)
|
|
28
|
+
config.active_support.deprecation = :log
|
|
29
|
+
config.secret_key_base = "cache-stache-test"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../cache_stache_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe CacheStache::DashboardController, type: :controller do
|
|
6
|
+
routes { CacheStache::Engine.routes }
|
|
7
|
+
|
|
8
|
+
let(:config) do
|
|
9
|
+
CacheStache::Configuration.new.tap do |c|
|
|
10
|
+
c.bucket_seconds = 300
|
|
11
|
+
c.retention_seconds = 3600
|
|
12
|
+
|
|
13
|
+
c.keyspace(:views) do
|
|
14
|
+
label "View Fragments"
|
|
15
|
+
match(/^views\//)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
c.keyspace(:models) do
|
|
19
|
+
label "Model Cache"
|
|
20
|
+
match(/community/)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
before do
|
|
26
|
+
allow(CacheStache).to receive(:configuration).and_return(config)
|
|
27
|
+
Rails.cache.clear
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "GET #index" do
|
|
31
|
+
it "renders successfully" do
|
|
32
|
+
get :index
|
|
33
|
+
expect(response).to be_successful
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context "with custom window parameter" do
|
|
37
|
+
it "accepts 5 minutes window" do
|
|
38
|
+
get :index, params: {window: "5m"}
|
|
39
|
+
expect(response).to be_successful
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "accepts 15 minutes window" do
|
|
43
|
+
get :index, params: {window: "15m"}
|
|
44
|
+
expect(response).to be_successful
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "accepts 1 hour window" do
|
|
48
|
+
get :index, params: {window: "1h"}
|
|
49
|
+
expect(response).to be_successful
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "accepts 6 hours window" do
|
|
53
|
+
get :index, params: {window: "6h"}
|
|
54
|
+
expect(response).to be_successful
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "accepts 1 day window" do
|
|
58
|
+
get :index, params: {window: "1d"}
|
|
59
|
+
expect(response).to be_successful
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "accepts 1 week window" do
|
|
63
|
+
get :index, params: {window: "1w"}
|
|
64
|
+
expect(response).to be_successful
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "defaults to 1 hour for invalid window" do
|
|
68
|
+
get :index, params: {window: "invalid"}
|
|
69
|
+
expect(response).to be_successful
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe "GET #keyspace" do
|
|
75
|
+
it "renders successfully for valid keyspace" do
|
|
76
|
+
get :keyspace, params: {name: "views"}
|
|
77
|
+
expect(response).to be_successful
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context "with custom window parameter" do
|
|
81
|
+
it "respects window parameter" do
|
|
82
|
+
get :keyspace, params: {name: "views", window: "6h"}
|
|
83
|
+
expect(response).to be_successful
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
context "with invalid keyspace" do
|
|
88
|
+
it "returns 404" do
|
|
89
|
+
get :keyspace, params: {name: "nonexistent"}
|
|
90
|
+
expect(response).to have_http_status(:not_found)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|