Nebulex.Adapters.Local (nebulex_local v3.0.0-rc.2)

View Source

A Local Generation Cache adapter for Nebulex; inspired by epocxy cache.

Generational caching using an ETS table (or multiple ones when used with :shards) for each generation of cached data. Accesses hit the newer generation first, and migrate from the older generation to the newer generation when retrieved from the stale table. When a new generation is started, the oldest one is deleted. This is a form of mass garbage collection which avoids using timers and expiration of individual cached elements.

This implementation of generation cache uses only two generations, referred to as the new and the old generation.

See Nebulex.Adapters.Local.Generation to learn more about generation management and garbage collection.

Overall features

  • Configurable backend (ets or :shards).
  • Expiration - A status based on TTL (Time To Live) option. To maintain cache performance, expired entries may not be immediately removed or evicted, they are expired or evicted on-demand, when the key is read.
  • Eviction - Generational Garbage Collection (see Nebulex.Adapters.Local.Generation).
  • Sharding - For intensive workloads, the Cache may also be partitioned (by using :shards backend and specifying the :partitions option).
  • Support for transactions via Erlang global name registration facility. See Nebulex.Adapter.Transaction.
  • Support for stats.
  • Automatic retry logic for handling race conditions during garbage collection (see Concurrency and resilience).

Concurrency and resilience

The local adapter implements automatic retry logic to handle race conditions that may occur when accessing ETS tables during garbage collection cycles. When the garbage collector deletes an old generation, processes holding references to that generation's ETS table may encounter ArgumentError exceptions when attempting to access it.

To ensure resilience and prevent crashes, all cache operations automatically retry up to 3 times when encountering such errors. The retry mechanism:

  • Catches ArgumentError exceptions that occur due to deleted ETS tables.
  • Re-fetches fresh generation references from the metadata table.
  • Retries the operation with the updated references.
  • Prevents infinite loops by limiting retries to a maximum of 3 attempts.
  • Adds a small delay (10ms) between retries to allow GC operations to complete.

This behavior is transparent to users and ensures that:

  • Operations remain reliable even under high concurrency.
  • Cache operations succeed despite concurrent generation changes.
  • No manual error handling is required for GC-related race conditions.
  • The system gracefully handles generation transitions.

The role of :gc_cleanup_delay

It's important to note that while the automatic retry logic provides an extra layer of safety, race conditions are unlikely to occur in practice due to the :gc_cleanup_delay configuration option (defaults to 10 seconds).

When garbage collection runs and creates a new generation, the old generation is not deleted immediately. Instead, it's kept alive for the duration specified by :gc_cleanup_delay. This grace period allows ongoing operations that hold references to the old generation to complete successfully before the table is actually deleted.

The automatic retry mechanism serves as a safeguard for edge cases where:

  • Operations take longer than the cleanup delay.
  • Extremely high concurrency scenarios.
  • Systems under heavy load.

Recommendation: Configure :gc_cleanup_delay with a reasonable timeout (the default 10 seconds is appropriate for most use cases). This ensures concurrent operations have sufficient time to transition gracefully, making the retry mechanism rarely necessary.

Example scenario

Consider this race condition scenario:

  1. Process A fetches generation references and starts a fetch/2 operation.
  2. Process B (GC) deletes the old generation while Process A is accessing it.
  3. Process A encounters an ArgumentError when trying to access the deleted table.
  4. Automatic retry catches the error, fetches fresh generation references, and retries.
  5. Operation succeeds using the updated generation references.

This automatic retry logic applies to ALL cache operations.

Configuration options

