Skip to content

Commit 7ab8357

Browse files
committed
chore: update docs
1 parent c6b181e commit 7ab8357

15 files changed

Lines changed: 690 additions & 222 deletions

guides/explanation/architecture.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ ElixirCache is designed around a simple principle: provide a consistent interfac
88

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

1516
## The Cache Behaviour
1617

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

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

86+
## Strategy Adapters
87+
88+
Strategy adapters implement the `Cache.Strategy` behaviour and compose over
89+
regular adapters to provide higher-level caching patterns. They are specified
90+
using a tuple format: `adapter: {StrategyModule, UnderlyingAdapterOrConfig}`.
91+
92+
### Cache.HashRing
93+
94+
Distributes cache keys across Erlang cluster nodes using a consistent hash ring
95+
powered by `libring`. Operations are forwarded to the owning node via
96+
`:erpc.call/4`. The ring monitors node membership automatically.
97+
98+
### Cache.MultiLayer
99+
100+
Chains multiple cache modules together. Reads cascade fastest → slowest with
101+
automatic backfill on slower-layer hits. Writes go slowest → fastest to ensure
102+
durability before populating fast layers.
103+
104+
### Cache.RefreshAhead
105+
106+
Proactively refreshes values in the background before they expire. When a `get`
107+
detects a value is within the refresh window, it returns the current value
108+
immediately and spawns an async task to fetch a fresh one. Uses per-node ETS
109+
deduplication and cross-node `:global` locking to prevent redundant refreshes.
110+
85111
## Telemetry Integration
86112

87113
ElixirCache provides telemetry events for all cache operations:

guides/how-to/choosing_adapter.md

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ defmodule MyApp.PersistentCache do
6262
adapter: Cache.DETS,
6363
name: :my_app_persistent_cache,
6464
opts: [
65-
file: "cache_data.dets"
65+
file_path: "/tmp/cache_data"
6666
]
6767
end
6868
```
@@ -82,9 +82,9 @@ defmodule MyApp.DistributedCache do
8282
adapter: Cache.Redis,
8383
name: :my_app_redis_cache,
8484
opts: [
85-
host: "localhost",
86-
port: 6379,
87-
pool_size: 5
85+
uri: "redis://localhost:6379",
86+
size: 10,
87+
max_overflow: 5
8888
]
8989
end
9090
```
@@ -154,24 +154,12 @@ defmodule MyApp.Cache do
154154
name: :my_app_cache,
155155
opts: get_opts()
156156

157-
defp get_adapter do
158-
case Mix.env() do
159-
:test -> Cache.Sandbox
160-
:dev -> Cache.ETS
161-
:prod -> Cache.Redis
162-
end
163-
end
164-
165-
defp get_opts do
166-
case Mix.env() do
167-
:test -> []
168-
:dev -> [read_concurrency: true]
169-
:prod -> [
170-
host: System.get_env("REDIS_HOST", "localhost"),
171-
port: String.to_integer(System.get_env("REDIS_PORT", "6379")),
172-
pool_size: 10
173-
]
174-
end
157+
if Mix.env() === :test do
158+
defp get_adapter, do: Cache.ETS
159+
defp get_opts, do: []
160+
else
161+
defp get_adapter, do: Cache.Redis
162+
defp get_opts, do: [uri: "redis://localhost:6379", size: 10]
175163
end
176164
end
177165
```

guides/how-to/redis_setup.md

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ defmodule MyApp.RedisCache do
2121
adapter: Cache.Redis,
2222
name: :my_app_redis,
2323
opts: [
24-
host: "localhost",
25-
port: 6379,
26-
pool_size: 5
24+
uri: "redis://localhost:6379",
25+
size: 10,
26+
max_overflow: 5
2727
]
2828
end
2929
```
@@ -46,38 +46,26 @@ end
4646

4747
## Redis Configuration Options
4848

49-
The Redis adapter supports various configuration options:
49+
The Redis adapter accepts the following options:
50+
51+
| Option | Type | Required | Description |
52+
|---|---|---|---|
53+
| `uri` | `string` | Yes | Redis connection URI (e.g. `"redis://localhost:6379"`, `"redis://:password@host:6379/0"`) |
54+
| `size` | `pos_integer` | No | Number of workers in the connection pool (default: 50) |
55+
| `max_overflow` | `pos_integer` | No | Maximum overflow workers the pool can create (default: 20) |
56+
| `strategy` | `:fifo` or `:lifo` | No | Queue strategy for the Poolboy connection pool |
57+
58+
Authentication, database selection, and SSL are configured via the URI string:
5059

