aprs.me/lib/aprsme/cache.ex
Graham McIntire 226a53bf61
Add Redis-based distributed caching and rate limiting
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>
2025-07-26 15:29:38 -05:00

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