The following options can be used to configure the adapter:

  • :cache (atom/0) - Required. The defined cache module.

  • :stats (boolean/0) - A flag to determine whether to collect cache stats. The default value is true.

  • :backend (backend/0) - The backend or storage to be used for the adapter. The default value is :ets.

  • :read_concurrency (boolean/0) - Since the adapter uses ETS tables internally, this option is when creating a new table or generation. See :ets.new/2 options. The default value is true.

  • :write_concurrency (boolean/0) - Since the adapter uses ETS tables internally, this option is when creating a new table or generation. See :ets.new/2 options. The default value is true.

  • :compressed (boolean/0) - Since the adapter uses ETS tables internally, this option is when creating a new table or generation. See :ets.new/2 options. The default value is false.

  • :backend_type - Since the adapter uses ETS tables internally, this option is when creating a new table or generation. See :ets.new/2 options. The default value is :set.

  • :partitions (pos_integer/0) - The number of ETS partitions when using the :shards backend. See :shards.new/2.

    The default value is System.schedulers_online().

  • :purge_chunk_size (pos_integer/0) - This option limits the max nested match specs based on the number of keys when purging the older cache generation. The default value is 100.

  • :gc_interval (pos_integer/0) - The interval time in milliseconds for garbage collection to run, create a new generation, make it the newer one, make the previous new generation the old one, and finally remove the previous old one. If not provided (or nil), the garbage collection never runs, so new generations must be created explicitly, e.g., MyCache.new_generation(opts) (the default); however, the adapter does not recommend this.

    Usage

    Always provide the :gc_interval option so the garbage collector can work appropriately out of the box. Unless you explicitly want to turn off the garbage collection or handle it yourself.

  • :max_size (pos_integer/0) - The maximum number of entries to store in the cache. If not provided (or nil), the health check to validate and release memory is not performed (the default).

  • :allocated_memory (pos_integer/0) - The maximum size in bytes for the cache storage. If not provided (or nil), the health check to validate and release memory is not performed (the default).

  • :gc_memory_check_interval (mem_check_interval/0) - The interval time in milliseconds for garbage collection to run the size and memory checks.

    Usage

    Beware: For the :gc_memory_check_interval option to work, you must configure one of :max_size or :allocated_memory (or both).

    The default value is 10000.

  • :gc_cleanup_delay (pos_integer/0) - The delay in milliseconds before the oldest generation is deleted. This grace period allows ongoing operations to complete before the generation is removed. The default value is 10000.

Usage

Nebulex.Cache is the wrapper around the cache. We can define a local cache as follows:

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

Where the configuration for the cache must be in your application environment, usually defined in your config/config.exs:

config :my_app, MyApp.LocalCache,
  gc_interval: :timer.hours(12),
  max_size: 1_000_000,
  allocated_memory: 2_000_000_000,
  gc_memory_check_interval: :timer.seconds(10)

For intensive workloads, the Cache may also be partitioned using :shards as cache backend (backend: :shards) and configuring the desired number of partitions via the :partitions option. Defaults to System.schedulers_online().

config :my_app, MyApp.LocalCache,
  backend: :shards,
  gc_interval: :timer.hours(12),
  max_size: 1_000_000,
  allocated_memory: 2_000_000_000,
  gc_memory_check_interval: :timer.seconds(10)
  partitions: System.schedulers_online() * 2

If your application was generated with a supervisor (by passing --sup to mix new) you will have a lib/my_app/application.ex file containing the application start callback that defines and starts your supervisor. You just need to edit the start/2 function to start the cache as a supervisor on your application's supervisor:

def start(_type, _args) do
  children = [
    {MyApp.LocalCache, []},
    ...
  ]

See Nebulex.Cache for more information.

The :ttl option

The :ttl is a runtime option meant to set a key's expiration time. It is evaluated on-demand when a key is retrieved, and if it has expired, it is removed from the cache. Hence, it can not be used as an eviction method; it is more for maintaining the cache's integrity and consistency. For this reason, you should always configure the eviction or GC options. See the "Eviction policy" section for more information.

Caveats when using :ttl option:

  • When using the :ttl option, ensure it is less than :gc_interval. Otherwise, the key may be evicted, and the :ttl hasn't happened yet because the garbage collector may run before a fetch operation has evaluated the :ttl and expired the key.
  • Consider the following scenario based on the previous caveat. You have :gc_interval set to 1 hrs. Then you put a new key with :ttl set to 2 hrs. One minute later, the GC runs, creating a new generation, and the key ends up in the older generation. Therefore, if the next GC cycle occurs (1 hr later) before the key is fetched (moving it to the newer generation), it is evicted from the cache when the GC removes the older generation so it won't be retrievable anymore.

