Skip to content

Latest commit

 

History

History
1910 lines (1415 loc) · 53.4 KB

File metadata and controls

1910 lines (1415 loc) · 53.4 KB

Local Scope First (nebulex_disk_lfu)

This repository is nebulex_disk_lfu (a persistent disk-based cache adapter with LFU eviction for Nebulex), not Nebulex core. When imported Nebulex sections reference missing usage-rules/*.md paths or Nebulex-core files, treat them as upstream guidance and prioritize this repository's local files and modules.

Local Rule Precedence (for this repo)

  1. This local preface.
  2. nebulex:workflow section in this file.
  3. nebulex:nebulex section in this file (as framework guidance).
  4. nebulex:elixir-style and nebulex:elixir sections in this file.

Local Key Files

  • lib/nebulex/adapters/disk_lfu.ex - Main disk LFU adapter implementation.
  • lib/nebulex/adapters/disk_lfu/options.ex - Adapter option definitions/docs.
  • lib/nebulex/adapters/disk_lfu/store.ex - Disk storage engine.
  • lib/nebulex/adapters/disk_lfu/meta.ex - Metadata management.
  • lib/nebulex/adapters/disk_lfu/helpers.ex - Shared adapter utilities.
  • lib/nebulex/adapters/disk_lfu/transaction/options.ex - Transaction option definitions.
  • test/nebulex/adapters/disk_lfu_test.exs - Adapter behavior tests.
  • test/nebulex/adapters/disk_lfu_eviction_test.exs - LFU eviction tests.
  • README.md - Public usage/configuration for this adapter.
  • CHANGELOG.md - Adapter release history.

nebulex:workflow usage

Agent Workflow

Rule Index

Start here, then read these at session start and refer back while coding:

  • usage-rules/nebulex.md - Nebulex-specific rules
  • usage-rules/elixir-style.md - Style guidelines
  • usage-rules/elixir.md - Core Elixir rules

If these files are not found, check AGENTS.md or the local usage-rules/ folder instead.

Rule Precedence

When rules conflict, prioritize them in this order:

  1. usage-rules/workflow.md
  2. usage-rules/nebulex.md
  3. usage-rules/elixir-style.md
  4. usage-rules/elixir.md

If these files are not found, apply the same precedence to the corresponding sections in AGENTS.md.

Session Bootstrap

At the start of each session, quickly establish context:

  1. Run git status --short and git diff --name-only to check local modifications and currently touched files.
  2. Run git log --oneline -20 to see recent changes.
  3. Run git branch -a to see active branches and current branch.
  4. Read README.md and the latest section of CHANGELOG.md.
  5. Check .tool-versions or the elixir version in mix.exs for supported Elixir/OTP versions.

If on a feature branch, also run:

  1. git log --oneline main..HEAD to see the branch's commits.
  2. git diff main...HEAD to understand the branch's full scope.

When relevant to the task:

  1. Check open issues and PRs with gh issue list and gh pr list. If gh is unavailable or unauthenticated, skip this step.

Current Project Status

  • Latest release: check the latest section in CHANGELOG.md.
  • Read CHANGELOG.md for recent features, breaking changes, and the project's direction.
  • Changelog policy: user-visible behavior changes should be documented; internal refactors may be omitted before a release.

PR Workflow

Reviewing PRs

  1. Read the PR description and all comments: gh pr view <number> and gh pr view <number> --comments.
  2. Review the diff: gh pr diff <number>.
  3. Check CHANGELOG.md to understand if the change aligns with the project's direction.
  4. Verify code follows usage-rules/ conventions (Elixir patterns, Nebulex-specific rules, style guidelines).
  5. Run the validation commands (see below) before approving.
  6. Provide constructive feedback referencing specific lines and conventions.
  7. Structure review feedback as:
    • findings first (ordered by severity, with file:line references),
    • open questions/assumptions,
    • brief summary last.

Opening PRs

  1. Branch from main with a descriptive branch name (e.g., fix/some-bug, feat/cache-warming-support).
  2. Update CHANGELOG.md under the appropriate section (Enhancements, Bug fixes, Backward-incompatible changes).
  3. Run all validation commands before pushing.
  4. Reference related GitHub issues in the PR description (e.g., "Closes #123").
  5. Use gh pr create with a clear title and description.

Commit Messages

Commit messages must follow the Conventional Commits format:

type(scope): short summary

Allowed Types

  • feat
  • fix
  • refactor
  • docs
  • test
  • chore
  • perf
  • ci
  • build

Rules

  1. Use imperative mood in the summary.
  2. Keep the summary lowercase and do not end it with a period.
  3. Use a scope when it adds clarity (e.g., cache, decorators, telemetry, workflow).
  4. Keep the first line concise (ideally <= 72 chars).

Examples

  • feat(cache): add runtime option validation for ttl
  • fix(decorators): handle nested context pop safely
  • chore(workflow): refine session bootstrap steps

Validation Commands

Before submitting or approving any code change, run:

# Quick targeted validation (recommended first)
mix test path/to/changed_test.exs

# Format check
mix format --check-formatted

# Static analysis
mix credo --strict

# Documentation (if docs were changed)
mix docs

Then run full-suite validation before merge/release:

mix test.ci

nebulex:nebulex usage

Nebulex Project-Specific Usage Rules

Project Overview

Nebulex is a fast, flexible, and extensible caching library for Elixir that provides:

  • Multiple cache adapters (local, distributed, multilevel, partitioned, coherent).
  • Declarative decorator-based caching inspired by Spring Cache Abstraction.
  • OTP design patterns and fault tolerance.
  • Telemetry instrumentation.
  • Event streaming via Nebulex.Streams for distributed invalidation.
  • Support for TTL, eviction policies, transactions, and more.

Key Files

Path Purpose
lib/nebulex/cache.ex Main Cache API
lib/nebulex/cache/ Cache feature modules (KV, Options, etc.)
lib/nebulex/adapter.ex Adapter behaviour and macros
lib/nebulex/adapters/ Built-in adapter modules in core (e.g., Nil, common helpers)
lib/nebulex/caching/decorators.ex Decorator implementation
lib/nebulex/caching/ Caching internals (Context, Runtime)
lib/nebulex/event.ex Cache event types
lib/nebulex/telemetry.ex Telemetry instrumentation
lib/nebulex/utils.ex Shared utilities
mix.exs Dependencies and project config
CHANGELOG.md Release history and breaking changes
test/ Test suite (mirrors lib/ structure)
guides/ User-facing guides, behavioral references, and examples
guides/introduction/ Getting started, available adapters
guides/learning/ Declarative caching, cache patterns, adapter creation, info API
guides/upgrading/v3.0.md v3 migration guide

Package Structure (v3)

Nebulex v3 separates adapters into dedicated packages:

  • nebulex - Core.
  • nebulex_local - Local cache adapter.
  • nebulex_distributed - Partitioned, multilevel, and coherent adapters.
  • nebulex_redis_adapter - Adapter for Redis (including Redis Cluster).
  • nebulex_adapters_cachex - Adapter for cachex library.
  • nebulex_disk_lfu - Persistent disk-based cache adapter with LFU eviction for Nebulex.

See guides/introduction/nbx-adapters.md for more information about the available adapters.

Add the required dependencies to your mix.exs:

defp deps do
  [
    {:nebulex, "~> 3.0"},
    {:nebulex_local, "~> 3.0"},
    # For distributed caching
    {:nebulex_distributed, "~> 3.0"},
    # Required for caching decorators
    {:decorator, "~> 1.4"},
    # Optional but highly recommended for observability
    {:telemetry, "~> 1.0"}
  ]
end

Note: The :telemetry dependency is optional but highly recommended for production environments. It enables cache metrics, monitoring, and integration with observability tools.

Architecture Patterns

Cache Definition

  • Caches MUST be defined using use Nebulex.Cache with :otp_app and :adapter options.
  • Caches should be started in the application supervision tree, not manually.
  • Use descriptive cache module names that indicate their purpose (e.g., MyApp.LocalCache, MyApp.UserCache).
  • Use adapter_opts for compile-time adapter options like :primary_storage_adapter.

Example:

defmodule MyApp.Cache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Local
end

With compile-time adapter options (Partitioned/Coherent adapters):

defmodule MyApp.PartitionedCache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Partitioned,
    adapter_opts: [primary_storage_adapter: Nebulex.Adapters.Local]
end

Avoid putting adapter options at the top level:

# WRONG - will raise an error
defmodule MyApp.PartitionedCache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Partitioned,
    primary_storage_adapter: Nebulex.Adapters.Local
end

Cache Topologies

Nebulex supports multiple cache topologies:

  • Local (Nebulex.Adapters.Local): Single-node generational cache with automatic garbage collection.
  • Partitioned (Nebulex.Adapters.Partitioned): Distributed cache with data sharded across cluster nodes using consistent hashing.
  • Multilevel (Nebulex.Adapters.Multilevel): Hierarchical cache with multiple levels (e.g., L1 local + L2 distributed).
  • Coherent (Nebulex.Adapters.Coherent): Local cache with distributed invalidation via Nebulex.Streams. Ideal for read-heavy workloads.

Multilevel Cache Configuration

Use inclusion_policy (not the deprecated model) for multilevel caches:

config :my_app, MyApp.NearCache,
  inclusion_policy: :inclusive,
  levels: [
    {MyApp.NearCache.L1, gc_interval: :timer.hours(12)},
    {MyApp.NearCache.L2, primary: [gc_interval: :timer.hours(12)]}
  ]

Adapter Pattern

  • All adapters MUST implement the Nebulex.Adapter behaviour.
  • Adapters MUST implement c:init/1 returning {:ok, child_spec, adapter_meta}.
  • Adapter functions MUST return {:ok, value} or {:error, reason} tuples.
  • Use wrap_error/2 from Nebulex.Utils to wrap errors consistently.
  • Implement optional behaviours as needed: Nebulex.Adapter.KV, Nebulex.Adapter.Queryable, etc.

Command Pattern

  • Use defcommand/2 macro from Nebulex.Adapter to build public command wrappers.
  • Use defcommandp/2 for private command wrappers.
  • Command functions automatically handle telemetry, metadata, and error wrapping.
  • The first parameter to commands should always be name (the cache name or PID).
  • The last parameter should always be opts (keyword list).

Example:

defcommand fetch(name, key, opts)
defcommandp do_put(name, key, value, on_write, ttl, keep_ttl?, opts), command: :put

Return Value Conventions

Tuple Returns

  • Read operations that can fail MUST return {:ok, value} or {:error, %Nebulex.KeyError{}} for missing keys.
  • Write operations MUST return {:ok, true} for success or {:ok, false} for conditional failures (e.g., put_new).
  • Delete operations MUST return :ok regardless of whether the key existed.
  • NEVER return bare :error atoms; always use {:error, reason} tuples.

Bang Functions

  • Provide bang versions (!) of functions that unwrap {:ok, value} or raise exceptions.
  • Bang functions MUST use unwrap_or_raise/1 from Nebulex.Utils.
  • Functions that return :ok should have bang versions that also return :ok.
  • Functions that return {:ok, boolean} should have bang versions that return the boolean.

Example:

def fetch!(name, key, opts) do
  unwrap_or_raise fetch(name, key, opts)
end

def put!(name, key, value, opts) do
  _ = unwrap_or_raise do_put(name, key, value, :put, opts)
  :ok
end

Fetch-or-Store Pattern

Use fetch_or_store/3 and get_or_store/3 for read-through caching:

# Returns {:ok, value} or {:error, reason}
cache.fetch_or_store("user:123", fn ->
  case Repo.get(User, 123) do
    nil -> {:error, :not_found}
    user -> {:ok, user}
  end
end)

# Returns value directly (raises on error)
cache.get_or_store!("user:123", fn ->
  Repo.get!(User, 123)
end)
  • fetch_or_store/3 - Returns {:ok, value} from cache or fallback function. If the fallback returns {:error, reason}, it propagates the error without caching.
  • get_or_store/3 - Simpler variant that stores the direct return value from the fallback function.

Options and Validation

Options Handling

  • Use Nebulex.Cache.Options module for option validation.
  • Call Options.validate_runtime_shared_opts!/1 to validate runtime options.
  • Use Options.pop_and_validate_timeout!/2 for TTL and timeout options.
  • Use Options.pop_and_validate_boolean!/2 for boolean options.
  • Use Options.pop_and_validate_integer!/2 for integer options.
  • Validate options as early as possible, preferably at the beginning of the function.

Example:

def put(name, key, value, opts) do
  {ttl, opts} = Options.pop_and_validate_timeout!(opts, :ttl)
  {keep_ttl?, opts} = Options.pop_and_validate_boolean!(opts, :keep_ttl, false)

  do_put(name, key, value, :put, ttl, keep_ttl?, opts)
end

Shared Options

  • All cache functions should accept :telemetry, :telemetry_event, and :telemetry_metadata options.
  • Document adapter-specific options clearly in the module documentation.

Decorators

Decorator Usage

  • Use use Nebulex.Caching to enable decorator support in a module.
  • Configure default cache via use Nebulex.Caching, cache: MyCache.
  • Always use decorators on functions, not on function heads with multiple clauses.
  • Prefer module captures over anonymous functions for better performance: match: &__MODULE__.match_fun/1.
  • Avoid capturing large data structures in decorator lambdas.

Invalid:

@decorate cacheable(key: id)
def get_user(nil), do: nil

def get_user(id) do
  # logic
end

Valid:

@decorate cacheable(key: id)
def get_user(id) do
  do_get_user(id)
end

defp do_get_user(nil), do: nil
defp do_get_user(id) do
  # logic
end

Decorator Options

  • Use :key option to specify explicit cache keys; avoid relying solely on default key generation.
  • Use :references for implementing cache key references and memory-efficient caching.
  • Use :match option to conditionally cache values (e.g., match: &match_fun/1).
  • Use :on_error option to control error handling (:raise or :nothing).
  • Specify TTL via :opts option: opts: [ttl: :timer.hours(1)].
  • Use :transaction option to wrap cache operations in a transaction, preventing race conditions and cache stampede.

Transaction example (prevents concurrent updates to the same key):

@decorate cacheable(key: id, transaction: true)
def get_user(id) do
  Repo.get(User, id)
end

cacheable Decorator

  • Use @decorate cacheable for read-through caching patterns.
  • Combine with :references option when the same value needs multiple cache keys.
  • Use :match function with references to ensure consistency (e.g., validating email matches).

Example:

@decorate cacheable(key: id)
def get_user(id) do
  Repo.get(User, id)
end

@decorate cacheable(key: email, references: &(&1 && &1.id), match: &match_email(&1, email))
def get_user_by_email(email) do
  Repo.get_by(User, email: email)
end

defp match_email(%{email: email}, email), do: true
defp match_email(_, _), do: false

cache_put Decorator

  • Use @decorate cache_put for write-through caching patterns.
  • Always use :match option to conditionally update cache (e.g., only on {:ok, value}).
  • Avoid using cache_put and cacheable on the same function.

Example:

@decorate cache_put(key: user.id, match: &match_ok/1)
def update_user(user, attrs) do
  user
  |> User.changeset(attrs)
  |> Repo.update()
end

defp match_ok({:ok, user}), do: {true, user}
defp match_ok({:error, _}), do: false

cache_evict Decorator

  • Use @decorate cache_evict for cache invalidation.
  • Use key: {:in, keys} to evict multiple keys at once.
  • Use :all_entries option to clear the entire cache.
  • Use :before_invocation option to evict before function execution.
  • Use :query option for complex eviction patterns based on match specifications.

Example:

@decorate cache_evict(key: {:in, [user.id, user.email]})
def delete_user(user) do
  Repo.delete(user)
end

@decorate cache_evict(all_entries: true)
def clear_all_users do
  Repo.delete_all(User)
end

@decorate cache_evict(query: &__MODULE__.query_for_tag/1)
def delete_by_tag(tag) do
  # Delete logic
end

def query_for_tag(%{args: [tag]}) do
  [{:entry, :"$1", %{tag: :"$2"}, :_, :_}, [{:"=:=", :"$2", tag}], [true]]
end

Nebulex.Streams

Nebulex.Streams provides event streaming for cache operations, enabling distributed cache invalidation patterns.

Enabling Streams

Add use Nebulex.Streams to your cache module:

defmodule MyApp.Cache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Local

  use Nebulex.Streams
end

Stream Handlers

Implement custom stream handlers by using Nebulex.Streams.Handler:

defmodule MyApp.CacheEventHandler do
  use Nebulex.Streams.Handler

  @impl true
  def handle_event(event, state) do
    # Handle cache events (put, delete, etc.)
    {:cont, state}
  end
end

Coherent Cache Pattern

The Nebulex.Adapters.Coherent adapter uses Nebulex.Streams to provide local caching with distributed invalidation:

  • Each node maintains its own local cache.
  • Write operations trigger invalidation events via Phoenix.PubSub.
  • Other nodes delete the invalidated keys from their local caches.
  • Next read on other nodes results in a cache miss, fetching fresh data.
defmodule MyApp.CoherentCache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Coherent,
    adapter_opts: [primary_storage_adapter: Nebulex.Adapters.Local]
end

Configuration:

config :my_app, MyApp.CoherentCache,
  primary: [
    gc_interval: :timer.hours(12),
    max_size: 1_000_000
  ]

Testing Patterns

Test Structure

  • Use deftests do macro for shared test suites that can run across multiple adapters.
  • Structure tests with describe blocks grouping related functionality.
  • Use context fixtures with %{cache: cache} for test setup.
  • Test both successful and error scenarios for each function.

Example:

defmodule MyAdapterTest do
  import Nebulex.CacheCase

  deftests do
    describe "put/3" do
      test "puts the given entry into the cache", %{cache: cache} do
        assert cache.put(:key, :value) == :ok
        assert cache.fetch!(:key) == :value
      end

      test "raises when invalid option is given", %{cache: cache} do
        assert_raise NimbleOptions.ValidationError, fn ->
          cache.put(:key, :value, ttl: "invalid")
        end
      end
    end
  end
end

Test Assertions

  • Use assert cache.function() == expected_value for exact equality.
  • Use assert_raise ErrorType, ~r"message pattern" for exception testing.
  • Test edge cases: nil, boolean values (true, false), empty collections.
  • Test both normal and bang (!) versions of functions.
  • Avoid pattern matching in assertions when the full value is known (use direct equality).

Invalid:

assert {:ok, value} = cache.fetch(:key)

Valid:

assert cache.fetch(:key) == {:ok, expected_value}

Telemetry

Telemetry Events

  • Emit telemetry events for all cache commands when :telemetry option is true.
  • Use :telemetry_prefix option to customize event names (defaults to [:cache_name, :cache]).
  • Provide comprehensive metadata: :adapter_meta, :command, :args, :result.
  • Support custom :telemetry_event and :telemetry_metadata options per command.

Telemetry Best Practices

  • Use Nebulex.Telemetry.span/3 for span events (start, stop, exception).
  • Include measurements like :duration and :system_time.
  • Document all telemetry events in module documentation with measurement and metadata keys.
  • Provide example telemetry handlers in documentation.

Error Handling

Error Types

  • Use Nebulex.Error for general cache errors.
  • Use Nebulex.KeyError for missing key errors.
  • Use Nebulex.CacheNotFoundError for dynamic cache lookup failures.
  • Wrap adapter-specific errors using wrap_error/2 from Nebulex.Utils.

Error Wrapping

  • Adapter functions should wrap errors consistently using wrap_error/2.
  • Include relevant context in error metadata (:key, :command, :reason).
  • Preserve original error information in the :reason field.

Example:

def fetch(_adapter_meta, key, opts) do
  case do_fetch(key) do
    {:ok, value} -> {:ok, value}
    {:error, :not_found} -> wrap_error Nebulex.KeyError, key: key
    {:error, reason} -> wrap_error Nebulex.Error, reason: reason, command: :fetch, key: key
  end
end

Performance Considerations

Key Generation

  • Provide explicit keys in decorators when possible; avoid relying on default key generation.
  • For complex keys, use module captures: key: &MyModule.generate_key/1.
  • Keep captured data in decorator lambdas small; fetch large configs inside functions.

Reference Keys

  • Use cache key references (:references option) to avoid storing duplicate values.
  • Store references in a local cache and values in a remote cache (e.g., Redis) for optimization.
  • Set TTL for references to prevent dangling keys.
  • Use external references with keyref(key, cache: AnotherCache) for cross-cache references.

Optimization

  • Use Stream for large result sets instead of loading all data at once.
  • Leverage Task.async_stream/3 for concurrent cache operations when appropriate.
  • Set appropriate TTL values to balance freshness and performance.
  • Use put_all/2 for batch operations instead of multiple put/3 calls.

Documentation Standards

Module Documentation

  • Start with a clear @moduledoc explaining the purpose and main features, except modules that use NimbleOptions for option documentation.
  • Options documented using NimbleOptions should provide functions to insert that documentation into the module docs. Therefore, it is not required to document an option in the moduledoc or in the function @doc if it is already inserted using NimbleOptions. For example, #{Nebulex.Cache.Options.start_link_options_docs()}.
  • Include usage examples in module documentation.
  • Document all compile-time options.
  • Document all runtime shared options.
  • Provide telemetry event documentation with measurements and metadata.
  • The maximum text length is 80 characters, and you should aim to adhere to this limit. However, there are special cases where exceeding it is acceptable. For example, you may exceed the limit for a link (e.g., my link) or a code snippet that only exceeds the limit by a few characters (e.g., 1 or 2). If a code snippet exceeds the 80-character limit by more than 1 or 2 characters, format it using the Elixir formatter.
  • When you make a change to the documentation, use mix docs to validate it.

Function Documentation

  • Use @doc for all public functions.
  • Include @typedoc for all custom types.
  • Provide examples in function documentation using doctests when applicable.
  • Document all options with descriptions and default values.
  • Group related functions using @doc group: "Group Name".
  • Follow the same 80-character line-length guidance described in "Module Documentation," including the same exceptions for links and short formatter-friendly code snippets.
  • When you make a change to the documentation, use mix docs to validate it.

Code Comments

  • Avoid obvious comments; code should be self-explanatory.
  • Use comments for complex algorithms or non-obvious business logic. Use a single # for code comments. E.g., # My comment ....
  • For separating sections in a module, use ##. E.g., ## API, ## Private functions, etc.
  • Mark internal functions with @doc false or @moduledoc false.
  • Use # Inline common instructions followed by @compile inline: [function_name: arity].
  • The maximum text length is 80 characters; use multiple lines if the comment exceeds the limit.

Naming Conventions

Modules

  • Adapter modules: Nebulex.Adapters.* (e.g., Nebulex.Adapters.Local).
  • Cache modules: <App>.Cache or <App>.<Context>Cache (e.g., MyApp.Cache, MyApp.UserCache).
  • Behaviour modules: Nebulex.Adapter.<Feature> (e.g., Nebulex.Adapter.KV).

Functions

  • Use descriptive function names: fetch/2, put/3, delete/2, has_key?/2.
  • Bang versions: fetch!/2, put!/3, delete!/2.
  • Private helpers: prefix with do_ (e.g., do_fetch, do_put).
  • Predicate functions: suffix with ? (e.g., has_key?/2, expired?/2).

Variables

  • Cache instance: cache.
  • Adapter metadata: adapter_meta.
  • Options: opts.
  • Keys: key or keys.
  • Values: value or values.
  • TTL: ttl.

Code Organization

File Structure

  • Main cache API: lib/nebulex/cache.ex.
  • Adapter behaviour: lib/nebulex/adapter.ex.
  • Adapter implementations: lib/nebulex/adapters/<adapter_name>.ex.
  • Cache features: lib/nebulex/cache/<feature>.ex.
  • Decorators: lib/nebulex/caching/decorators.ex.
  • Mix tasks: lib/mix/tasks/<task_name>.ex.

Module Grouping

  • Keep related functionality together (e.g., all KV operations in Nebulex.Cache.KV).
  • Use nested modules for options, helpers, and internal implementation details.
  • Separate public API from internal implementation.

Mix Tasks

Nebulex provides mix nbx.gen.cache to generate a cache module:

mix nbx.gen.cache -c MyApp.Cache

This generates a cache using Nebulex.Adapters.Local by default. For other adapters (Partitioned, Multilevel, Coherent), manually update the generated module:

# Generated (Local adapter by default)
defmodule MyApp.Cache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Local
end

# Manually update for Partitioned
defmodule MyApp.PartitionedCache do
  use Nebulex.Cache,
    otp_app: :my_app,
    adapter: Nebulex.Adapters.Partitioned,
    adapter_opts: [primary_storage_adapter: Nebulex.Adapters.Local]
end

Common Pitfalls to Avoid

General

  • Do NOT use decorators on multi-clause functions without proper wrapper functions.
  • Do NOT forget to validate options at the beginning of functions.
  • Do NOT return inconsistent error types; always use tuples or raise exceptions via bang functions.
  • Do NOT capture large data structures in decorator lambdas.
  • Do NOT forget to handle nil, boolean, and edge case values in tests.
  • Do NOT use cache_put and cacheable decorators on the same function.
  • Do NOT forget to evict cache references when using :references option; use TTL or explicit eviction.
  • Do NOT implement adapter callbacks without proper error wrapping.

v3-Specific

  • Do NOT use primary_storage_adapter at the top level; wrap it in adapter_opts: [primary_storage_adapter: ...].
  • Do NOT use the deprecated model option for multilevel caches; use inclusion_policy instead.
  • Do NOT use mix nbx.gen.cache.partitioned or mix nbx.gen.cache.multilevel (they don't exist); use mix nbx.gen.cache and manually configure the adapter.
  • Do NOT use the old all/2 callback; use get_all/2 with query spec instead.
  • Do NOT use :keys option in decorators; use key: {:in, keys} instead.
  • Do NOT use :key_generator option; use key: &MyModule.generate_key/1.
  • Do NOT skip telemetry support in adapter implementations.
  • Do NOT use pattern matching in test assertions when the full value is known.

Backward Compatibility

  • Maintain backward compatibility when adding new options (use default values).
  • Deprecate old APIs before removal; provide migration path in documentation.
  • Follow semantic versioning strictly: major version for breaking changes.
  • Test against multiple Elixir and OTP versions in CI.

Dependencies

  • Keep dependencies minimal and well-justified.
  • Prefer standard library solutions over external dependencies.
  • Use optional dependencies for non-core features.
  • Document all dependencies in README with their purpose.

nebulex:elixir-style usage

Elixir Style

Most of these guidelines are based on The Elixir Style Guide by Christopher Adams, licensed under CC-BY-3.0.

Formatting

Whitespace

  • Use blank lines between defs to break up a function into logical paragraphs. For example:

    def some_function(some_data) do
      some_data |> other_function() |> List.first()
    end
    
    def some_function do
      result
    end
    
    def some_other_function do
      another_result
    end
    
    def a_longer_function do
      one
      two
    
      three
      four
    end
  • If the function head and do: clause are too long to fit on the same line, put do: on a new line, indented one level more than the previous line. For example:

    def some_function([:foo, :bar, :baz] = args),
      do: Enum.map(args, fn arg -> arg <> " is on a very long line!" end)

    When the do: clause starts on its own line, treat it as a multiline function by separating it with blank lines.

    # not preferred
    def some_function([]), do: :empty
    def some_function(_),
      do: :very_long_line_here
    
    # preferred
    def some_function([]), do: :empty
    
    def some_function(_),
      do: :very_long_line_here
  • Add a blank line after a multiline assignment as a visual cue that the assignment is 'over'. For example:

    # not preferred
    some_string =
      "Hello"
      |> String.downcase()
      |> String.trim()
    another_string <> some_string
    
    # preferred
    some_string =
      "Hello"
      |> String.downcase()
      |> String.trim()
    
    another_string <> some_string
    # also not preferred
    something =
      if x == 2 do
        "Hi"
      else
        "Bye"
      end
    String.downcase(something)
    
    # preferred
    something =
      if x == 2 do
        "Hi"
      else
        "Bye"
      end
    
    String.downcase(something)

Parentheses

  • Use parentheses when defining a type.

    # not preferred
    @type name :: atom
    
    # preferred
    @type name() :: atom

General guidelines

The rules in this section may not be applied by the code formatter, but they are generally preferred practice.

Expressions

  • Keep single-line def clauses of the same function together, but separate multiline defs with a blank line. For example:

    def some_function(nil), do: {:error, "No Value"}
    def some_function([]), do: :ok
    
    def some_function([first | rest]) do
      some_function(rest)
    end
  • If you have more than one multiline def, do not use single-line defs. For example:

    def some_function(nil) do
      {:error, "No Value"}
    end
    
    def some_function([]) do
      :ok
    end
    
    def some_function([first | rest]) do
      some_function(rest)
    end
    
    def some_function([first | rest], opts) do
      some_function(rest, opts)
    end
  • Use the pipe operator to chain functions together. For example:

    # not preferred
    String.trim(String.downcase(some_string))
    
    # preferred
    some_string |> String.downcase() |> String.trim()
    
    # Multiline pipelines are not further indented
    some_string
    |> String.downcase()
    |> String.trim()
    
    # Multiline pipelines on the right side of a pattern match
    # should be indented on a new line
    sanitized_string =
      some_string
      |> String.downcase()
      |> String.trim()
  • Avoid using the pipe operator just once, unless the first expression is a function. For example:

    # not preferred
    some_string |> String.downcase()
    
    # preferred
    String.downcase(some_string)
    
    # not preferred
    Version.parse(System.version())
    
    # preferred
    System.version() |> Version.parse()
  • Use parentheses when a def has arguments, and omit them when it doesn't. For example:

    # not preferred
    def some_function arg1, arg2 do
      # body omitted
    end
    
    def some_function() do
      # body omitted
    end
    
    # preferred
    def some_function(arg1, arg2) do
      # body omitted
    end
    
    def some_function do
      # body omitted
    end
  • Use do: for single-line if/unless statements.

    # preferred
    if some_condition, do: # some_stuff
    
  • Use true as the last condition of the cond special form when you need a clause that always matches.

    # not preferred
    cond do
      1 + 2 == 5 ->
        "Nope"
    
      1 + 3 == 5 ->
        "Uh, uh"
    
      :else ->
        "OK"
    end
    
    # preferred
    cond do
      1 + 2 == 5 ->
        "Nope"
    
      1 + 3 == 5 ->
        "Uh, uh"
    
      true ->
        "OK"
    end

Naming

  • Use snake_case for atoms, functions and variables.

    # not preferred
    :"some atom"
    :SomeAtom
    :someAtom
    
    someVar = 5
    
    def someFunction do
      ...
    end
    
    # preferred
    :some_atom
    
    some_var = 5
    
    def some_function do
      ...
    end
  • Use CamelCase for modules (keep acronyms like HTTP, RFC, XML uppercase).

    # not preferred
    defmodule Somemodule do
      ...
    end
    
    defmodule Some_Module do
      ...
    end
    
    defmodule SomeXml do
      ...
    end
    
    # preferred
    defmodule SomeModule do
      ...
    end
    
    defmodule SomeXML do
      ...
    end
  • Functions that return a boolean (true or false) should be named with a trailing question mark.

    def cool?(var) do
      String.contains?(var, "cool")
    end
  • Boolean checks that can be used in guard clauses (custom guards) should be named with an is_ prefix.

    defguard is_cool(var) when var == "cool"
    defguard is_very_cool(var) when var == "very cool"

Comments

  • Write expressive code and try to convey your program's intention through control-flow, structure and naming.

  • Comments longer than a word are capitalized, and sentences use punctuation. Use one space after periods.

# not preferred
# these lowercase comments are missing punctuation

# preferred
# Capitalization example
# Use punctuation for complete sentences.
  • Limit comment lines to 80 characters.

Comment Annotations

  • Annotations should usually be written on the line immediately above the relevant code.

  • The annotation keyword is uppercase, and is followed by a colon and a space, then a note describing the problem.

# TODO: Deprecate in v1.5.
def some_function(arg), do: {:ok, arg}
  • In cases where the problem is so obvious that any documentation would be redundant, annotations may be left with no note. This usage should be the exception and not the rule.
start_task()

# FIXME
Process.sleep(5000)
  • Use TODO to note missing features or functionality that should be added at a later date.

  • Use FIXME to note broken code that needs to be fixed.

  • Use OPTIMIZE to note slow or inefficient code that may cause performance problems.

  • Use HACK to note code smells where questionable coding practices were used and should be refactored away.

  • Use REVIEW to note anything that should be looked at to confirm it is working as intended. For example: REVIEW: Are we sure this is how the client does X currently?

  • Use other custom annotation keywords if it feels appropriate, but be sure to document them in your project's README or similar.

Comment Constants

  • When defining a constant, pick a descriptive name that reflects the intention or usage of the constant and add a comment with a short description.

Not preferred:

@retries 10

Preferred:

# Default HTTP retries
@http_retries 10
  • When the constant is a timeout in milliseconds, use :timer module instead of explicit value (e.g., :timer.seconds/1, :timer.minutes/1, :timer.hours/1).

Not preferred:

# Default HTTP request timeout in milliseconds
@http_request_timeout 10_000

Preferred:

# Default HTTP request timeout in milliseconds
@http_request_timeout :timer.seconds(10)
  • When the constant is a list of atoms or strings, a regex, or anything that can be expressed using Sigils, then use Sigils.

Not preferred:

# User types
@user_types [:admin, :editor, :customer]

# Supported country codes
@user_types ["US", "ES", "CO"]

Preferred:

# User types
@user_types ~w(admin editor customer)a

# Supported country codes
@user_types ~w(US ES CO)

Modules

  • List module attributes, directives, and macros in the following order:

    1. @moduledoc
    2. @behaviour
    3. use
    4. import
    5. require
    6. alias
    7. @module_attribute
    8. defstruct
    9. @type
    10. @callback
    11. @macrocallback
    12. @optional_callbacks
    13. defmacro, defmodule, defguard, def, etc.

    Add a blank line between each grouping, and sort the terms (like module names) alphabetically. Here's an overall example of how you should order things in your modules:

    defmodule MyModule do
      @moduledoc """
      An example module
      """
    
      @behaviour MyBehaviour
    
      use GenServer
    
      import Something
      import SomethingElse
    
      require Integer
    
      alias My.Long.Module.Name
      alias My.Other.Module.Example
    
      @module_attribute :foo
      @other_attribute 100
    
      defstruct [:name, params: []]
    
      @type params :: [{binary, binary}]
    
      @callback some_function(term) :: :ok | {:error, term}
    
      @macrocallback macro_name(term) :: Macro.t()
    
      @optional_callbacks macro_name: 1
    
      @doc false
      defmacro __using__(_opts), do: :no_op
    
      @doc """
      Determines when a term is `:ok`. Allowed in guards.
      """
      defguard is_ok(term) when term == :ok
    
      @impl true
      def init(state), do: {:ok, state}
    
      # Define other functions here.
    end
  • Use the __MODULE__ pseudo variable when a module refers to itself. This avoids having to update any self-references when the module name changes.

    defmodule SomeProject.SomeModule do
      defstruct [:name]
    
      def name(%__MODULE__{name: name}), do: name
    end

Typespecs

  • Place @typedoc and @type definitions together, and separate each pair with a blank line.

    defmodule SomeModule do
      @moduledoc false
    
      @typedoc "The name"
      @type name() :: atom()
    
      @typedoc "The result"
      @type result() :: {:ok, any()} | {:error, any()}
    
      ...
    end
  • Name the main type for a module t(), for example: the type specification for a struct.

    defstruct name: nil, params: []
    
    @typedoc "The type for ..."
    @type t() :: %__MODULE__{
            name: String.t() | nil,
            params: Keyword.t()
          }
  • Place specifications right before the function definition, after the @doc, without separating them by a blank line.

    @doc """
    Some function description.
    """
    @spec some_function(any()) :: result()
    def some_function(some_data) do
      {:ok, some_data}
    end

Structs

  • Use a list of atoms for struct fields that default to nil, followed by the other keywords.

    # not preferred
    defstruct name: nil, params: nil, active: true
    
    # preferred
    defstruct [:name, :params, active: true]
  • Omit square brackets when the argument of a defstruct is a keyword list.

    # not preferred
    defstruct [params: [], active: true]
    
    # preferred
    defstruct params: [], active: true
    
    # required - brackets are not optional, with at least one atom in the list
    defstruct [:name, params: [], active: true]
  • If a struct definition spans multiple lines, put each element on its own line, keeping the elements aligned.

    defstruct foo: "test",
              bar: true,
              baz: false,
              qux: false,
              quux: 1

    If a multiline struct requires brackets, format it as a multiline list:

    defstruct [
      :name,
      params: [],
      active: true
    ]

Exceptions

  • Make exception names end with a trailing Error.

    # not preferred
    defmodule BadHTTPCode do
      defexception [:message]
    end
    
    defmodule BadHTTPCodeException do
      defexception [:message]
    end
    
    # preferred
    defmodule BadHTTPCodeError do
      defexception [:message]
    end
  • Use lowercase error messages when raising exceptions, with no trailing punctuation.

    # not preferred
    raise ArgumentError, "This is not valid."
    
    # preferred
    raise ArgumentError, "this is not valid"

Collections

  • Always use the special syntax for keyword lists.

    # not preferred
    some_value = [{:a, "baz"}, {:b, "qux"}]
    
    # preferred
    some_value = [a: "baz", b: "qux"]
  • Use the shorthand key-value syntax for maps when all of the keys are atoms.

    # not preferred
    %{:a => 1, :b => 2, :c => 0}
    
    # preferred
    %{a: 1, b: 2, c: 3}
  • Use the verbose key-value syntax for maps if any key is not an atom.

    # not preferred
    %{"c" => 0, a: 1, b: 2}
    
    # preferred
    %{:a => 1, :b => 2, "c" => 0}

Testing

  • When writing ExUnit assertions, put the expression being tested to the left of the operator, and the expected result to the right, unless the assertion is a pattern match.

    # not preferred
    assert true == actual_function(1)
    
    # preferred
    assert actual_function(1) == true
    
    # required - the assertion is a pattern match, and the `expected` variable is used later
    assert {:ok, expected} = actual_function(3)
    assert expected.atom == :atom
    assert expected.int == 123
    
    # preferred - if the right side is known, even if it is a tuple
    assert actual_function(11) == {:ok, %{atom: :atom, int: 123}}
    
    # preferred - if the right side is known (using a variable)
    expected = %{atom: :atom, int: 123}
    assert actual_function(11) == {:ok, expected}

Extra guidelines

  • Use a blank line for the return or final statement (unless it is a single line).

    Avoid:

    def some_function(arg) do
      Logger.info("Arg: #{inspect(some_data)}")
      :ok
    end
    

    Prefer:

    def some_function(some_data) do
      Logger.info("Arg: #{inspect(some_data)}")
    
      :ok
    end
    
  • Use multi-line when a function returns with a pipe.

    Avoid:

    def some_function(some_data) do
      some_data |> other_function() |> List.first()
    end
    

    Prefer:

    def some_function(some_data) do
      some_data
      |> other_function()
      |> List.first()
    end
    
  • Use with when only one case has to be handled, either the success or the error.

    Avoid: case forwarding the same result

    case some_call() do
      :ok ->
        :ok
    
      {:error, reason} = error ->
        Logger.error("Error: #{inspect(reason)}")
    
        error
    end
    

    Prefer: with handling only the needed case

    with {:error, reason} = error <- some_call() do
      Logger.error("Error: #{inspect(reason)}")
    
      error
    end
    

nebulex:elixir usage

Elixir Core Usage Rules

Pattern Matching

  • Use pattern matching over conditional logic when possible
  • Prefer to match on function heads instead of using if/else or case in function bodies
  • %{} matches ANY map, not just empty maps. Use map_size(map) == 0 guard to check for truly empty maps

Error Handling

  • Use {:ok, result} and {:error, reason} tuples for operations that can fail
  • Avoid raising exceptions for control flow
  • Use with for chaining operations that return {:ok, _} or {:error, _}
  • Bang functions (!) that explicitly raise exceptions on failure are acceptable (e.g., File.read!/1, String.to_integer!/1)
  • Avoid rescuing exceptions unless for a very specific case (e.g., cleaning up resources, logging critical errors)

Common Mistakes to Avoid

  • Elixir has no return statement, nor early returns. The last expression in a block is always returned.
  • Don't use Enum functions on large collections when Stream is more appropriate
  • Avoid nested case statements - refactor to a single case, with or separate functions
  • Don't use String.to_atom/1 on user input (memory leak risk)
  • Lists and enumerables cannot be indexed with brackets. Use pattern matching or Enum functions
  • Prefer Enum functions like Enum.reduce over recursion
  • When recursion is necessary, prefer to use pattern matching in function heads for base case detection
  • Using the process dictionary is typically a sign of unidiomatic code
  • Only use macros if explicitly requested
  • There are many useful standard library functions, prefer to use them where possible

Function Design

  • Use guard clauses: when is_binary(name) and byte_size(name) > 0
  • Prefer multiple function clauses over complex conditional logic
  • Name functions descriptively: calculate_total_price/2 not calc/2
  • Predicate function names should not start with is and should end in a question mark.
  • Names like is_thing should be reserved for guards

Data Structures

  • Use structs over maps when the shape is known: defstruct [:name, :age]
  • Prefer keyword lists for options: [timeout: 5000, retries: 3]
  • Use maps for dynamic key-value data
  • Prefer to prepend to lists [new | list] not list ++ [new]

Mix Tasks

  • Use mix help to list available mix tasks
  • Use mix help task_name to get docs for an individual task
  • Read the docs and options fully before using tasks

Testing

  • Run tests in a specific file with mix test test/my_test.exs and a specific test with the line number mix test path/to/test.exs:123
  • Limit the number of failed tests with mix test --max-failures n
  • Use @tag to tag specific tests, and mix test --only tag to run only those tests
  • Use assert_raise for testing expected exceptions: assert_raise ArgumentError, fn -> invalid_function() end
  • Use mix help test for full documentation on running tests

Debugging

  • Use dbg/1 to print values while debugging. This will display the formatted value and other relevant information in the console.

Elixir guidelines

  • Elixir lists do not support index based access via the access syntax

    Never do this (invalid):

    i = 0
    mylist = ["blue", "green"]
    mylist[i]
    

    Instead, always use Enum.at, pattern matching, or List for index based list access, i.e.:

    i = 0
    mylist = ["blue", "green"]
    Enum.at(mylist, i)
    
  • Elixir variables are immutable, but can be rebound, so for block expressions like if, case, cond, etc you must bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, i.e.:

    # INVALID: we are rebinding inside the `if` and the result never gets assigned
    if connected?(socket) do
      socket = assign(socket, :val, val)
    end
    
    # VALID: we rebind the result of the `if` to a new variable
    socket =
      if connected?(socket) do
        assign(socket, :val, val)
      end
    
  • Never nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors

  • Never use map access syntax (changeset[:field]) on structs as they do not implement the Access behaviour by default. For regular structs, you must access the fields directly, such as my_struct.field or use higher level APIs that are available on the struct if they exist, Ecto.Changeset.get_field/2 for changesets

  • Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common Time, Date, DateTime, and Calendar interfaces by accessing their documentation as necessary. Never install additional dependencies unless asked or for date/time parsing (which you can use the date_time_parser package)

  • Don't use String.to_atom/1 on user input (memory leak risk)

  • Predicate function names should not start with is_ and should end in a question mark. Names like is_thing should be reserved for guards

  • Elixir's built-in OTP primitives, such as DynamicSupervisor and Registry, require names in the child spec, such as {DynamicSupervisor, name: MyApp.MyDynamicSup}, then you can use DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)

  • Use Task.async_stream(collection, callback, options) for concurrent enumeration with back-pressure. The majority of times you will want to pass timeout: :infinity as option

Mix guidelines

  • Read the docs and options before using tasks (by using mix help task_name)
  • To debug test failures, run tests in a specific file with mix test test/my_test.exs or run all previously failed tests with mix test --failed
  • mix deps.clean --all is almost never needed. Avoid using it unless you have good reason

Extra Elixir guidelines

  • The in operator in guards requires a compile-time known value on the right side (literal list or range)

    Never do this (invalid): using a variable which is unknown at compile time

    def t(x, y) when x in y, do: {x, y}
    

    This will raise ArgumentError: invalid right argument for operator "in", it expects a compile-time proper list or compile-time range on the right side when used in guard expressions

    Valid: use a known value for the list or range

    def t(x, y) when x in [1, 2, 3], do: {x, y}
    def t(x, y) when x in 1..10, do: {x, y}
    
  • In tests, avoid using assert with pattern matching when the expected value is fully known. Use direct equality comparison instead for clearer test failures

    Avoid:

    assert {:ok, ^value} = testing()
    assert {:error, :not_found} = fetch()
    

    Prefer:

    assert testing() == {:ok, value}
    assert fetch() == {:error, :not_found}
    

    Exception: Pattern matching is acceptable when you only want to assert part of a complex structure

    # OK: asserting only specific fields of a large struct/map
    assert {:ok, %{id: ^id}} = get_order()
    
  • In tests, avoid duplicating test data across multiple tests. Use constants, fixture files, or private fixture functions instead

    Avoid: Duplicating test data

    test "validates user email" do
      assert valid_email?("user@example.com")
    end
    
    test "creates user" do
      assert create_user("user@example.com")
    end
    

    Prefer: Use module attributes for constants or fixture functions

    @valid_email "user@example.com"
    
    test "validates user email" do
      assert valid_email?(@valid_email)
    end
    
    test "creates user" do
      assert create_user(@valid_email)
    end
    

    For complex data structures, create fixture functions:

    defp user_fixture(attrs \\ %{}) do
      %User{
        name: "John Doe",
        email: "john@example.com",
        age: 30
      }
      |> Map.merge(attrs)
    end
    

usage_rules usage

A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills from dependencies

Using Usage Rules

Many packages have usage rules, which you should thoroughly consult before taking any action. These usage rules contain guidelines and rules directly from the package authors. They are your best source of knowledge for making decisions.

Modules & functions in the current app and dependencies

When looking for docs for modules & functions that are dependencies of the current project, or for Elixir itself, use mix usage_rules.docs

# Search a whole module
mix usage_rules.docs Enum

# Search a specific function
mix usage_rules.docs Enum.zip

# Search a specific function & arity
mix usage_rules.docs Enum.zip/1

Searching Documentation

You should also consult the documentation of any tools you are using, early and often. The best way to accomplish this is to use the usage_rules.search_docs mix task. Once you have found what you are looking for, use the links in the search results to get more detail. For example:

# Search docs for all packages in the current application, including Elixir
mix usage_rules.search_docs Enum.zip

# Search docs for specific packages
mix usage_rules.search_docs Req.get -p req

# Search docs for multi-word queries
mix usage_rules.search_docs "making requests" -p req

# Search only in titles (useful for finding specific functions/modules)
mix usage_rules.search_docs "Enum.zip" --query-by title

usage_rules:otp usage

OTP Usage Rules

GenServer Best Practices

  • Keep state simple and serializable
  • Handle all expected messages explicitly
  • Use handle_continue/2 for post-init work
  • Implement proper cleanup in terminate/2 when necessary

Process Communication

  • Use GenServer.call/3 for synchronous requests expecting replies
  • Use GenServer.cast/2 for fire-and-forget messages.
  • When in doubt, use call over cast, to ensure back-pressure
  • Set appropriate timeouts for call/3 operations

Fault Tolerance

  • Set up processes such that they can handle crashing and being restarted by supervisors
  • Use :max_restarts and :max_seconds to prevent restart loops

Task and Async

  • Use Task.Supervisor for better fault tolerance
  • Handle task failures with Task.yield/2 or Task.shutdown/2
  • Set appropriate task timeouts
  • Use Task.async_stream/3 for concurrent enumeration with back-pressure