Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,58 @@ top_posts = RequestStore.fetch(:top_posts) do
end
```

### Redis Caching (Optional)

You can add Redis as a persistent cache layer. Think of it like this:

```
Your app → RequestStore (super fast, but cleared each request)
→ Redis (fast, persists between requests)
→ Database (slow)
```

#### Why bother?

Say you call `User.find(123)` ten times in one request. Without RequestStore, that's ten Redis calls. With it, you get one Redis call and nine instant memory reads.

And between requests? Redis keeps your data warm so you're not hitting the database every time.

#### Setup

```ruby
# config/initializers/request_store.rb
require 'redis'

RequestStore.configure do |config|
config.redis_client = Redis.new(url: ENV['REDIS_URL'])
config.redis_enabled = true
config.redis_prefix = "myapp"
config.redis_ttl = 604800 # 1 week
end
```

#### Example

```ruby
def current_user
RequestStore.fetch("user:#{session[:user_id]}") do
# Only runs if not in RequestStore or Redis
User.find(session[:user_id])
end
end

# First call in request: checks RequestStore (miss) → checks Redis → maybe hits DB
# Second call in same request: instant memory lookup
# First call in next request: checks RequestStore (miss) → hits Redis (no DB!)
```

#### Notes

- RequestStore clears after every request (that's the point)
- Redis sticks around between requests (default: 1 week)
- Set `redis_ttl` to whatever makes sense (week, month, or nil for forever)
- It fails gracefully - if Redis is down, you just get slower requests

### Rails 2 compatibility

The gem includes a Railtie that will configure everything properly for Rails 3+
Expand Down
32 changes: 27 additions & 5 deletions lib/request_store.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require "request_store/version"
require "request_store/configuration"
require "request_store/redis_store"
require "request_store/middleware"
require "request_store/railtie" if defined?(Rails::Railtie)

Expand All @@ -12,6 +14,7 @@ def self.store=(store)
end

def self.clear!
RedisStore.clear! if configuration.redis_enabled?
Thread.current[:request_store] = {}
end

Expand All @@ -28,31 +31,50 @@ def self.active?
end

def self.read(key)
# Check thread-local store first
return store[key] if store.key?(key)

# Fall back to Redis if enabled
if configuration.redis_enabled?
value = RedisStore.read(key)
store[key] = value if value # Cache in thread-local store
return value
end

store[key]
end

def self.[](key)
store[key]
read(key)
end

def self.write(key, value)
store[key] = value
RedisStore.write(key, value) if configuration.redis_enabled?
value
end

def self.[]=(key, value)
store[key] = value
write(key, value)
end

def self.exist?(key)
store.key?(key)
return true if store.key?(key)
configuration.redis_enabled? && RedisStore.exist?(key)
end

def self.fetch(key)
store[key] = yield unless exist?(key)
store[key]
if exist?(key)
read(key)
else
value = yield
write(key, value)
value
end
end

def self.delete(key, &block)
RedisStore.delete(key) if configuration.redis_enabled?
store.delete(key, &block)
end
end
29 changes: 29 additions & 0 deletions lib/request_store/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module RequestStore
class Configuration
attr_accessor :redis_client, :redis_enabled, :redis_prefix, :redis_ttl, :skip_redis_on_error

def initialize
@redis_client = nil
@redis_enabled = false
@redis_prefix = "request_store"
@redis_ttl = 604800 # 1 week default TTL (in seconds)
@skip_redis_on_error = true # Fail gracefully by default
end

def redis_enabled?
@redis_enabled && !@redis_client.nil?
end
end

def self.configuration
@configuration ||= Configuration.new
end

def self.configure
yield(configuration)
end

def self.reset_configuration!
@configuration = Configuration.new
end
end
142 changes: 142 additions & 0 deletions lib/request_store/redis_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
module RequestStore
module RedisStore
class << self
# Generate Redis key with namespace prefix
def redis_key(key)
"#{RequestStore.configuration.redis_prefix}:#{key}"
end

# Read from Redis cache
def read(key)
return nil unless RequestStore.configuration.redis_enabled?

begin
value = RequestStore.configuration.redis_client.get(redis_key(key))
deserialize(value)
rescue => e
handle_error(e, "reading from Redis")
nil
end
end

# Write to Redis cache
def write(key, value)
return value unless RequestStore.configuration.redis_enabled?

begin
serialized = serialize(value)
r_key = redis_key(key)

RequestStore.configuration.redis_client.set(r_key, serialized)

# Set TTL if configured
if RequestStore.configuration.redis_ttl
RequestStore.configuration.redis_client.expire(r_key, RequestStore.configuration.redis_ttl)
end

value
rescue => e
handle_error(e, "writing to Redis")
value
end
end

# Check if key exists in Redis
def exist?(key)
return false unless RequestStore.configuration.redis_enabled?

begin
RequestStore.configuration.redis_client.exists(redis_key(key)) > 0
rescue => e
handle_error(e, "checking existence in Redis")
false
end
end

# Delete from Redis cache
def delete(key)
return nil unless RequestStore.configuration.redis_enabled?

begin
value = read(key)
RequestStore.configuration.redis_client.del(redis_key(key))
value
rescue => e
handle_error(e, "deleting from Redis")
nil
end
end

# Clear all Redis data - this clears ALL keys with the prefix
# Use with caution in production!
def clear!
return unless RequestStore.configuration.redis_enabled?

begin
# Get all keys with our prefix and delete them
pattern = "#{RequestStore.configuration.redis_prefix}:*"
keys = RequestStore.configuration.redis_client.keys(pattern)
RequestStore.configuration.redis_client.del(*keys) if keys.any?
rescue => e
handle_error(e, "clearing Redis cache")
end
end

# Get all keys with the configured prefix
def keys
return [] unless RequestStore.configuration.redis_enabled?

begin
pattern = "#{RequestStore.configuration.redis_prefix}:*"
RequestStore.configuration.redis_client.keys(pattern).map do |k|
k.sub("#{RequestStore.configuration.redis_prefix}:", '')
end
rescue => e
handle_error(e, "getting keys from Redis")
[]
end
end

# Get all data as a hash (can be expensive!)
def all
return {} unless RequestStore.configuration.redis_enabled?

begin
result = {}
keys.each do |key|
result[key] = read(key)
end
result
rescue => e
handle_error(e, "getting all data from Redis")
{}
end
end

private

def serialize(value)
Marshal.dump(value)
rescue => _e
# Fall back to string representation if Marshal fails
value.to_s
end

def deserialize(value)
return nil if value.nil?
Marshal.load(value)
rescue => _e
# If unmarshal fails, return the raw value
value
end

def handle_error(error, operation)
if RequestStore.configuration.skip_redis_on_error
warn "RequestStore Redis error while #{operation}: #{error.message}"
else
raise error
end
end
end
end
end
2 changes: 2 additions & 0 deletions request_store.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ Gem::Specification.new do |gem|

gem.add_development_dependency "rake"
gem.add_development_dependency "minitest", "~> 5.0"
gem.add_development_dependency "redis", ">= 4.0"
gem.add_development_dependency "mock_redis", "~> 0.36.0"
end
Loading