Eviction policy

This adapter implements a generational cache, which means its primary eviction mechanism pushes a new cache generation and removes the oldest one. This mechanism ensures the garbage collector removes the least frequently used keys when it runs and deletes the oldest generation. At the same time, only the most frequently used keys are always available in the newer generation. In other words, the generation cache also enforces an LRU (Least Recently Used) eviction policy.

The following conditions trigger the garbage collector to run:

  • When the time interval defined by :gc_interval is completed. This makes the garbage-collector process to run creating a new generation and forcing to delete the oldest one. This interval defines how often you want to evict the least frequently used entries or the retention period for the cached entries. The retention period for the least frequently used entries is equivalent to two garbage collection cycles (since we keep two generations), which means the GC removes all entries not accessed in the cache during that time.

  • When the time interval defined by :gc_memory_check_interval is completed. Beware: This option works alongside the :max_size and :allocated_memory options. The interval defines when the GC must run to validate the cache size and memory and release space if any of the limits are exceeded. It is mainly for keeping the cached data under the configured memory size limits and avoiding running out of memory at some point.

Configuring the GC options

This section helps you understand how the different configuration options work and gives you an idea of what values to set, especially if this is your first time using Nebulex with the local adapter.

Understanding a few things in advance is essential to configure the cache with appropriate values. For example, the average size of an entry so we can configure a reasonable value for the max size or allocated memory. Also, the reads and writes load. The problem is that sometimes it is challenging to have this information in advance, especially when it is a new app or when we use the cache for the first time. The following are tips to help you to configure the cache (especially if it is your for the first time):

  • To configure the GC, consider the retention period for the least frequently used entries you desire. For example, if the GC is 1 hr, you will keep only those entries accessed periodically during the last 2 hrs (two GC cycles, as outlined above). If it is your first time using the local adapter, you may start configuring the :gc_interval to 12 hrs to ensure daily data retention. Then, you can analyze the data and change the value based on your findings.

  • Configure the :max_size or :allocated_memory option (or both) to keep memory healthy under the given limits (avoid running out of memory). Configuring these options will ensure the GC releases memory space whenever a limit is reached or exceeded. For example, one may assign 50% of the total memory to the :allocated_memory. It depends on how much memory you need and how much your app needs to run. For the :max_size, consider how many entries you expect to keep in the cache; you could start with something between 100_000 and 1_000_000.

  • Finally, when configuring :max_size or :allocated_memory (or both), you must also configure :gc_memory_check_interval (defaults to 10 sec). By default, the GC will run every 10 seconds to validate the cache size and memory.

Queryable API

Since the adapter implementation uses ETS tables underneath, the query must be a valid ETS Match Spec. However, there are some predefined or shorthand queries you can use. See the "Predefined queries" section for information.

The adapter defines an entry as a tuple {:entry, key, value, touched, ttl, tag}, meaning the match pattern within the ETS Match Spec must be like {:entry, :"$1", :"$2", :"$3", :"$4", :"$5"}. To make query building easier, you can use the Ex2ms library.

# Using raw ETS match spec
iex> match_spec = [
...>   {
...>     {:entry, :"$1", :"$2", :_, :_, :_},
...>     [{:>, :"$2", 1}],
...>     [{{:"$1", :"$2"}}]
...>   }
...> ]
iex> MyCache.get_all(query: match_spec)
{:ok, [{:b, 2}, {:c, 3}]}

# Using Ex2ms for easier query building
iex> import Ex2ms
iex> match_spec = fun do
...>   {_, key, value, _, _, _} when value > 1 -> {key, value}
...> end
iex> MyCache.get_all(query: match_spec)
{:ok, [{:b, 2}, {:c, 3}]}

You can use the Ex2ms or MatchSpec library to build queries easier.

Building Match Specs with QueryHelper

The Nebulex.Adapters.Local.QueryHelper module provides a user-friendly, SQL-like syntax for building ETS match specifications without needing to know the internal entry tuple structure. This is especially useful when working with the local adapter's queryable API.

Why use QueryHelper?