5160
```elixir
5261
defmodule MyApp.RedisCache do
5362
use Cache,
5463
adapter: Cache.Redis,
5564
name: :my_app_redis,
5665
opts: [
57-
# Connection settings
58-
host: "redis.example.com",
59-
port: 6379,
60-
password: "your_password", # Optional
61-
database: 0, # Optional, default is 0
62-
63-
# Connection pool settings
64-
pool_size: 10, # Number of connections in the pool
65-
max_overflow: 5, # Maximum number of overflow workers
66-
67-
# Timeout settings
68-
timeout: 5000, # Connection timeout in milliseconds
69-
70-
# SSL options
71-
ssl: true, # Enable SSL
72-
ssl_opts: [ # SSL options
73-
verify: :verify_peer,
74-
cacertfile: "/path/to/ca_certificate.pem",
75-
certfile: "/path/to/client_certificate.pem",
76-
keyfile: "/path/to/client_key.pem"
77-
],
78-
79-
# Encoding options
80-
compression_level: 1 # Level of compression (0-9, higher = more compression)
66+
uri: "redis://:my_password@redis.example.com:6379/2",
67+
size: 10,
68+
max_overflow: 5
8169
]
8270
end
8371
```
@@ -92,10 +80,9 @@ defmodule MyApp.RedisCache do
9280
adapter: Cache.Redis,
9381
name: :my_app_redis,
9482
opts: [
95-
host: System.get_env("REDIS_HOST", "localhost"),
96-
port: String.to_integer(System.get_env("REDIS_PORT", "6379")),
97-
password: System.get_env("REDIS_PASSWORD"),
98-
pool_size: String.to_integer(System.get_env("REDIS_POOL_SIZE", "10"))
83+
uri: System.get_env("REDIS_URL", "redis://localhost:6379"),
84+
size: String.to_integer(System.get_env("REDIS_POOL_SIZE", "10")),
85+
max_overflow: 5
9986
]
10087
end
10188
```

guides/how-to/testing_with_cache.md

Lines changed: 28 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -4,105 +4,71 @@ This guide explains how to effectively test applications that use ElixirCache, f
44

55
## Using the Sandbox Mode
66

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

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

13-
### Configuring Your Cache for Testing
14+
### Configuring Your Cache
1415

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

1719
```elixir
18-
# In lib/my_app/cache.ex
1920
defmodule MyApp.Cache do
2021
use Cache,
21-
adapter: get_cache_adapter(),
22+
adapter: Cache.Redis,
2223
name: :my_app_cache,
23-
opts: get_cache_opts(),
24-
# Enable sandbox mode in test environment
25-
sandbox?: Mix.env() == :test
26-
27-
defp get_cache_adapter do
28-
case Mix.env() do
29-
:test -> Cache.ETS
30-
:dev -> Cache.ETS
31-
:prod -> Cache.Redis
32-
end
33-
end
34-
35-
defp get_cache_opts do
36-
case Mix.env() do
37-
:test -> []
38-
:dev -> []
39-
:prod -> [host: "redis.example.com", port: 6379]
40-
end
41-
end
24+
opts: [uri: "redis://localhost:6379"],
25+
sandbox?: Mix.env() === :test
4226
end
4327
```
4428

29+
When `sandbox?` is `true`, `Cache.Sandbox` is used as the adapter automatically.
30+
You do not need to switch adapters between environments.
31+
4532
### Setting Up the Sandbox Registry
4633

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

4936
```elixir
5037
# In test/test_helper.exs
38+
Cache.SandboxRegistry.start_link()
5139
ExUnit.start()
52-
53-
# Start the sandbox registry for your tests
54-
{:ok, _pid} = Cache.SandboxRegistry.start_link()
55-
56-
# Start your application's supervision tree
57-
Application.ensure_all_started(:my_app)
5840
```
5941

6042
### Using the Sandbox in Tests
6143

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

6448
```elixir
6549
defmodule MyApp.CacheTest do
6650
use ExUnit.Case, async: true
6751

68-
defmodule TestCache do
69-
use Cache,
70-
adapter: Cache.Redis, # The actual adapter doesn't matter in sandbox mode
71-
name: :test_cache,
72-
opts: [],
73-
sandbox?: Mix.env() === :test
74-
end
75-
7652
setup do
77-
# This single line is all you need to set up sandbox isolation
78-
Cache.SandboxRegistry.start(TestCache)
53+
Cache.SandboxRegistry.start(MyApp.Cache)
7954
:ok
8055
end
81-
82-
test "can store and retrieve values" do
83-
assert :ok = TestCache.put("test-key", "test-value")
84-
assert {:ok, "test-value"} = TestCache.get("test-key")
85-
end
86-
87-
test "can handle complex data structures" do
88-
data = %{users: [%{name: "Alice"}, %{name: "Bob"}]}
89-
assert :ok = TestCache.put("complex-key", data)
90-
assert {:ok, ^data} = TestCache.get("complex-key")
56+
57+
test "stores and retrieves values" do
58+
assert :ok === MyApp.Cache.put("key", "value")
59+
assert {:ok, "value"} === MyApp.Cache.get("key")
9160
end
9261

93-
test "provides isolation between tests" do
94-
# This will return nil because each test has an isolated cache
95-
assert {:ok, nil} = TestCache.get("test-key")
62+
test "each test is isolated" do
63+
assert {:ok, nil} === MyApp.Cache.get("key")
9664
end
9765
end
9866
```
9967

100-
10168
## Tips for Testing with ElixirCache
10269

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

0 commit comments

Comments
 (0)