Caching
Azu provides a powerful and flexible caching system similar to Rails.cache, supporting multiple storage backends and Rails-like API patterns. The caching system is designed for high performance and thread safety.
Table of Contents
Dependencies
Redis Installation
To use the Redis cache store, you need to have Redis server installed and running.
Installing Redis
macOS (Homebrew):
brew install redis
brew services start redis
Ubuntu/Debian:
sudo apt update
sudo apt install redis-server
sudo systemctl start redis-server
sudo systemctl enable redis-server
CentOS/RHEL:
sudo yum install redis
sudo systemctl start redis
sudo systemctl enable redis
Docker:
# Run Redis in Docker
docker run -d --name redis -p 6379:6379 redis:latest
# Or with persistence
docker run -d --name redis -p 6379:6379 -v redis_data:/data redis:latest redis-server --appendonly yes
Verifying Redis Installation
# Test Redis connectivity
redis-cli ping
# Should return: PONG
# Check Redis version
redis-cli info server | grep redis_version
Azu Dependencies
Add Redis to your shard.yml
:
dependencies:
redis:
github: stefanwille/crystal-redis
version: ~> 2.9.0
Then install dependencies:
shards install
Basic Usage
Simple Get/Set Operations
# Basic cache operations
Azu.cache.set("user:123", user_data, ttl: 1.hour)
cached_user = Azu.cache.get("user:123")
# Check if key exists
if Azu.cache.exists?("user:123")
puts "User data is cached"
end
# Delete cached data
Azu.cache.delete("user:123")
Get with Block Syntax (Rails-like)
The get
method supports block syntax with TTL, providing an alternative to fetch
:
# Cache expensive operations using get with block
user_data = Azu.cache.get("user:#{user_id}", ttl: 1.hour) do
# This block only runs if the key is not cached
database.query("SELECT * FROM users WHERE id = ?", user_id)
end
# Complex data processing with get syntax
analytics_data = Azu.cache.get("analytics:#{date}", ttl: 1.day) do
expensive_analytics_calculation(date)
end
Fetch with Block (Rails-like)
The fetch
method provides the traditional Rails caching pattern:
# Cache expensive operations using fetch
user_data = Azu.cache.fetch("user:#{user_id}", ttl: 30.minutes) do
# This block only runs if the key is not cached
database.query("SELECT * FROM users WHERE id = ?", user_id)
end
# Complex data processing
analytics_data = Azu.cache.fetch("analytics:#{date}", ttl: 1.day) do
expensive_analytics_calculation(date)
end
Endpoint Example
struct UserEndpoint
include Endpoint(UserRequest, UserResponse)
get "/users/:id"
def call : UserResponse
# Using fetch method
user_data = Azu.cache.fetch("user:#{request.id}", ttl: 15.minutes) do
# Expensive database query
User.find(request.id).to_json
end
UserResponse.new(JSON.parse(user_data))
end
end
# Alternative using get with block syntax
struct UserEndpointAlt
include Endpoint(UserRequest, UserResponse)
get "/users-alt/:id"
def call : UserResponse
# Using get method with block
user_data = Azu.cache.get("user:#{request.id}", ttl: 15.minutes) do
# Expensive database query
User.find(request.id).to_json
end
UserResponse.new(JSON.parse(user_data))
end
end
Configuration
Environment Variables
Configure caching behavior through environment variables:
# Enable/disable caching (default: true)
CACHE_ENABLED=true
# Cache store type (default: memory)
CACHE_STORE=memory
# Maximum cache size for memory store (default: 1000)
CACHE_MAX_SIZE=5000
# Default TTL in seconds (default: 3600)
CACHE_DEFAULT_TTL=7200
# Key prefix for all cache keys (default: azu)
CACHE_KEY_PREFIX=myapp
# Enable compression (default: false)
CACHE_COMPRESS=false
# Enable serialization (default: true)
CACHE_SERIALIZE=true
Programmatic Configuration
# Configure cache in application setup
Azu.configure do |config|
config.cache_config.enabled = true
config.cache_config.store = "memory"
config.cache_config.max_size = 2000
config.cache_config.default_ttl = 3600
config.cache_config.key_prefix = "myapp"
end
Cache Stores
Memory Store (Default)
Thread-safe, in-memory cache with LRU eviction:
# Automatic with default configuration
Azu.cache.set("key", "value")
# Manual configuration
config = Azu::Cache::Configuration.new
config.store = "memory"
config.max_size = 1000
cache = Azu::Cache::Manager.new(config)
Features:
Thread-safe with mutex protection
LRU (Least Recently Used) eviction
TTL support with automatic cleanup
Memory usage tracking
Access statistics
Redis Store
Distributed cache with Redis backend for multi-server deployments:
# Configure Redis cache
ENV["CACHE_STORE"] = "redis"
ENV["CACHE_REDIS_URL"] = "redis://localhost:6379/0"
# Or programmatically
config = Azu::Cache::Configuration.new
config.store = "redis"
config.redis_url = "redis://localhost:6379/1"
config.redis_pool_size = 10
config.redis_timeout = 5
cache = Azu::Cache::Manager.new(config)
Features:
Connection pooling for thread safety
Native Redis operations for better performance
Automatic failover and error handling
Supports all Redis data types
TTL support with Redis EXPIRE
Atomic increment/decrement operations
Multi-key operations with Redis pipelines
Redis Configuration:
CACHE_REDIS_URL
redis://localhost:6379/0
Redis connection URL
CACHE_REDIS_POOL_SIZE
5
Connection pool size
CACHE_REDIS_TIMEOUT
5
Connection timeout in seconds
# Redis URL formats
"redis://localhost:6379/0" # Basic
"redis://user:password@localhost:6379/1" # With auth
"redis://localhost:6379/2?timeout=10" # With options
"rediss://localhost:6380/0" # SSL/TLS
Null Store
Disabled cache for testing or when caching is not needed:
# Disable caching
ENV["CACHE_ENABLED"] = "false"
# Or programmatically
config = Azu::Cache::Configuration.new
config.enabled = false
cache = Azu::Cache::Manager.new(config)
Advanced Usage
Counter Operations
# Page view counter
Azu.cache.increment("page_views:#{page_id}")
Azu.cache.increment("page_views:#{page_id}", 5) # Increment by 5
# Decrement counters
Azu.cache.decrement("inventory:#{product_id}")
Azu.cache.decrement("inventory:#{product_id}", 3) # Decrement by 3
# Rate limiting example
struct RateLimitedEndpoint
include Endpoint(RateLimitRequest, RateLimitResponse)
def call : RateLimitResponse
key = "rate_limit:#{request.ip}"
current_count = Azu.cache.increment(key, ttl: 1.minute) || 0
if current_count > 100
raise Azu::Response::Error.new("Rate limit exceeded", 429)
end
RateLimitResponse.new(current_count)
end
end
Multi-Key Operations
# Get multiple keys at once
keys = ["user:1", "user:2", "user:3"]
results = Azu.cache.get_multi(keys)
results.each do |key, value|
puts "#{key}: #{value}"
end
# Set multiple keys at once
values = {
"user:1" => user1_data,
"user:2" => user2_data,
"user:3" => user3_data
}
Azu.cache.set_multi(values, ttl: 30.minutes)
Time Convenience Methods
# Use convenience methods for time spans
Azu.cache.set("key", "value", ttl: 5.minutes)
Azu.cache.set("key", "value", ttl: 2.hours)
Azu.cache.set("key", "value", ttl: 1.day)
# These extend Integer types
5.minutes # => Time::Span.new(minutes: 5)
2.hours # => Time::Span.new(hours: 2)
1.day # => Time::Span.new(days: 1)
Cache Statistics
# Get cache performance stats
stats = Azu.cache.stats
puts "Cache enabled: #{stats["enabled"]}"
puts "Store type: #{stats["store_type"]}"
puts "Current size: #{stats["size"]}"
puts "Hit rate: #{stats["hit_rate"]}%"
puts "Memory usage: #{stats["memory_usage_mb"]} MB"
Redis Performance Benefits
Redis store provides significant advantages for distributed applications:
# Redis-optimized counter operations
struct PageViewEndpoint
include Endpoint(PageViewRequest, PageViewResponse)
post "/page-views/:page_id"
def call : PageViewResponse
# Uses Redis INCR command for atomic increment
count = Azu.cache.increment("page_views:#{request.page_id}") || 0
# Set daily expiration using Redis EXPIRE
if count == 1
Azu.cache.increment("page_views:#{request.page_id}", 0, ttl: 1.day)
end
PageViewResponse.new(request.page_id, count)
end
end
# Redis multi-key operations for better performance
struct UserBatchEndpoint
include Endpoint(UserBatchRequest, UserBatchResponse)
get "/users/batch"
def call : UserBatchResponse
user_ids = request.user_ids
cache_keys = user_ids.map { |id| "user:#{id}" }
# Single Redis MGET command instead of multiple GET commands
cached_users = Azu.cache.get_multi(cache_keys)
# Process results efficiently
users = user_ids.map do |user_id|
key = "user:#{user_id}"
cached_users[key] || fetch_user_from_db(user_id)
end
UserBatchResponse.new(users)
end
end
Redis Connection Management
# Connection pooling automatically handles concurrent requests
struct HighTrafficEndpoint
include Endpoint(HighTrafficRequest, HighTrafficResponse)
def call : HighTrafficResponse
# Multiple concurrent requests share the Redis connection pool
data = Azu.cache.fetch("high_traffic_data", ttl: 1.minute) do
expensive_calculation()
end
HighTrafficResponse.new(data)
end
end
# Redis health checking
struct HealthCheckEndpoint
include Endpoint(HealthCheckRequest, HealthCheckResponse)
get "/health"
def call : HealthCheckResponse
redis_status = if Azu.cache.ping == "PONG"
"healthy"
else
"unhealthy"
end
HealthCheckResponse.new(redis_status)
end
end
Performance Considerations
Memory Store Performance
Thread Safety: Uses mutex for thread-safe operations
LRU Eviction: Efficiently manages memory usage
Cleanup: Automatic expired entry cleanup
Access Tracking: Maintains access order for LRU
Optimization Tips
# Use appropriate TTL values
Azu.cache.set("frequently_accessed", data, ttl: 5.minutes)
Azu.cache.set("rarely_accessed", data, ttl: 1.hour)
# Batch operations when possible
Azu.cache.set_multi(multiple_values, ttl: 30.minutes)
# Use key prefixes for organization
Azu.cache.set("user:profile:#{id}", profile_data)
Azu.cache.set("user:settings:#{id}", settings_data)
Cache Key Design
# Good: Hierarchical keys
"user:123:profile"
"user:123:settings"
"analytics:2024-01-15:page_views"
# Good: Include version for cache invalidation
"user:123:profile:v2"
"api:response:v1.2:users"
# Avoid: Very long keys
"user_profile_with_all_settings_and_preferences_123"
Best Practices
1. Use Appropriate TTL Values
# Short TTL for frequently changing data
Azu.cache.set("stock_price:#{symbol}", price, ttl: 1.minute)
# Medium TTL for user data
Azu.cache.set("user:#{id}", user_data, ttl: 15.minutes)
# Long TTL for static data
Azu.cache.set("config:#{key}", config_value, ttl: 1.hour)
2. Handle Cache Misses Gracefully
def get_user_data(user_id)
Azu.cache.fetch("user:#{user_id}", ttl: 15.minutes) do
begin
database.get_user(user_id).to_json
rescue DatabaseError
# Return default data if database is unavailable
default_user_data(user_id).to_json
end
end
end
3. Cache Invalidation Strategies
# Time-based invalidation (automatic)
Azu.cache.set("user:#{id}", data, ttl: 30.minutes)
# Manual invalidation
def update_user(user_id, new_data)
User.update(user_id, new_data)
Azu.cache.delete("user:#{user_id}")
end
# Pattern-based invalidation (for related data)
def clear_user_cache(user_id)
# Clear all user-related cache entries
Azu.cache.delete("user:#{user_id}")
Azu.cache.delete("user:#{user_id}:profile")
Azu.cache.delete("user:#{user_id}:settings")
end
4. Testing with Cache
# Disable cache in tests
ENV["CACHE_ENABLED"] = "false"
# Or use a separate cache instance for testing
config = Azu::Cache::Configuration.new
config.store = "memory"
config.max_size = 100
test_cache = Azu::Cache::Manager.new(config)
# Clear cache between tests
Azu.cache.clear
API Reference
Core Methods
# Get value from cache
Azu.cache.get(key : String) : String?
# Get value from cache with block and TTL (Rails-like)
Azu.cache.get(key : String, ttl : Time::Span? = nil, &block : -> String) : String
# Set value in cache
Azu.cache.set(key : String, value : String, ttl : Time::Span? = nil) : Bool
# Fetch with block (Rails-like)
Azu.cache.fetch(key : String, ttl : Time::Span? = nil, &block : -> String) : String
# Check if key exists
Azu.cache.exists?(key : String) : Bool
# Delete key
Azu.cache.delete(key : String) : Bool
# Clear all cache
Azu.cache.clear : Bool
# Get cache size
Azu.cache.size : Int32
Counter Operations
# Increment counter
Azu.cache.increment(key : String, amount : Int32 = 1, ttl : Time::Span? = nil) : Int32?
# Decrement counter
Azu.cache.decrement(key : String, amount : Int32 = 1, ttl : Time::Span? = nil) : Int32?
Multi-Key Operations
# Get multiple keys
Azu.cache.get_multi(keys : Array(String)) : Hash(String, String?)
# Set multiple keys
Azu.cache.set_multi(values : Hash(String, String), ttl : Time::Span? = nil) : Bool
Statistics
# Get cache statistics
Azu.cache.stats : Hash(String, Int32 | Float64 | String)
Environment Variables Reference
CACHE_ENABLED
true
Enable/disable caching
CACHE_STORE
memory
Cache store type (memory
, redis
, null
)
CACHE_MAX_SIZE
1000
Maximum cache entries (memory store)
CACHE_DEFAULT_TTL
3600
Default TTL in seconds
CACHE_KEY_PREFIX
azu
Prefix for all cache keys
CACHE_COMPRESS
false
Enable compression
CACHE_SERIALIZE
true
Enable serialization
CACHE_REDIS_URL
redis://localhost:6379/0
Redis connection URL
CACHE_REDIS_POOL_SIZE
5
Redis connection pool size
CACHE_REDIS_TIMEOUT
5
Redis connection timeout (seconds)
Error Handling
The cache system is designed to be resilient:
Disabled Cache: When caching is disabled, all operations return safely
Memory Limits: LRU eviction prevents memory exhaustion
Thread Safety: Mutex protection ensures safe concurrent access
Graceful Degradation: Failed cache operations don't break application flow
# Cache operations never throw exceptions
result = Azu.cache.get("key") # Returns nil if not found
success = Azu.cache.set("key", "value") # Returns false if failed
Usage Patterns Comparison
Azu provides multiple ways to cache data, giving you flexibility to choose the pattern that best fits your coding style:
Pattern 1: Traditional Get/Set
# Check cache first, set if missing
user_data = Azu.cache.get("user:#{user_id}")
if user_data.nil?
user_data = fetch_user_from_database(user_id)
Azu.cache.set("user:#{user_id}", user_data, ttl: 1.hour)
end
Pattern 2: Get with Block (Rails-like)
# All-in-one: get or execute block and cache
user_data = Azu.cache.get("user:#{user_id}", ttl: 1.hour) do
fetch_user_from_database(user_id)
end
Pattern 3: Fetch with Block (Traditional Rails)
# Traditional Rails pattern
user_data = Azu.cache.fetch("user:#{user_id}", ttl: 1.hour) do
fetch_user_from_database(user_id)
end
Both patterns 2 and 3 are functionally equivalent and provide the same caching behavior. Choose the one that feels more natural to your coding style.
This caching system provides a solid foundation for building high-performance web applications with Azu, offering both simplicity for basic use cases and powerful features for advanced scenarios.
Troubleshooting
Common Redis Issues
Connection Refused
Problem: Redis operation failed: Connection refused
Solutions:
Check if Redis is running:
redis-cli ping
Start Redis service:
# macOS brew services start redis # Linux sudo systemctl start redis-server
Check Redis configuration:
# Check Redis config redis-cli config get bind redis-cli config get port
Authentication Errors
Problem: Redis operation failed: NOAUTH Authentication required
Solution: Configure Redis password in your cache URL:
config.redis_url = "redis://:your-password@localhost:6379/0"
Connection Pool Timeout
Problem: Redis connection pool timeout
Solutions:
Increase pool size:
config.redis_pool_size = 20
Increase timeout:
config.redis_timeout = 10 # seconds
Check for connection leaks in your code
Memory Issues
Problem: Redis running out of memory
Solutions:
Configure Redis max memory:
redis-cli config set maxmemory 256mb redis-cli config set maxmemory-policy allkeys-lru
Use appropriate TTL values:
Azu.cache.set("key", "value", ttl: 1.hour) # Not permanent
Monitor Redis memory usage:
redis-cli info memory
Performance Optimization
Redis Configuration
For production use, consider these Redis configuration optimizations:
# In redis.conf or via redis-cli config set
# Enable persistence (choose one)
save 900 1 # Save if at least 1 key changed in 900 seconds
# OR
appendonly yes # Append-only file for durability
# Memory optimization
maxmemory-policy allkeys-lru
tcp-keepalive 300
# Network optimization
tcp-backlog 511
timeout 300
Connection Pooling Best Practices
# Production configuration
config = Azu::Cache::Configuration.new
config.store = "redis"
config.redis_url = ENV["REDIS_URL"]
config.redis_pool_size = 20 # Adjust based on concurrency needs
config.redis_timeout = 5 # Connection timeout in seconds
config.default_ttl = 3600 # 1 hour default TTL
Monitoring Redis
Health Checks
struct RedisHealthCheck
def self.healthy? : Bool
begin
Azu.cache.ping == "PONG"
rescue
false
end
end
def self.stats : Hash(String, String)
info = Azu.cache.redis_info
return {"status" => "unavailable"} unless info
{
"status" => "healthy",
"redis_version" => info["redis_version"]? || "unknown",
"connected_clients" => info["connected_clients"]? || "0",
"used_memory_human" => info["used_memory_human"]? || "0",
"keyspace_hits" => info["keyspace_hits"]? || "0",
"keyspace_misses" => info["keyspace_misses"]? || "0"
}
end
end
Key Metrics to Monitor
Memory usage:
used_memory
andused_memory_peak
Hit ratio:
keyspace_hits
/ (keyspace_hits
+keyspace_misses
)Connected clients:
connected_clients
Operations per second:
instantaneous_ops_per_sec
Network usage:
instantaneous_input_kbps
,instantaneous_output_kbps
This caching system provides a solid foundation for building high-performance web applications with Azu, offering both simplicity for basic use cases and powerful features for advanced scenarios.
Last updated
Was this helpful?