When building match specs manually, you need to know that entries are stored as {:entry, key, value, touched, exp, tag} tuples. QueryHelper abstracts this away, letting you work with named field bindings instead:

# Without QueryHelper - you need to know the exact tuple structure
import Ex2ms
match_spec = fun do
  {:entry, k, v, _, _, _} when k == :foo -> v
end

# With QueryHelper - clean, declarative syntax
use Nebulex.Adapters.Local.QueryHelper
match_spec = match_spec key: k, value: v, where: k == :foo, select: v

Getting started

Use Nebulex.Adapters.Local.QueryHelper in your module to enable the match_spec/1 macro and Ex2ms support:

defmodule MyCache.Queries do
  use Nebulex.Adapters.Local.QueryHelper

  def by_key(key) do
    match_spec key: k, value: v, where: k == key, select: v
  end

  def by_tag(tag) do
    match_spec tag: t, where: t == tag, select: true
  end

  def expensive_keys do
    match_spec key: k, value: v, where: is_integer(v) and v > 100, select: k
  end
end

Syntax

The match_spec/1 macro accepts a keyword list with:

  • Field bindings - :key, :value, :touched, :exp, :tag - Bind entry fields to variables. Fields not mentioned are automatically wildcarded.
  • :where - Optional guard clause with conditions (supports all ETS guard functions).
  • :select - Required return expression specifying what to return.

Examples

# Match all entries where value is greater than 10
match_spec value: v, where: v > 10, select: v

# Match entries with a specific tag
match_spec key: k, tag: t, where: t == :important, select: k

# Complex guards with multiple conditions
match_spec key: k, value: v, exp: e,
           where: is_integer(v) and e != :infinity,
           select: {k, v, e}

# Match without guards (all entries)
match_spec key: k, value: v, select: {k, v}

# Return entire entry
match_spec key: k, tag: t, where: t == :session, select: :"$_"

# Query only specific fields
match_spec tag: t, where: t == :cache_group, select: true

Using with cache operations

QueryHelper match specs work seamlessly with all queryable operations:

use Nebulex.Adapters.Local.QueryHelper

# Get all values where key is an integer greater than 10
ms = match_spec key: k, value: v, where: is_integer(k) and k > 10, select: v
MyCache.get_all!(query: ms)

# Count entries with a specific tag
ms = match_spec tag: t, where: t == :user_session, select: true
MyCache.count_all!(query: ms)

# Delete expired entries (exp is not :infinity and less than now)
now = System.system_time(:millisecond)
ms = match_spec exp: e, where: e != :infinity and e < now, select: true
MyCache.delete_all!(query: ms)

# Stream entries in batches
ms = match_spec value: v, where: is_binary(v), select: v
MyCache.stream!(query: ms) |> Enum.take(100)

Practical examples

Here are some common patterns using QueryHelper:

defmodule MyApp.CacheQueries do
  use Nebulex.Adapters.Local.QueryHelper

  # Find all entries for a specific user
  def user_entries(user_id) do
    match_spec tag: t, where: t == {:user, user_id}, select: :"$_"
  end

  # Find entries expiring soon (within next hour)
  def expiring_soon do
    cutoff = System.system_time(:millisecond) + :timer.hours(1)
    match_spec exp: e, where: e != :infinity and e < cutoff, select: true
  end

  # Get all cached integers
  def integer_values do
    match_spec value: v, where: is_integer(v), select: v
  end

  # Complex filtering with multiple conditions
  def recent_tagged_entries(tag, min_time) do
    match_spec key: k,
               value: v,
               tag: t,
               touched: ts,
               where: t == tag and ts > min_time,
               select: {k, v}
  end
end

# Use in your application
MyApp.Cache.get_all!(query: MyApp.CacheQueries.user_entries(123))
MyApp.Cache.delete_all!(query: MyApp.CacheQueries.expiring_soon())

Working with cache references

When using the :references option with Nebulex.Caching decorators (like @decorate cacheable/3), Nebulex creates reference entries to track dependencies between cached values. The keyref_match_spec/2 helper makes it easy to find and clean up these reference entries.

