Skip to content
Merged
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
11 changes: 7 additions & 4 deletions .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.14.1' # Define the elixir version [required]
otp-version: '25.1.2' # Define the OTP version [required]
version-type: strict
version-file: .tool-versions

- name: Restore dependencies cache
uses: actions/cache@v3
Expand All @@ -49,5 +49,8 @@ jobs:
- name: Run Coveralls
run: mix coveralls.json

- name: Upload to codecov.io
uses: codecov/codecov-action@v1
- uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
verbose: true
4 changes: 2 additions & 2 deletions .github/workflows/credo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.14.1' # Define the elixir version [required]
otp-version: '25.1.2' # Define the OTP version [required]
version-type: strict
version-file: .tool-versions

- name: Restore dependencies cache
uses: actions/cache@v3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/dialyzer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.14.1' # Define the elixir version [required]
otp-version: '25.1.2' # Define the OTP version [required]
version-type: strict
version-file: .tool-versions

- name: Restore dependencies cache
uses: actions/cache@v3
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ jobs:
- name: Set up Elixir
uses: erlef/setup-beam@v1
with:
elixir-version: '1.14.1' # Define the elixir version [required]
otp-version: '25.1.2' # Define the OTP version [required]
version-type: strict
version-file: .tool-versions

- name: Restore dependencies cache
uses: actions/cache@v3
Expand Down
32 changes: 29 additions & 3 deletions guides/explanation/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ ElixirCache is designed around a simple principle: provide a consistent interfac

1. **Core Interface**: Defined by the `Cache` module
2. **Adapters**: Backend-specific implementations
3. **Term Encoder**: Handles serialization and compression
4. **Telemetry Integration**: For observability and metrics
5. **Sandbox System**: For isolated testing
3. **Strategy Adapters**: Higher-level patterns that compose over adapters
4. **Term Encoder**: Handles serialization and compression
5. **Telemetry Integration**: For observability and metrics
6. **Sandbox System**: For isolated testing

## The Cache Behaviour

Expand Down Expand Up @@ -82,6 +83,31 @@ A simple implementation using Elixir's Agent for lightweight in-memory storage.

Wraps the ConCache library to provide its expiration and callback capabilities.

## Strategy Adapters

Strategy adapters implement the `Cache.Strategy` behaviour and compose over
regular adapters to provide higher-level caching patterns. They are specified
using a tuple format: `adapter: {StrategyModule, UnderlyingAdapterOrConfig}`.

### Cache.HashRing

Distributes cache keys across Erlang cluster nodes using a consistent hash ring
powered by `libring`. Operations are forwarded to the owning node via
`:erpc.call/4`. The ring monitors node membership automatically.

### Cache.MultiLayer

Chains multiple cache modules together. Reads cascade fastest → slowest with
automatic backfill on slower-layer hits. Writes go slowest → fastest to ensure
durability before populating fast layers.

### Cache.RefreshAhead

Proactively refreshes values in the background before they expire. When a `get`
detects a value is within the refresh window, it returns the current value
immediately and spawns an async task to fetch a fresh one. Uses per-node ETS
deduplication and cross-node `:global` locking to prevent redundant refreshes.

## Telemetry Integration

ElixirCache provides telemetry events for all cache operations:
Expand Down
32 changes: 10 additions & 22 deletions guides/how-to/choosing_adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ defmodule MyApp.PersistentCache do
adapter: Cache.DETS,
name: :my_app_persistent_cache,
opts: [
file: "cache_data.dets"
file_path: "/tmp/cache_data"
]
end
```
Expand All @@ -71,9 +71,9 @@ defmodule MyApp.DistributedCache do
adapter: Cache.Redis,
name: :my_app_redis_cache,
opts: [
host: "localhost",
port: 6379,
pool_size: 5
uri: "redis://localhost:6379",
size: 10,
max_overflow: 5
]
end
```
Expand Down Expand Up @@ -143,24 +143,12 @@ defmodule MyApp.Cache do
name: :my_app_cache,
opts: get_opts()

