{"id":27595683,"url":"https://github.com/dreamingechoes/search_cache","last_synced_at":"2025-04-22T12:16:47.468Z","repository":{"id":287536420,"uuid":"965051844","full_name":"dreamingechoes/search_cache","owner":"dreamingechoes","description":"A simple example of how GenServer works in Elixir.","archived":false,"fork":false,"pushed_at":"2025-04-12T10:14:23.000Z","size":0,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-12T10:40:03.562Z","etag":null,"topics":["elixir","elixir-lang","genserver"],"latest_commit_sha":null,"homepage":"https://dreamingecho.es/blog/the-anatomy-of-a-genserver","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dreamingechoes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-04-12T09:57:24.000Z","updated_at":"2025-04-12T10:14:26.000Z","dependencies_parsed_at":"2025-04-12T10:52:02.252Z","dependency_job_id":null,"html_url":"https://github.com/dreamingechoes/search_cache","commit_stats":null,"previous_names":["dreamingechoes/search_cache"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dreamingechoes%2Fsearch_cache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dreamingechoes%2Fsearch_cache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dreamingechoes%2Fsearch_cache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dreamingechoes%2Fsearch_cache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dreamingechoes","download_url":"https://codeload.github.com/dreamingechoes/search_cache/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250237844,"owners_count":21397403,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["elixir","elixir-lang","genserver"],"created_at":"2025-04-22T12:16:46.973Z","updated_at":"2025-04-22T12:16:47.460Z","avatar_url":"https://github.com/dreamingechoes.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SearchCache\n\nA 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.\n\nThis 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.\n\n## ✨ Features\n\n- ✅ Built on [GenServer](https://hexdocs.pm/elixir/GenServer.html) for process-based state and message handling\n- 🕒 In-memory cache with configurable TTL expiration\n- 🧹 FIFO eviction strategy when reaching a configurable max size\n- 📊 Periodic logging of stats via `handle_info/2`\n- 🛡 Built-in [Telemetry](https://hexdocs.pm/telemetry) instrumentation for monitoring\n- 🧪 100% test coverage with ExUnit, including concurrent scenarios\n- 🌟 Clean public API (`fetch/2`, `cache/3`, `cache_sync/3`)\n- 🧱 Isolated test processes using Registry\n- ✅ CI-ready with GitHub Actions + ExCoveralls for coverage reports\n\n## 🚀 Getting Started\n\n### 1. Clone and install dependencies\n\n```bash\n$ git clone https://github.com/your-name/search_cache.git\n$ cd search_cache\n$ mix deps.get\n```\n\n### 2. Run the app in IEx\n\n```bash\n$ iex -S mix\n```\n\n### 3. Use the Cache\n\n```elixir\niex\u003e SearchCache.cache(\"elixir\", %{docs: [\"Getting Started\"]})\n:ok\n\niex\u003e SearchCache.fetch(\"elixir\")\n%{docs: [\"Getting Started\"]}\n```\n\n## ✨ Usage Example\n\nYou can embed `SearchCache` into a real app by supervising it and interacting through its public API:\n\n```elixir\n# application.ex\nchildren = [\n  {Registry, keys: :unique, name: Registry.SearchCache},\n  {SearchCache, name: SearchCache}\n]\n```\n\n### Dictionary Example\n\nSuppose you’re building a multilingual dictionary lookup:\n\n```elixir\n# Start a named SearchCache instance (if not already supervised)\n{:ok, _pid} = SearchCache.start_link(name: :dict)\n\n# Define and cache dictionary entries\nentries = %{\n  \"hello\" =\u003e %{es: \"hola\", fr: \"bonjour\"},\n  \"thanks\" =\u003e %{es: \"gracias\", fr: \"merci\"}\n}\n\n# Store the entry under the :dict process\nSearchCache.cache(:dict, \"hello\", entries[\"hello\"])\n\n# Later, fetch a translation\ncase SearchCache.fetch(:dict, \"hello\") do\n  nil -\u003e \"Word not found\"\n  result -\u003e result[:es]  # =\u003e \"hola\"\nend\n```\n\nThis use case is great for:\n- ⚡ Reducing repeated parsing or DB lookups\n- 🧠 Storing complex, nested data structures per term\n- 🔠 Supporting multi-language apps with low-latency access\n\nYou can also spin up named instances dynamically:\n\n```elixir\n{:ok, _pid} = SearchCache.start_link(name: {:via, Registry, {Registry.SearchCache, :my_dict}})\nSearchCache.cache({:via, Registry, {Registry.SearchCache, :my_dict}}, \"bye\", %{es: \"adiós\"})\n```\n\n## 💯 Test Coverage Highlights\n\n- ✅ Tests for all public API behaviors (sync and async)\n- 🕰 TTL expiration simulated using `:sys.replace_state/2`\n- 🔀 Concurrency safety ensured via `Task.async/await`\n- 📅 `handle_info/2` log behavior tested with `ExUnit.CaptureIO`\n- 🛡 Telemetry validation with `:telemetry.attach_many`\n- 🧼 Fully isolated GenServers using `Registry` and `start_supervised!`\n\n## 🔧 Configuration\n\nThe following parameters can be adjusted directly in `lib/search_cache.ex`:\n\n```elixir\n@ttl_seconds 300        # Cache entry TTL (in seconds)\n@max_cache_size 100     # Max number of entries before eviction\n@log_interval_ms 60_000 # Interval between stats logs\n```\n\nFor more advanced setups, these could be passed via `start_link/1` options and stored in the GenServer state.\n\n## 📦 Project Structure\n\n```\nsearch_cache/\n├── lib/\n│   ├── search_cache.ex             # GenServer implementation\n│   └── search_cache/application.ex # Application + Registry supervisor\n├── test/\n│   └── search_cache_test.exs       # Full ExUnit test coverage\n├── config/\n│   └── config.exs                  # Environment config\n├── .formatter.exs\n├── .gitignore\n├── LICENSE\n├── mix.exs\n├── README.md\n└── .github/workflows/ci.yml        # CI pipeline (build + test + coverage)\n```\n\n## 🛡 Telemetry Events\n\nThe following events are emitted for observability and monitoring:\n\n- `[:search_cache, :fetch]`\n  - **Measurement**: `%{hit: true | false}`\n  - **Metadata**: `%{query: string}`\n  - Emitted every time a `fetch/2` call is made, indicating whether the key was found.\n\n- `[:search_cache, :cache]`\n  - **Measurement**: `%{size: integer}`\n  - **Metadata**: `%{query: string}`\n  - Emitted whenever an entry is cached, with current total entries.\n\nTo consume these events, use `:telemetry.attach/4` or `:telemetry.attach_many/4` like so:\n\n```elixir\n:telemetry.attach_many(\"logger\", [\n  [:search_cache, :fetch],\n  [:search_cache, :cache]\n], fn event, meas, meta, _config -\u003e\n  IO.inspect({event, meas, meta}, label: \"[Telemetry]\")\nend, nil)\n```\n\nThis allows you to log metrics, report to a dashboard (e.g., Prometheus or AppSignal), or trigger custom alerts.\n\n## 🧠 Use Cases\n\nThis module can be adapted for:\n\n- Caching expensive search or API results\n- In-memory rate limiting or throttling\n- Memoization of function output\n- Request deduplication / response coalescing\n- Lightweight state management for prototyping\n\n## 📖 Learn More\n\n- [Elixir GenServer Documentation](https://hexdocs.pm/elixir/GenServer.html)\n- [Telemetry in Elixir](https://hexdocs.pm/telemetry/Telemetry.html)\n- [Elixir Registry](https://hexdocs.pm/elixir/Registry.html)\n- [ExCoveralls Docs](https://github.com/parroty/excoveralls)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdreamingechoes%2Fsearch_cache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdreamingechoes%2Fsearch_cache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdreamingechoes%2Fsearch_cache/lists"}