The function builds a match spec that finds all cache keys (reference keys) that point to a specific referenced key. This is useful for:

  • Invalidating all entries that depend on a specific key
  • Counting how many references point to a key
  • Getting a list of dependent cache keys

Examples

import Nebulex.Adapters.Local.QueryHelper

# Delete all references to a specific key (any cache)
ms = keyref_match_spec(:user_123)
MyCache.delete_all!(query: ms)

# Delete references in a specific cache only
ms = keyref_match_spec(:user_123, cache: MyApp.UserCache)
MyCache.delete_all!(query: ms)

# Count how many cache entries reference a key
ms = keyref_match_spec(:product_456)
count = MyCache.count_all!(query: ms)

# Get all cache keys that reference a specific key
ms = keyref_match_spec(:user_123)
reference_keys = MyCache.get_all!(query: ms)

Example: Invalidating cached method results

When using the :references option with caching decorators, you can easily invalidate all cached results that depend on a specific entity:

defmodule MyApp.UserAccounts do
    use Nebulex.Caching, cache: MyApp.Cache
    use Nebulex.Adapters.Local.QueryHelper

    @decorate cacheable(key: id)
    def get_user_account(id) do
      # your logic ...
    end

    @decorate cacheable(key: email, references: &(&1 && &1.id))
    def get_user_account_by_email(email) do
      # your logic ...
    end

    @decorate cacheable(key: token, references: &(&1 && &1.id))
    def get_user_account_by_token(token) do
      # your logic ...
    end

    @decorate cache_evict(key: user.id, query: &__MODULE__.keyref_query/1)
    def update_user_account(user, attrs) do
      # your logic ...
    end

    def keyref_query(%{args: [user | _]} = _context) do
      keyref_match_spec(user.id)
    end
  end
end

See Nebulex.Adapters.Local.QueryHelper for complete documentation.

Tagging entries

The local adapter supports tagging cache entries with arbitrary terms via the :tag option. Tags provide a powerful way to organize and query cache entries by associating metadata with them. This is especially useful for:

  • Logical grouping - Group related entries (e.g., all entries for a specific user, session, or feature).
  • Selective invalidation - Delete all entries with a specific tag without affecting other cached data.
  • Efficient filtering - Query entries by tag using ETS match specs.

Tagging entries

You can tag entries when using put/3, put!/3, put_all/2, put_all!/2, and related operations:

# Tag a single entry
MyCache.put("user:123:profile", user_data, tag: :user_123)

# Tag multiple entries at once
MyCache.put_all(
  [
    {"session:abc:data", session_data},
    {"session:abc:prefs", preferences},
  ],
  tag: :session_abc
)

# Different tags for different entry groups
MyCache.put_all([a: 1, b: 2, c: 3], tag: :group_a)
MyCache.put_all([d: 4, e: 5, f: 6], tag: :group_b)

When you don't provide a tag, entries are stored with a nil tag value.

Querying by tag

You can query entries by tag using either QueryHelper (recommended for cleaner syntax) or Ex2ms:

# Using QueryHelper (recommended)
use Nebulex.Adapters.Local.QueryHelper

# Get all values for entries with a specific tag
match_spec = match_spec value: v, tag: t, where: t == :group_a, select: v
MyCache.get_all!(query: match_spec)
#=> [1, 2, 3]

# Delete all entries with a specific tag
match_spec = match_spec tag: t, where: t == :group_a, select: true
MyCache.delete_all!(query: match_spec)

# Query entries with multiple tags (return key-value tuples)
match_spec = match_spec key: k, value: v, tag: t,
                       where: t == :group_a or t == :group_b,
                       select: {k, v}
MyCache.get_all!(query: match_spec)
#=> [{:a, 1}, {:b, 2}, {:c, 3}, {:d, 4}, {:e, 5}, {:f, 6}]

# Count entries with a specific tag
match_spec = match_spec tag: t, where: t == :group_a, select: true
MyCache.count_all!(query: match_spec)
#=> 3

Alternatively, you can use Ex2ms if you need to work with the raw tuple structure:

# Using Ex2ms (alternative approach)
import Ex2ms