defp get_adapter do
case Mix.env() do
:test -> Cache.Sandbox
:dev -> Cache.ETS
:prod -> Cache.Redis
end
end

defp get_opts do
case Mix.env() do
:test -> []
:dev -> [read_concurrency: true]
:prod -> [
host: System.get_env("REDIS_HOST", "localhost"),
port: String.to_integer(System.get_env("REDIS_PORT", "6379")),
pool_size: 10
]
end
if Mix.env() === :test do
defp get_adapter, do: Cache.ETS
defp get_opts, do: []
else
defp get_adapter, do: Cache.Redis
defp get_opts, do: [uri: "redis://localhost:6379", size: 10]
end
end
```
Expand Down
51 changes: 19 additions & 32 deletions guides/how-to/redis_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ defmodule MyApp.RedisCache do
adapter: Cache.Redis,
name: :my_app_redis,
opts: [
host: "localhost",
port: 6379,
pool_size: 5
uri: "redis://localhost:6379",
size: 10,
max_overflow: 5
]
end
```
Expand All @@ -46,38 +46,26 @@ end

## Redis Configuration Options

The Redis adapter supports various configuration options:
The Redis adapter accepts the following options:

| Option | Type | Required | Description |
|---|---|---|---|
| `uri` | `string` | Yes | Redis connection URI (e.g. `"redis://localhost:6379"`, `"redis://:password@host:6379/0"`) |
| `size` | `pos_integer` | No | Number of workers in the connection pool (default: 50) |
| `max_overflow` | `pos_integer` | No | Maximum overflow workers the pool can create (default: 20) |
| `strategy` | `:fifo` or `:lifo` | No | Queue strategy for the Poolboy connection pool |

Authentication, database selection, and SSL are configured via the URI string:

```elixir
defmodule MyApp.RedisCache do
use Cache,
adapter: Cache.Redis,
name: :my_app_redis,
opts: [
# Connection settings
host: "redis.example.com",
port: 6379,
password: "your_password", # Optional
database: 0, # Optional, default is 0

# Connection pool settings
pool_size: 10, # Number of connections in the pool
max_overflow: 5, # Maximum number of overflow workers

# Timeout settings
timeout: 5000, # Connection timeout in milliseconds

# SSL options
ssl: true, # Enable SSL
ssl_opts: [ # SSL options
verify: :verify_peer,
cacertfile: "/path/to/ca_certificate.pem",
certfile: "/path/to/client_certificate.pem",
keyfile: "/path/to/client_key.pem"
],

# Encoding options
compression_level: 1 # Level of compression (0-9, higher = more compression)
uri: "redis://:my_password@redis.example.com:6379/2",
size: 10,
max_overflow: 5
]
end
```
Expand All @@ -92,10 +80,9 @@ defmodule MyApp.RedisCache do
adapter: Cache.Redis,
name: :my_app_redis,
opts: [
host: System.get_env("REDIS_HOST", "localhost"),
port: String.to_integer(System.get_env("REDIS_PORT", "6379")),
password: System.get_env("REDIS_PASSWORD"),
pool_size: String.to_integer(System.get_env("REDIS_POOL_SIZE", "10"))
uri: System.get_env("REDIS_URL", "redis://localhost:6379"),
size: String.to_integer(System.get_env("REDIS_POOL_SIZE", "10")),
max_overflow: 5
]
end
```
Expand Down
90 changes: 28 additions & 62 deletions guides/how-to/testing_with_cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,105 +4,71 @@ This guide explains how to effectively test applications that use ElixirCache, f

## Using the Sandbox Mode

ElixirCache provides a sandbox mode specifically designed for testing. This ensures that your tests:
ElixirCache provides a sandbox mode that gives each test its own isolated cache
namespace. This ensures your tests:

1. Are isolated from each other
2. Don't leave lingering cache data between test runs
3. Can run in parallel without conflicts

### Configuring Your Cache for Testing
### Configuring Your Cache

In your test environment, you can wrap any cache adapter with the sandbox functionality:
Use `sandbox?: Mix.env() === :test` on your cache module. The adapter stays the
same in every environment — the sandbox wraps whatever adapter you choose:

```elixir
# In lib/my_app/cache.ex
defmodule MyApp.Cache do
use Cache,
adapter: get_cache_adapter(),
adapter: Cache.Redis,
name: :my_app_cache,
opts: get_cache_opts(),
# Enable sandbox mode in test environment
sandbox?: Mix.env() == :test

