This commit introduces a dual-mode caching and rate limiting system that can use either Redis (for distributed deployments) or ETS/Cachex (for single-node setups). Key changes: - Add Cache abstraction layer that automatically switches between Redis and Cachex - Implement RedisCache module with full distributed cache functionality - Implement RedisRateLimiter with sliding window algorithm for accurate rate limiting - Add RateLimiterWrapper to provide unified API for both implementations - Update application startup to conditionally use Redis when REDIS_URL is set - Migrate all cache operations to use the new Cache abstraction - Support graceful fallback to ETS-based solutions when Redis is unavailable The system automatically detects Redis availability via REDIS_URL environment variable and switches between implementations without code changes. This enables proper distributed caching and rate limiting in Kubernetes deployments while maintaining backward compatibility for development environments. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
124 lines
2.7 KiB
Elixir
124 lines
2.7 KiB
Elixir
defmodule Aprsme.Cache do
|
|
@moduledoc """
|
|
Cache abstraction layer that works with both Cachex and RedisCache.
|
|
Provides a unified API regardless of the underlying implementation.
|
|
"""
|
|
|
|
@doc """
|
|
Get a value from cache
|
|
"""
|
|
def get(cache_name, key) do
|
|
if using_redis?() do
|
|
Aprsme.RedisCache.get(cache_name, key)
|
|
else
|
|
Cachex.get(cache_name, key)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Put a value in cache with optional TTL
|
|
"""
|
|
def put(cache_name, key, value, opts \\ []) do
|
|
if using_redis?() do
|
|
# Convert TTL from milliseconds to seconds for Redis
|
|
opts = convert_ttl_to_seconds(opts)
|
|
Aprsme.RedisCache.put(cache_name, key, value, opts)
|
|
else
|
|
Cachex.put(cache_name, key, value, opts)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Delete a key from cache
|
|
"""
|
|
def del(cache_name, key) do
|
|
if using_redis?() do
|
|
Aprsme.RedisCache.del(cache_name, key)
|
|
else
|
|
Cachex.del(cache_name, key)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Clear all keys from cache
|
|
"""
|
|
def clear(cache_name) do
|
|
if using_redis?() do
|
|
Aprsme.RedisCache.clear(cache_name)
|
|
else
|
|
Cachex.clear(cache_name)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Get cache statistics
|
|
"""
|
|
def stats(cache_name) do
|
|
if using_redis?() do
|
|
Aprsme.RedisCache.stats(cache_name)
|
|
else
|
|
Cachex.stats(cache_name)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Check if key exists
|
|
"""
|
|
def exists?(cache_name, key) do
|
|
if using_redis?() do
|
|
Aprsme.RedisCache.exists?(cache_name, key)
|
|
else
|
|
Cachex.exists?(cache_name, key)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Get TTL for a key
|
|
"""
|
|
def ttl(cache_name, key) do
|
|
if using_redis?() do
|
|
Aprsme.RedisCache.ttl(cache_name, key)
|
|
else
|
|
Cachex.ttl(cache_name, key)
|
|
end
|
|
end
|
|
|
|
# Helper functions
|
|
|
|
defp using_redis? do
|
|
System.get_env("REDIS_URL") != nil
|
|
end
|
|
|
|
defp convert_ttl_to_seconds(opts) do
|
|
case Keyword.get(opts, :ttl) do
|
|
nil ->
|
|
opts
|
|
|
|
ttl_ms when is_integer(ttl_ms) ->
|
|
# Convert milliseconds to seconds
|
|
Keyword.put(opts, :ttl, div(ttl_ms, 1000))
|
|
|
|
_ ->
|
|
opts
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Convert timeout keyword list to milliseconds
|
|
"""
|
|
def to_timeout(opts) do
|
|
# Cachex's to_timeout is private, so we implement our own
|
|
Enum.reduce(opts, 0, fn
|
|
{:second, n}, acc -> acc + n * 1000
|
|
{:seconds, n}, acc -> acc + n * 1000
|
|
{:minute, n}, acc -> acc + n * 60 * 1000
|
|
{:minutes, n}, acc -> acc + n * 60 * 1000
|
|
{:hour, n}, acc -> acc + n * 60 * 60 * 1000
|
|
{:hours, n}, acc -> acc + n * 60 * 60 * 1000
|
|
{:day, n}, acc -> acc + n * 24 * 60 * 60 * 1000
|
|
{:days, n}, acc -> acc + n * 24 * 60 * 60 * 1000
|
|
{:millisecond, n}, acc -> acc + n
|
|
{:milliseconds, n}, acc -> acc + n
|
|
end)
|
|
end
|
|
end
|