https://github.com/dreamingechoes/search_cache
A simple example of how GenServer works in Elixir.
https://github.com/dreamingechoes/search_cache
elixir elixir-lang genserver
Last synced: about 1 year ago
JSON representation
A simple example of how GenServer works in Elixir.
- Host: GitHub
- URL: https://github.com/dreamingechoes/search_cache
- Owner: dreamingechoes
- License: mit
- Created: 2025-04-12T09:57:24.000Z (about 1 year ago)
- Default Branch: main
- Last Pushed: 2025-04-12T10:14:23.000Z (about 1 year ago)
- Last Synced: 2025-04-12T10:40:03.562Z (about 1 year ago)
- Topics: elixir, elixir-lang, genserver
- Language: Elixir
- Homepage: https://dreamingecho.es/blog/the-anatomy-of-a-genserver
- Size: 0 Bytes
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# SearchCache
A minimal, production-ready Elixir GenServer-based in-memory cache with TTL (time-to-live) support, observability via Telemetry, and a clean, well-tested public API.
This project is built to complement the article ["The Anatomy of a GenServer"](https://dreamingecho.es/blog/the-anatomy-of-a-genserver), illustrating best practices in stateful process design, fault-tolerant behavior, and real-world concurrency patterns in Elixir.
## β¨ Features
- β
Built on [GenServer](https://hexdocs.pm/elixir/GenServer.html) for process-based state and message handling
- π In-memory cache with configurable TTL expiration
- π§Ή FIFO eviction strategy when reaching a configurable max size
- π Periodic logging of stats via `handle_info/2`
- π‘ Built-in [Telemetry](https://hexdocs.pm/telemetry) instrumentation for monitoring
- π§ͺ 100% test coverage with ExUnit, including concurrent scenarios
- π Clean public API (`fetch/2`, `cache/3`, `cache_sync/3`)
- π§± Isolated test processes using Registry
- β
CI-ready with GitHub Actions + ExCoveralls for coverage reports
## π Getting Started
### 1. Clone and install dependencies
```bash
$ git clone https://github.com/your-name/search_cache.git
$ cd search_cache
$ mix deps.get
```
### 2. Run the app in IEx
```bash
$ iex -S mix
```
### 3. Use the Cache
```elixir
iex> SearchCache.cache("elixir", %{docs: ["Getting Started"]})
:ok
iex> SearchCache.fetch("elixir")
%{docs: ["Getting Started"]}
```
## β¨ Usage Example
You can embed `SearchCache` into a real app by supervising it and interacting through its public API:
```elixir
# application.ex
children = [
{Registry, keys: :unique, name: Registry.SearchCache},
{SearchCache, name: SearchCache}
]
```
### Dictionary Example
Suppose youβre building a multilingual dictionary lookup:
```elixir
# Start a named SearchCache instance (if not already supervised)
{:ok, _pid} = SearchCache.start_link(name: :dict)
# Define and cache dictionary entries
entries = %{
"hello" => %{es: "hola", fr: "bonjour"},
"thanks" => %{es: "gracias", fr: "merci"}
}
# Store the entry under the :dict process
SearchCache.cache(:dict, "hello", entries["hello"])
# Later, fetch a translation
case SearchCache.fetch(:dict, "hello") do
nil -> "Word not found"
result -> result[:es] # => "hola"
end
```
This use case is great for:
- β‘ Reducing repeated parsing or DB lookups
- π§ Storing complex, nested data structures per term
- π Supporting multi-language apps with low-latency access
You can also spin up named instances dynamically:
```elixir
{:ok, _pid} = SearchCache.start_link(name: {:via, Registry, {Registry.SearchCache, :my_dict}})
SearchCache.cache({:via, Registry, {Registry.SearchCache, :my_dict}}, "bye", %{es: "adiΓ³s"})
```
## π― Test Coverage Highlights
- β
Tests for all public API behaviors (sync and async)
- π° TTL expiration simulated using `:sys.replace_state/2`
- π Concurrency safety ensured via `Task.async/await`
- π
`handle_info/2` log behavior tested with `ExUnit.CaptureIO`
- π‘ Telemetry validation with `:telemetry.attach_many`
- π§Ό Fully isolated GenServers using `Registry` and `start_supervised!`
## π§ Configuration
The following parameters can be adjusted directly in `lib/search_cache.ex`:
```elixir
@ttl_seconds 300 # Cache entry TTL (in seconds)
@max_cache_size 100 # Max number of entries before eviction
@log_interval_ms 60_000 # Interval between stats logs
```
For more advanced setups, these could be passed via `start_link/1` options and stored in the GenServer state.
## π¦ Project Structure
```
search_cache/
βββ lib/
β βββ search_cache.ex # GenServer implementation
β βββ search_cache/application.ex # Application + Registry supervisor
βββ test/
β βββ search_cache_test.exs # Full ExUnit test coverage
βββ config/
β βββ config.exs # Environment config
βββ .formatter.exs
βββ .gitignore
βββ LICENSE
βββ mix.exs
βββ README.md
βββ .github/workflows/ci.yml # CI pipeline (build + test + coverage)
```
## π‘ Telemetry Events
The following events are emitted for observability and monitoring:
- `[:search_cache, :fetch]`
- **Measurement**: `%{hit: true | false}`
- **Metadata**: `%{query: string}`
- Emitted every time a `fetch/2` call is made, indicating whether the key was found.
- `[:search_cache, :cache]`
- **Measurement**: `%{size: integer}`
- **Metadata**: `%{query: string}`
- Emitted whenever an entry is cached, with current total entries.
To consume these events, use `:telemetry.attach/4` or `:telemetry.attach_many/4` like so:
```elixir
:telemetry.attach_many("logger", [
[:search_cache, :fetch],
[:search_cache, :cache]
], fn event, meas, meta, _config ->
IO.inspect({event, meas, meta}, label: "[Telemetry]")
end, nil)
```
This allows you to log metrics, report to a dashboard (e.g., Prometheus or AppSignal), or trigger custom alerts.
## π§ Use Cases
This module can be adapted for:
- Caching expensive search or API results
- In-memory rate limiting or throttling
- Memoization of function output
- Request deduplication / response coalescing
- Lightweight state management for prototyping
## π Learn More
- [Elixir GenServer Documentation](https://hexdocs.pm/elixir/GenServer.html)
- [Telemetry in Elixir](https://hexdocs.pm/telemetry/Telemetry.html)
- [Elixir Registry](https://hexdocs.pm/elixir/Registry.html)
- [ExCoveralls Docs](https://github.com/parroty/excoveralls)