defp get_cache_adapter do
case Mix.env() do
:test -> Cache.ETS
:dev -> Cache.ETS
:prod -> Cache.Redis
end
end

defp get_cache_opts do
case Mix.env() do
:test -> []
:dev -> []
:prod -> [host: "redis.example.com", port: 6379]
end
end
opts: [uri: "redis://localhost:6379"],
sandbox?: Mix.env() === :test
end
```

When `sandbox?` is `true`, `Cache.Sandbox` is used as the adapter automatically.
You do not need to switch adapters between environments.

### Setting Up the Sandbox Registry

To use the sandbox functionality in your tests, you need to start the `Cache.SandboxRegistry` in your test setup:
Add `Cache.SandboxRegistry.start_link()` to your `test/test_helper.exs`:

```elixir
# In test/test_helper.exs
Cache.SandboxRegistry.start_link()
ExUnit.start()

# Start the sandbox registry for your tests
{:ok, _pid} = Cache.SandboxRegistry.start_link()

# Start your application's supervision tree
Application.ensure_all_started(:my_app)
```

### Using the Sandbox in Tests

Using the sandbox in your tests is very simple. All you need to do is start the sandbox registry in your setup block:
Register your cache in each test's `setup` block with
`Cache.SandboxRegistry.start/1`. This starts the cache supervisor and
registers the current test process for isolation:

```elixir
defmodule MyApp.CacheTest do
use ExUnit.Case, async: true

defmodule TestCache do
use Cache,
adapter: Cache.Redis, # The actual adapter doesn't matter in sandbox mode
name: :test_cache,
opts: [],
sandbox?: Mix.env() === :test
end

setup do
# This single line is all you need to set up sandbox isolation
Cache.SandboxRegistry.start(TestCache)
Cache.SandboxRegistry.start(MyApp.Cache)
:ok
end

test "can store and retrieve values" do
assert :ok = TestCache.put("test-key", "test-value")
assert {:ok, "test-value"} = TestCache.get("test-key")
end

test "can handle complex data structures" do
data = %{users: [%{name: "Alice"}, %{name: "Bob"}]}
assert :ok = TestCache.put("complex-key", data)
assert {:ok, ^data} = TestCache.get("complex-key")

test "stores and retrieves values" do
assert :ok === MyApp.Cache.put("key", "value")
assert {:ok, "value"} === MyApp.Cache.get("key")
end

test "provides isolation between tests" do
# This will return nil because each test has an isolated cache
assert {:ok, nil} = TestCache.get("test-key")
test "each test is isolated" do
assert {:ok, nil} === MyApp.Cache.get("key")
end
end
```


## Tips for Testing with ElixirCache

1. **Always use the sandbox in tests**: This prevents interference between tests.
2. **Clean up after each test**: Use `on_exit` to unregister from the sandbox.
3. **Use unique keys**: Even with sandboxing, using descriptive, unique keys makes debugging easier.
4. **Test edge cases**: Including cache misses, errors, and TTL expiration.
5. **Consider using fixtures**: For commonly cached data structures.
6. **Verify telemetry events**: If your application relies on cache metrics.
1. **Always use `sandbox?: Mix.env() === :test`**: Keep the same adapter everywhere — the sandbox handles isolation.
2. **Use `Cache.SandboxRegistry.start/1` in setup**: This is the only line needed per test.
3. **Tests can be `async: true`**: Each test gets its own sandbox namespace.
4. **Test edge cases**: Cache misses, errors, and TTL expiration.
5. **Verify telemetry events**: If your application relies on cache metrics.
Loading
Loading