# Get all values for entries with a specific tag
match_spec = fun do
  {_, _, value, _, _, tag} when tag == :group_a -> value
end

MyCache.get_all!(query: match_spec)
#=> [1, 2, 3]

# Query entries with multiple tags
match_spec = fun do
  {_, key, value, _, _, tag} when tag == :group_a or tag == :group_b ->
    {key, value}
end

MyCache.get_all!(query: match_spec)
#=> [{:a, 1}, {:b, 2}, {:c, 3}, {:d, 4}, {:e, 5}, {:f, 6}]

Practical example

Here's a complete example showing how to use tags for user session management:

# Store user session data with tags
user_id = 123
session_id = "abc-def-ghi"

MyCache.put_all([
  {"user:#{user_id}:profile", user_profile},
  {"user:#{user_id}:settings", user_settings},
  {"user:#{user_id}:permissions", permissions}
], tag: {:user, user_id})

# Later, invalidate all data for this user using QueryHelper
use Nebulex.Adapters.Local.QueryHelper

invalidate_user = match_spec tag: t, where: t == {:user, 123}, select: true
MyCache.delete_all!(query: invalidate_user)

# Or using Ex2ms (alternative)
import Ex2ms

invalidate_user = fun do
  {_, _, _, _, _, tag} when tag == {:user, 123} -> true
end

MyCache.delete_all!(query: invalidate_user)

Tags can be any Elixir term (atoms, tuples, strings, etc.), giving you flexibility in how you organize your cache entries.

Using Caching Decorators with QueryHelper

The Nebulex.Caching decorators (@decorate) integrate seamlessly with QueryHelper and tagging for powerful cache management patterns. This section shows practical examples of combining decorators with QueryHelper and tags.

Entry Tagging with Decorators

You can tag entries automatically when using @decorate cacheable and @decorate cache_put by specifying the :tag option:

defmodule MyApp.UserCache do
  use Nebulex.Caching, cache: MyApp.Cache
  use Nebulex.Adapters.Local.QueryHelper

  # Cache user data with automatic tagging
  @decorate cacheable(key: user_id, opts: [tag: :users])
  def get_user(user_id) do
    # fetch user from database
    {:ok, user}
  end

  # Cache user permissions with automatic tagging
  @decorate cacheable(key: user_id, opts: [tag: :permissions])
  def get_user_permissions(user_id) do
    # fetch permissions from database
    {:ok, permissions}
  end

  # Store session data with automatic tagging
  @decorate cache_put(key: session_id, opts: [tag: :sessions])
  def create_session(session_id, data) do
    data
  end
end

Selective Cache Invalidation with Tags

Use the @decorate cache_evict decorator with QueryHelper to invalidate entries by tag. This is useful for clearing related cached data:

defmodule MyApp.UserCache do
  use Nebulex.Caching, cache: MyApp.Cache
  use Nebulex.Adapters.Local.QueryHelper

  # ... cacheable functions as above ...

  # Evict all cached data for a specific user
  @decorate cache_evict(query: &evict_user_query/1)
  def invalidate_user(user_id) do
    :ok
  end

  defp evict_user_query(context) do
    # Return a QueryHelper match spec to evict entries by tag
    match_spec tag: t, where: t == :users, select: true
  end
end

Cache Reference Invalidation with keyref_match_spec

When using the :references option to track cache dependencies, you can use keyref_match_spec with cache_evict to invalidate all dependent entries:

defmodule MyApp.UserAccounts do
  use Nebulex.Caching, cache: MyApp.Cache
  use Nebulex.Adapters.Local.QueryHelper

  # Cache user account by ID
  @decorate cacheable(key: user_id)
  def get_user_account(user_id) do
    fetch_user(user_id)
  end

  # Cache user account by email, referencing the user ID
  @decorate cacheable(key: email, references: &(&1 && &1.id))
  def get_user_account_by_email(email) do
    user = fetch_user_by_email(email)
    {:ok, user}
  end

  # Cache user account by token, also referencing the user ID
  @decorate cacheable(key: token, references: &(&1 && &1.id))
  def get_user_account_by_token(token) do
    user = fetch_user_by_token(token)
    {:ok, user}
  end

  # Evict all cache entries referencing a specific user
  # This invalidates all lookups (by id, email, token) in one operation
  @decorate cache_evict(key: user_id, query: &invalidate_refs/1)
  def update_user_account(user_id, attrs) do
    :ok
  end

  defp invalidate_refs(%{args: [user_id | _]} = _context) do
    keyref_match_spec(user_id)
  end
end

Practical Pattern: Clearing All Session Data

Here's a complete example showing how to manage user sessions with automatic tagging and selective eviction:

defmodule MyApp.Sessions do
  use Nebulex.Caching, cache: MyApp.Cache
  use Nebulex.Adapters.Local.QueryHelper

  # Store session data with automatic tagging by user ID
  @decorate cache_put(key: session_id, opts: [tag: {:session, user_id}])
  def store_session(user_id, session_id, data) do
    data
  end

  # Evict all sessions for a specific user when they log out
  @decorate cache_evict(query: &evict_user_sessions_query/1)
  def logout_user(user_id) do
    :ok
  end

  defp evict_user_sessions_query(%{args: [user_id]} = _context) do
    match_spec tag: t, where: t == {:session, user_id}
  end

  # Clear all sessions across all users (e.g., during maintenance)
  @decorate cache_evict(query: &evict_all_sessions_query/1)
  def clear_all_sessions do
    :ok
  end

  defp evict_all_sessions_query(_context) do
    # Match all entries with a session tag (pattern {:session, _})
    match_spec tag: t, where: is_tuple(t) and elem(t, 0) == :session
  end
end

The combination of decorators, QueryHelper, and tagging provides a clean, declarative way to manage cache lifecycles with minimal boilerplate.

Transaction API

This adapter inherits the default implementation provided by Nebulex.Adapter.Transaction. Therefore, the transaction command accepts the following options:

  • :keys (list of term/0) - The list of keys the transaction will lock. Since the lock ID is generated based on the key, the transaction uses a fixed lock ID if the option is not provided or is an empty list. Then, all subsequent transactions without this option (or set to an empty list) are serialized, and performance is significantly affected. For that reason, it is recommended to pass the list of keys involved in the transaction. The default value is [].

  • :nodes (list of atom/0) - The list of the nodes where to set the lock.

    The default value is [node()].

  • :retries (:infinity | non_neg_integer/0) - If the key has already been locked by another process and retries are not equal to 0, the process sleeps for a while and tries to execute the action later. When :retries attempts have been made, an exception is raised. If :retries is :infinity (the default), the function will eventually be executed (unless the lock is never released). The default value is :infinity.

Extended API (extra functions)

This adapter provides some additional convenience functions to the Nebulex.Cache API.

Creating new generations:

MyCache.new_generation()
MyCache.new_generation(gc_interval_reset: false)

Retrieving the current generations:

MyCache.generations()

Retrieving the newer generation:

MyCache.newer_generation()

Summary

Types

Adapter's backend type

The type for the :gc_memory_check_interval option value.

Types

backend()

@type backend() :: :ets | :shards

Adapter's backend type

mem_check_interval()

@type mem_check_interval() ::
  pos_integer()
  | (limit :: :size | :memory,
     current :: non_neg_integer(),
     max :: non_neg_integer() ->
       timeout :: pos_integer())

The type for the :gc_memory_check_interval option value.

The :gc_memory_check_interval value can be:

  • A positive integer with the time in milliseconds.
  • An anonymous function to call in runtime and must return the next interval in milliseconds. The function receives three arguments:
    • The first argument is an atom indicating the limit, whether it is :size or :memory.
    • The second argument is the current value for the limit. For example, if the limit in the first argument is :size, the second argument tells the current cache size (number of entries in the cache). If the limit is :memory, it means the recent cache memory in bytes.
    • The third argument is the maximum limit provided in the configuration. When the limit in the first argument is :size, it is the :max_size. On the other hand, if the limit is :memory, it is the :allocated_memory.

Functions

entry(args \\ [])

(macro)

entry(record, args)

(macro)

with_retry(fun, retries \\ 3)