### Configure and Run Shared Tests Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/creating-new-adapter.md Edit the main test file to use the shared test suite and define a setup callback to start the cache process. ```elixir defmodule NebulexMemoryAdapterTest do use ExUnit.Case, async: true use NebulexMemoryAdapter.CacheTest alias NebulexMemoryAdapter.TestCache, as: Cache setup do pid = start_supervised!(Cache) _ignore = Cache.delete_all!() :ok {:ok, cache: Cache, name: Cache} end end ``` -------------------------------- ### Setup Nebulex Local Dependency Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/creating-new-adapter.md Execute these commands to clone the Nebulex repository locally and set the NEBULEX_PATH environment variable. ```console export NEBULEX_PATH=nebulex mix nbx.setup ``` -------------------------------- ### Elixir Mix Task Documentation Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/elixir.md Get documentation for an individual mix task by using `mix help` followed by the task name. ```bash mix help task_name ``` -------------------------------- ### Use Multilevel Cache Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Demonstrates basic usage of the multilevel cache by putting and getting a key-value pair with a specified TTL. ```elixir iex> Blog.NearCache.put("foo", "bar", ttl: :timer.hours(1)) :ok iex> Blog.NearCache.get!("foo") "bar" ``` -------------------------------- ### Conventional Commits Examples Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/workflow.md Examples of commit messages following the Conventional Commits format, including type, scope, and a concise summary. ```text feat(cache): add runtime option validation for ttl ``` ```text fix(decorators): handle nested context pop safely ``` ```text chore(workflow): refine session bootstrap steps ``` -------------------------------- ### Get Entry with Default Value Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use `Blog.Cache.get/2` to retrieve an entry, returning a default value if the key is not found. `get!/2` raises an exception on error. ```elixir # Using `get` callback iex> {:ok, user1} = Blog.Cache.get(1) iex> user1.id 1 # Returns the default because the key doesn't exist iex> Blog.Cache.get("unknown") {:ok, nil} iex> Blog.Cache.get("unknown", "default") {:ok, "default"} # Using `get!` (same as `get` but raises an exception in case of error) iex> user1 = Blog.Cache.get!(1) iex> user1.id 1 iex> Blog.Cache.get!("unknown") nil iex> Blog.Cache.get!("unknown", "default") "default" iex> Enum.map(1..3, &(Blog.Cache.get!(&1).first_name)) ["Galileo", "Charles", "Albert"] ``` -------------------------------- ### Configure Multilevel Cache Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/nebulex.md Configure multilevel caches using the `inclusion_policy` and `levels` options. This example sets up two levels with specific garbage collection intervals. ```elixir 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)]}] ``` -------------------------------- ### Elixir Module Structure Example Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/elixir-style.md Follow this order for module attributes, directives, and macros. Add blank lines between groupings and sort terms alphabetically. ```elixir 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 ``` -------------------------------- ### Query-Based Eviction with @cache_evict Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Perform bulk cache eviction based on criteria using the `:query` option. The query format is adapter-specific; this example uses Nebulex.Adapters.Local.QueryHelper. ```elixir use Nebulex.Adapters.Local.QueryHelper @decorate cache_evict(query: &query_for_category/1) def delete_category_products(category_id) do # Evicts all products in this category Repo.delete_all(from p in Product, where: p.category_id == ^category_id) end defp query_for_category(%{args: [category_id]}) do # QueryHelper provides a clean DSL for building match specs match_spec value: %{category_id: cat_id}, where: cat_id == ^category_id, select: true end ``` -------------------------------- ### Setup Nebulex Caching Decorators Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Add the `:decorator` dependency to your `mix.exs` and enable caching decorators in your module using the `use Nebulex.Caching` macro. ```elixir defmodule MyApp.Products do use Nebulex.Caching, cache: MyApp.Cache, on_error: :nothing # Your decorated functions here... end ``` -------------------------------- ### Get All Cache Info Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/info-api.md Retrieves all available information about the cache, including server details, memory usage, and statistics. This requires the cache to be set up in the application's supervision tree. ```elixir iex> MyApp.Cache.info!() %{ server: %{ nbx_version: "3.0.0", cache_module: "MyApp.Cache", cache_adapter: "Nebulex.Adapters.Local", cache_name: "MyApp.Cache", cache_pid: #PID<0.111.0> }, memory: %{ total: 1_000_000, used: 0 }, stats: %{ deletions: 0, evictions: 0, expirations: 0, hits: 0, misses: 0, updates: 0, writes: 0 } } ``` -------------------------------- ### Add Cache to Supervision Tree Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Integrate the `Blog.Cache` into your application's supervision tree in `lib/blog/application.ex` to ensure it starts with the application. ```elixir def start(_type, _args) do children = [ Blog.Cache ] # ... rest of your supervision tree ``` -------------------------------- ### Update `get` calls to use trailing bang Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/upgrading/v3.0.md Use the `get!/1` function for a quick update to the `get` calls. This version handles missing keys by raising an error instead of returning `nil`. ```diff - value = MyApp.Cache.get("key") + value = MyApp.Cache.get!("key") ``` -------------------------------- ### Evicting Entries with References Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md When evicting entries that have references, manually specify all keys to ensure both the primary value and its references are removed. This example shows how to delete a user and its associated reference. ```elixir # Evict both the primary key and the reference @decorate cache_evict(key: {:in, [user.id, user.email]}) def delete_user(user) do # This evicts both the user.id key and the user.email reference Repo.delete(user) end ``` -------------------------------- ### Add Cache to Supervision Tree Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Includes the newly defined `Blog.NearCache` in the application's supervision tree to ensure it starts with the application. ```elixir children = [ Blog.Cache, Blog.PartitionedCache, Blog.NearCache ] ... ``` -------------------------------- ### Retrieve All Cache Information Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use `Blog.Cache.info()` to get comprehensive details about the cache's server status, memory usage, and statistics. The result is a map containing server, memory, and stats information. ```elixir iex> {:ok, info} = Blog.Cache.info() iex> info ``` -------------------------------- ### Transaction Example for Cacheable Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/nebulex.md Use the `:transaction` option to wrap cache operations, preventing race conditions and cache stampede when multiple processes might update the same key concurrently. ```elixir @decorate cacheable(key: id, transaction: true) def get_user(id) do Repo.get(User, id) end ``` -------------------------------- ### Nebulex Cache Testing Structure Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/nebulex.md Use `Nebulex.CacheCase` and the `deftests` macro for shared test suites across different Nebulex adapters. Structure tests with `describe` and use context fixtures for setup. ```elixir 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 ``` -------------------------------- ### Configure Partitioned Cache Options Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Set up the configuration for the partitioned adapter, including backend, GC interval, and memory limits. ```elixir config :blog, Blog.PartitionedCache, primary: [ # When using :shards as backend backend: :shards, # GC interval for pushing new generation: 12 hrs gc_interval: :timer.hours(12), # Max 1 million entries in cache max_size: 1_000_000, # Max 2 GB of memory allocated_memory: 2_000_000_000, # GC memory check interval gc_memory_check_interval: :timer.seconds(10) ] ``` -------------------------------- ### Fetch Project Dependencies Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/creating-new-adapter.md Run this command after setting up the Nebulex dependency to fetch all project dependencies. ```console mix deps.get ``` -------------------------------- ### Build Documentation Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/architecture.md Generate project documentation. This command must produce no warnings, ensuring all public APIs are properly documented and linked. ```bash mix docs ``` -------------------------------- ### Update `get` calls with default value or error handling Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/upgrading/v3.0.md When updating `get` calls, you can now provide a default value or handle the response as an `{:ok, value}` or `{:error, reason}` tuple. ```elixir # Using the default value = MyApp.Cache.get!("key", "default") ``` ```elixir # Handling the response case MyApp.Cache.get("key", "default") do {:ok, "default"} -> #=> your logic handling success with default value {:ok, value} -> #=> your logic handling success {:error, reason} -> #=> your logic handling other errors end ``` -------------------------------- ### Get Time To Live (TTL) with ttl Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use the `ttl` function to get the remaining Time To Live (TTL) or expiration time for a key. If no TTL is set, it defaults to `:infinity`. Returns an error tuple if the key doesn't exist. ```elixir iex> Blog.Cache.ttl(1) {:ok, :infinity} ``` ```elixir iex> {:error, %Nebulex.KeyError{} = e} = Blog.Cache.ttl("nonexistent") iex> e.key "nonexistent" ``` -------------------------------- ### Register a Basic Cache Event Listener Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Set up a basic event listener by defining a module with a `handle/1` function and registering it using `Blog.Cache.register_event_listener/1`. This listener will be invoked for all cache events. ```elixir defmodule Blog.Cache.EventHandler do def handle(event) do IO.inspect(event, label: "Cache Event") end end # Register the event listener iex> Blog.Cache.register_event_listener(&Blog.Cache.EventHandler.handle/1) ``` -------------------------------- ### QueryHelper for Multiple Product Criteria Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Shows how to use `QueryHelper` to match products based on category, status, and stock level. Requires `use Nebulex.Adapters.Local.QueryHelper`. ```elixir use Nebulex.Adapters.Local.QueryHelper # Match products by multiple criteria defp query_products(%{args: [category_id, status]}) do match_spec( value: %{ category_id: cat_id, status: st, stock: stock }, where: cat_id == ^category_id and st == ^status and stock > 0, select: true ) end ``` -------------------------------- ### Generate Partitioned Cache Module Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use the mix nbx.gen.cache task to generate the initial cache module and configuration files. ```bash mix nbx.gen.cache -c Blog.PartitionedCache ``` -------------------------------- ### Basic Product Caching with Nebulex Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Implement basic read-through caching for product data. Use `cacheable` for reads, `cache_put` for updates, and `cache_evict` for deletions. Ensure `on_error` is set to `:nothing` for graceful error handling. ```elixir defmodule MyApp.Catalog do use Nebulex.Caching, cache: MyApp.Cache, on_error: :nothing alias MyApp.Repo alias MyApp.Catalog.Product @ttl :timer.hours(2) @decorate cacheable(key: id, opts: [ttl: @ttl]) def get_product(id) do Repo.get(Product, id) end @decorate cache_put(key: product.id, match: &match_ok/1, opts: [ttl: @ttl]) def update_product(product, attrs) do product |> Product.changeset(attrs) |> Repo.update() end @decorate cache_evict(key: id) def delete_product(id) do Repo.delete(Product, id) end defp match_ok({:ok, product}), do: {true, product} defp match_ok(_), do: false end ``` -------------------------------- ### Elixir Guard Clause Example Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/elixir.md Use guard clauses in function definitions to add conditions that must be met for the clause to be executed. ```elixir when is_binary(name) and byte_size(name) > 0 ``` -------------------------------- ### Implement Nebulex Adapter with Agent Initialization Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/creating-new-adapter.md Refines the adapter's initialization to use an Agent with an empty map as its initial state, preparing for key-value storage. ```elixir defmodule NebulexMemoryAdapter do @behaviour Nebulex.Adapter import Nebulex.Utils @impl Nebulex.Adapter defmacro __before_compile__(_env), do: :ok @impl Nebulex.Adapter def init(_opts) do child_spec = Supervisor.child_spec({Agent, fn -> %{} end}, id: {Agent, 1}) {:ok, child_spec, %{}} end end ``` -------------------------------- ### Get Specific Cache Info Section Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/info-api.md Fetches a specific section of cache information, such as only the statistics or memory details. This is useful for targeted monitoring. ```elixir iex> MyApp.Cache.info!(:stats) %{ deletions: 0, evictions: 0, expirations: 0, hits: 0, misses: 0, updates: 0, writes: 0 } ``` ```elixir iex> MyApp.Cache.info!([:stats, :memory]) %{ memory: %{ total: 1_000_000, used: 0 }, stats: %{ deletions: 0, evictions: 0, expirations: 0, hits: 0, misses: 0, updates: 0, writes: 0 } } ``` -------------------------------- ### Run Tests Source: https://github.com/elixir-nebulex/nebulex/blob/main/README.md Execute all tests using `mix test`. For comprehensive checks including CI, use `mix test.ci`. ```bash $ mix test ``` ```bash $ mix test.ci ``` -------------------------------- ### Use Timeout Option with Partitioned Cache Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Demonstrates how to specify a timeout in milliseconds for operations on the partitioned cache. ```elixir iex> Blog.PartitionedCache.get("foo", timeout: 10) #=> {:ok, value} ``` ```elixir # when the command's call timed out an error is returned iex> Blog.PartitionedCache.put("foo", "bar", timeout: 10) #=> {:error, %Nebulex.Error{reason: :timeout}} ``` -------------------------------- ### Run Benchmarks Source: https://github.com/elixir-nebulex/nebulex/blob/main/README.md Run benchmark tests using `mix bench`. The benchmarks focus on the Nebulex abstraction layer performance. ```bash $ mix bench ``` -------------------------------- ### Add Cache to Supervision Tree Source: https://github.com/elixir-nebulex/nebulex/blob/main/README.md Include your defined Nebulex.Cache module within the children list of your application's start function to ensure it is supervised. ```elixir def start(_type, _args) do children = [ MyApp.Cache ] # ... rest of your supervision tree ``` -------------------------------- ### Basic Cache Operations Source: https://github.com/elixir-nebulex/nebulex/blob/main/README.md Demonstrates basic cache operations: putting a value with a key and fetching it back using the defined cache module. ```elixir iex> MyApp.Cache.put("foo", "bar") :ok iex> MyApp.Cache.fetch("foo") {:ok, "bar"} ``` -------------------------------- ### Fetch Entry by Key Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use `Blog.Cache.fetch/1` to retrieve an entry by its key. It returns `{:ok, value}` if found, or `{:error, %Nebulex.KeyError{}}` if not. `fetch!/1` raises an exception on error. ```elixir # Using `fetch` callback iex> {:ok, user1} = Blog.Cache.fetch(1) iex> user1.id 1 # If the key doesn't exist an error tuple is returned iex> {:error, %Nebulex.KeyError{} = e} = Blog.Cache.fetch("unknown") iex> e.key "unknown" # Using `fetch!` (same as `fetch` but raises an exception in case of error) iex> user1 = Blog.Cache.fetch!(1) iex> user1.id 1 ``` -------------------------------- ### QueryHelper for Expensive Products Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Demonstrates using `QueryHelper` to match products within a category that have a price greater than 100. Requires `use Nebulex.Adapters.Local.QueryHelper`. ```elixir use Nebulex.Adapters.Local.QueryHelper # Match products in a category with price > 100 defp query_expensive_products(%{args: [category_id]}) do match_spec value: %{category_id: cat_id, price: price}, where: cat_id == ^category_id and price > 100, select: true end ``` -------------------------------- ### Define Cache with Compile-Time Adapter Options Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/nebulex.md Configure compile-time adapter options, such as `:primary_storage_adapter`, when using adapters like Partitioned or Coherent. These options are passed via `adapter_opts`. ```elixir defmodule MyApp.PartitionedCache do use Nebulex.Cache, otp_app: :my_app, adapter: Nebulex.Adapters.Partitioned, adapter_opts: [primary_storage_adapter: Nebulex.Adapters.Local] end ``` -------------------------------- ### Cache-Aside: Imperative Read Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/cache-usage-patterns.md Check the cache first, and if the key is not found, fetch the value from the system-of-record (SoR) and then store it in the cache. ```elixir with {:error, _reason} <- MyCache.fetch(key) do value = SoR.get(key) # e.g., Ecto.Repo MyCache.put(key, value) value end ``` -------------------------------- ### Accounts Context with Caching Source: https://github.com/elixir-nebulex/nebulex/blob/main/README.md Implement an accounts context using `Nebulex.Caching` to cache user data. Cache entries expire after 1 hour. ```elixir defmodule MyApp.Accounts do use Nebulex.Caching, cache: MyApp.Cache alias MyApp.Accounts.User alias MyApp.Repo # Cache entries expire after 1 hour @ttl :timer.hours(1) @decorate cacheable(key: {User, id}, opts: [ttl: @ttl]) def get_user!(id) do Repo.get!(User, id) end @decorate cacheable(key: {User, username}, references: & &1.id) def get_user_by_username(username) do Repo.get_by(User, [username: username]) end @decorate cache_put( key: {User, user.id}, match: &__MODULE__.match_update/1, opts: [ttl: @ttl] ) def update_user(%User{} = user, attrs) do user |> User.changeset(attrs) |> Repo.update() end @decorate cache_evict(key: {User, user.id}) def delete_user(%User{} = user) do Repo.delete(user) end def create_user(attrs \\ %{}) do %User{} \\ |> User.changeset(attrs) |> Repo.insert() end def match_update({:ok, value}), do: {true, value} def match_update({:error, _}), do: false end ``` -------------------------------- ### Update Transaction API calls Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/upgrading/v3.0.md Transaction API callbacks now require the ok/error tuple API. The `transaction/1` function should use the `get!/1` variant for fetching keys within the transaction. ```diff - bool = MyApp.Cache.in_transaction?() + {:ok, bool} = MyApp.Cache.in_transaction?() ``` ```diff - value = MyApp.Cache.transaction(fn -> MyApp.Cache.get("key") end) + {:ok, value} = MyApp.Cache.transaction(fn -> MyApp.Cache.get!("key") end) ``` -------------------------------- ### Configure Partitioned Cache with Custom Primary Storage Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Specify a different primary storage adapter, like Nebulex.Adapters.Cachex, for the partitioned cache. ```elixir defmodule Blog.PartitionedCache do use Nebulex.Cache, otp_app: :blog, adapter: Nebulex.Adapters.Partitioned, adapter_opts: [primary_storage_adapter: Nebulex.Adapters.Cachex] end ``` -------------------------------- ### Handle Ok/Error Tuples in Cache API Calls Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/upgrading/v3.0.md Nebulex v3 introduces an ok/error tuple API for all cache functions. This example shows how to handle the success case by ignoring the result. ```diff - :ok = MyApp.Cache.put("key", "value") + _ignore = MyApp.Cache.put("key", "value") ``` -------------------------------- ### Tag-Based Reference Grouping for Eviction Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Use tags to group related cache entries. Evict all entries with a specific tag in a single operation. This requires planning tags during caching setup. ```elixir use Nebulex.Adapters.Local.QueryHelper @decorate cacheable(key: id, opts: [tag: "user"]) def get_user(id) do Repo.get(User, id) end @decorate cacheable( key: email, references: &(&1 && &1.id), opts: [tag: "user"] ) def get_user_by_email(email) do Repo.get_by(User, email: email) end @decorate cacheable( key: username, references: &(&1 && &1.id), opts: [tag: "user"] ) def get_user_by_username(username) do Repo.get_by(User, username: username) end # Evict the user and ALL references by tag @decorate cache_evict(query: &evict_user_by_tag/1) def delete_user(user) do Repo.delete(user) end defp evict_user_by_tag(%{args: [_user]}) do match_spec tag: t, where: t == "user", select: true end ``` -------------------------------- ### Create New Mix Project Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/creating-new-adapter.md Use this command to generate a new Elixir Mix project for your custom Nebulex adapter. ```console mix new nebulex_memory_adapter ``` -------------------------------- ### Elixir DynamicSupervisor Child Specification Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/elixir.md When using `DynamicSupervisor`, ensure the child spec includes a name for referencing the supervisor. This allows starting children dynamically using the supervisor's name. ```elixir {DynamicSupervisor, name: MyApp.MyDynamicSup} ``` ```elixir DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec) ``` -------------------------------- ### Add Nebulex and Dependencies to mix.exs Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Update your `mix.exs` file to include Nebulex, a cache adapter, and recommended dependencies like `:decorator` and `:telemetry`. ```elixir defp deps do [ {:nebulex, "~> 3.0"}, # Use the official local cache adapter {:nebulex_local, "~> 3.0"}, # Required for caching decorators (recommended) {:decorator, "~> 1.4"}, # Required for telemetry events (recommended) {:telemetry, "~> 1.0"}, # Required for :shards backend in local adapter {:shards, "~> 1.1"} ] end ``` -------------------------------- ### Caching Products by ID and Slug with References Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Handle multiple access patterns (ID and slug) for products using Nebulex references. This stores the product data once and uses a reference for the slug, optimizing memory usage. Eviction automatically removes both. ```elixir @decorate cacheable(key: id, opts: [ttl: @ttl]) def get_product(id) do Repo.get(Product, id) end @decorate cacheable(key: slug, references: &(&1 && &1.id), opts: [ttl: @ttl]) def get_product_by_slug(slug) do Repo.get_by(Product, slug: slug) end # When evicting, remove both keys @decorate cache_evict(key: {:in, [product.id, product.slug]}) def delete_product(product) do Repo.delete(product) end ``` -------------------------------- ### Cache Put Decorator with Conditional Update Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/nebulex.md Use `@decorate cache_put` for write-through caching. Always use the `:match` option to conditionally update the cache, for example, only when a database operation returns `{:ok, value}`. ```elixir @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 ``` -------------------------------- ### Implement Nebulex.Adapter.KV Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/creating-new-adapter.md Provides a complete in-memory adapter implementation for Nebulex.Adapter.KV, handling fetch, put, delete, and other key-value operations using Elixir's Agent. ```elixir defmodule NebulexMemoryAdapter do @behaviour Nebulex.Adapter @behaviour Nebulex.Adapter.KV import Nebulex.Utils @impl Nebulex.Adapter defmacro __before_compile__(_env), do: :ok @impl Nebulex.Adapter def init(_opts) do child_spec = Supervisor.child_spec({Agent, fn -> %{} end}, id: {Agent, 1}) {:ok, child_spec, %{}} end ## Nebulex.Adapter.KV Implementation @impl Nebulex.Adapter.KV def fetch(adapter_meta, key, _opts) do wrap_ok Agent.get(adapter_meta.pid, &Map.get(&1, key)) end @impl Nebulex.Adapter.KV def put(adapter_meta, key, value, on_write, ttl, _opts) do Agent.update(adapter_meta.pid, &Map.put(&1, key, value)) {:ok, true} end @impl Nebulex.Adapter.KV def put_all(adapter_meta, entries, on_write, ttl, _opts) do entries = Map.new(entries) Agent.update(adapter_meta.pid, &Map.merge(&1, entries)) {:ok, true} end @impl Nebulex.Adapter.KV def delete(adapter_meta, key, _opts) do wrap_ok Agent.update(adapter_meta.pid, &Map.delete(&1, key)) end @impl Nebulex.Adapter.KV def take(adapter_meta, key, _opts) do value = Agent.get(adapter_meta.pid, &Map.get(&1, key)) delete(adapter_meta, key, []) {:ok, value} end @impl Nebulex.Adapter.KV def update_counter(adapter_meta, key, amount, default, ttl, _opts) do Agent.update(adapter_meta.pid, fn state -> Map.update(state, key, default + amount, fn v -> v + amount end) end) wrap_ok Agent.get(adapter_meta.pid, &Map.get(&1, key)) end @impl Nebulex.Adapter.KV def has_key?(adapter_meta, key, _opts) do wrap_ok Agent.get(adapter_meta.pid, &Map.has_key?(&1, key)) end @impl Nebulex.Adapter.KV def ttl(_adapter_meta, _key, _opts) do {:ok, nil} end @impl Nebulex.Adapter.KV def expire(_adapter_meta, _key, _ttl, _opts) do {:ok, true} end @impl Nebulex.Adapter.KV def touch(_adapter_meta, _key, _opts) do {:ok, true} end end ``` -------------------------------- ### Create New Elixir Application Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use this command to create a new Elixir application with a supervision tree, which is required for Nebulex. ```bash mix new blog --sup ``` -------------------------------- ### Insert Multiple New Entries Conditionally Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use `Blog.Cache.put_new_all/1` to insert multiple entries only if none of the keys already exist. `put_new_all!/1` raises an error if any key exists. ```elixir iex> new_users = %{ ...> 6 => %{id: 6, first_name: "Isaac", last_name: "Newton"}, ...> 7 => %{id: 7, first_name: "Marie", last_name: "Curie"} ...> } iex> Blog.Cache.put_new_all(new_users) {:ok, true} # none of the entries is inserted if at least one key already exists iex> Blog.Cache.put_new_all(new_users) {:ok, false} # same as previous one but raises `Nebulex.Error` in case of error iex> Blog.Cache.put_new_all!(new_users) false ``` -------------------------------- ### Configure Partitioned Cache Adapter Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Update the generated cache module to use the Nebulex.Adapters.Partitioned. ```elixir defmodule Blog.PartitionedCache do use Nebulex.Cache, otp_app: :blog, adapter: Nebulex.Adapters.Partitioned end ``` -------------------------------- ### Define Public and Private Commands with Nebulex Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/nebulex.md Use `defcommand/2` for public command wrappers and `defcommandp/2` for private ones. Commands handle telemetry, metadata, and error wrapping automatically. The first parameter is the cache name or PID, and the last is an options keyword list. ```elixir defcommand fetch(name, key, opts) defcommandp do_put(name, key, value, on_write, ttl, keep_ttl?, opts), command: :put ``` -------------------------------- ### Test Helper Configuration for Nebulex Adapter Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/creating-new-adapter.md Configures the test environment to load necessary support files from the Nebulex dependency, enabling adapter testing. ```elixir # Nebulex dependency path nbx_dep_path = Mix.Project.deps_paths()[:nebulex] for file <- File.ls!("#{nbx_dep_path}/test/support"), file != "test_cache.ex" do Code.require_file("#{nbx_dep_path}/test/support/" <> file, __DIR__) end for file <- File.ls!("#{nbx_dep_path}/test/shared/cache") do Code.require_file("#{nbx_dep_path}/test/shared/cache/" <> file, __DIR__) end for file <- File.ls!("#{nbx_dep_path}/test/shared"), file != "cache" do Code.require_file("#{nbx_dep_path}/test/shared/" <> file, __DIR__) end # Load shared tests for file <- File.ls!("test/shared"), not File.dir?("test/shared/" <> file) do Code.require_file("./shared/" <> file, __DIR__) end ExUnit.start() ``` -------------------------------- ### Implementing Nebulex.Adapter.Queryable Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/creating-new-adapter.md This snippet shows the complete implementation of a Nebulex adapter that includes both KV and Queryable behaviors. It uses an Agent to manage an in-memory map for storing cache data. ```elixir defmodule NebulexMemoryAdapter do @behaviour Nebulex.Adapter @behaviour Nebulex.Adapter.KV @behaviour Nebulex.Adapter.Queryable import Nebulex.Utils @impl Nebulex.Adapter defmacro __before_compile__(_env), do: :ok @impl Nebulex.Adapter def init(_opts) do child_spec = Supervisor.child_spec({Agent, fn -> %{} end}, id: {Agent, 1}) {:ok, child_spec, %{}} end @impl Nebulex.Adapter.KV def fetch(adapter_meta, key, _opts) do wrap_ok Agent.get(adapter_meta.pid, &Map.get(&1, key)) end @impl Nebulex.Adapter.KV def put(adapter_meta, key, value, ttl, op, opts) def put(adapter_meta, key, value, ttl, :put_new, opts) do if get(adapter_meta, key, []) do false else put(adapter_meta, key, value, ttl, :put, opts) true end |> wrap_ok() end def put(adapter_meta, key, value, ttl, :replace, opts) do if get(adapter_meta, key, []) do put(adapter_meta, key, value, ttl, :put, opts) true else false end |> wrap_ok() end def put(adapter_meta, key, value, _ttl, _on_write, _opts) do Agent.update(adapter_meta.pid, &Map.put(&1, key, value)) {:ok, true} end @impl Nebulex.Adapter.KV def put_all(adapter_meta, entries, ttl, op, opts) def put_all(adapter_meta, entries, ttl, :put_new, opts) do if get_all(adapter_meta, Map.keys(entries), []) == %{} do put_all(adapter_meta, entries, ttl, :put, opts) true else false end |> wrap_ok() end def put_all(adapter_meta, entries, _ttl, _on_write, _opts) do entries = Map.new(entries) Agent.update(adapter_meta.pid, &Map.merge(&1, entries)) {:ok, true} end @impl Nebulex.Adapter.KV def delete(adapter_meta, key, _opts) do wrap_ok Agent.update(adapter_meta.pid, &Map.delete(&1, key)) end @impl Nebulex.Adapter.KV def take(adapter_meta, key, _opts) do value = get(adapter_meta, key, []) delete(adapter_meta, key, []) {:ok, value} end @impl Nebulex.Adapter.KV def update_counter(adapter_meta, key, amount, _ttl, default, _opts) do Agent.update(adapter_meta.pid, fn state -> Map.update(state, key, default + amount, fn v -> v + amount end) end) wrap_ok get(adapter_meta, key, []) end @impl Nebulex.Adapter.KV def has_key?(adapter_meta, key, _opts) do wrap_ok Agent.get(adapter_meta.pid, &Map.has_key?(&1, key)) end @impl Nebulex.Adapter.KV def ttl(_adapter_meta, _key, _opts) do {:ok, nil} end @impl Nebulex.Adapter.KV def expire(_adapter_meta, _key, _ttl, _opts) do {:ok, true} end @impl Nebulex.Adapter.KV def touch(_adapter_meta, _key, _opts) do {:ok, true} end @impl Nebulex.Adapter.Queryable def execute(adapter_meta, query_meta, _opts) do do_execute(adapter_meta.pid, query_meta) end def do_execute(pid, %{op: :delete_all} = query_meta) do deleted = do_execute(pid, %{query_meta | op: :count_all}) Agent.update(pid, fn _state -> %{} end) {:ok, deleted} end def do_execute(pid, %{op: :count_all}) do wrap_ok Agent.get(pid, &map_size/1) end def do_execute(pid, %{op: :get_all, query: {:q, nil}}) do wrap_ok Agent.get(pid, &Map.values/1) end # Fetching multiple keys def do_execute(pid, %{op: :get_all, query: {:in, keys}}) do pid |> Agent.get(&Map.take(&1, keys)) |> Map.to_list() |> wrap_ok() end @impl Nebulex.Adapter.Queryable def stream(adapter_meta, query_meta, _opts) do do_stream(adapter_meta.pid, query_meta) end def do_stream(pid, %{query: {:q, q}, select: select}) when q in [nil, :all] do fun = case select do :value -> &Map.values/1 {:key, :value} -> &Map.to_list/1 _ -> &Map.keys/1 end wrap_ok Agent.get(pid, fun) end def do_stream(_pid_, query) do wrap_error Nebulex.QueryError, query: query end end ``` -------------------------------- ### Add Nebulex Dependencies to mix.exs Source: https://github.com/elixir-nebulex/nebulex/blob/main/README.md Include Nebulex, a cache adapter (e.g., Nebulex.Adapters.Local), and optional dependencies like :decorator and :telemetry in your project's mix.exs file. ```elixir def deps do [ {:nebulex, "~> 3.0"}, {:nebulex_local, "~> 3.0"}, # Generational local cache adapter {:decorator, "~> 1.4"}, # Required for caching decorators {:telemetry, "~> 1.3"} # Required for telemetry events ] end ``` -------------------------------- ### Insert Multiple Entries into Cache Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use `Blog.Cache.put_all/1` to insert multiple key/value pairs at once. The entries can be provided as a map or a list of key/value tuples. ```elixir iex> users = %{ ...> 1 => %{id: 1, first_name: "Galileo", last_name: "Galilei"}, ...> 2 => %{id: 2, first_name: "Charles", last_name: "Darwin"}, ...> 3 => %{id: 3, first_name: "Albert", last_name: "Einstein"} ...> } iex> Blog.Cache.put_all(users) :ok ``` -------------------------------- ### Elixir Testing Specific Files Source: https://github.com/elixir-nebulex/nebulex/blob/main/usage-rules/elixir.md Run tests in a specific file using `mix test` followed by the file path. ```bash mix test test/my_test.exs ``` -------------------------------- ### Generate Cache Configuration Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use the `nbx.gen.cache` mix task to generate the necessary configuration files for your Nebulex cache. ```bash mix nbx.gen.cache -c Blog.Cache ``` -------------------------------- ### Combined Tag and Query Eviction Strategy Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Combine tag-based eviction with direct reference queries for maximum robustness. The tag-based method acts as the primary cleanup, while the query serves as a fallback for any orphaned references. ```elixir use Nebulex.Adapters.Local.QueryHelper @decorate cacheable(key: id, opts: [tag: "user"]) def get_user(id) do Repo.get(User, id) end @decorate cacheable(key: email, references: &(&1 && &1.id), opts: [tag: "user"]) def get_user_by_email(email) do Repo.get_by(User, email: email) end @decorate cacheable(key: username, references: &(&1 && &1.id), opts: [tag: "user"]) def get_user_by_username(username) do Repo.get_by(User, username: username) end # Use both tag and query for maximum robustness: # - Tag evicts everything if references are properly tagged # - Query catches any orphaned references as a fallback @decorate cache_evict(query: &evict_users_by_tag/0) def delete_all_user_data(user) do Repo.delete(user) end defp evict_users_by_tag do match_spec tag: t, where: t == "user", select: true end # For individual user deletion, use keyref_match_spec for flexibility @decorate cache_evict(key: user.id, query: &evict_user_refs/1) def delete_user(user) do Repo.delete(user) end defp evict_user_refs(%{args: [user]}) do keyref_match_spec(user.id) end ``` -------------------------------- ### QueryHelper DSL for Category Query Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/declarative-caching.md Uses the `QueryHelper` DSL to create a readable match specification for querying a category by its ID. Requires `use Nebulex.Adapters.Local.QueryHelper`. ```elixir use Nebulex.Adapters.Local.QueryHelper defp query_for_category(%{args: [category_id]}) do match_spec value: %{category_id: cat_id}, where: cat_id == ^category_id, select: true end ``` -------------------------------- ### Insert New Entry Conditionally Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/introduction/getting-started.md Use `Blog.Cache.put_new/3` to insert an entry only if the key does not already exist. `put_new!/3` raises an error if the key exists. ```elixir iex> new_user = %{id: 4, first_name: "John", last_name: "Doe"} iex> Blog.Cache.put_new(new_user.id, new_user, ttl: 900) {:ok, true} iex> Blog.Cache.put_new(new_user.id, new_user) {:ok, false} # Same as the previous one but raises `Nebulex.Error` in case of error iex> Blog.Cache.put_new!(new_user.id, new_user) false ``` -------------------------------- ### Update `all/2` calls to `get_all/2` Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/upgrading/v3.0.md Replace calls to `MyApp.Cache.all()` with `MyApp.Cache.get_all!()`. The new `get_all/2` function accepts a query-spec as its first argument. ```diff - results = MyApp.Cache.all() + results = MyApp.Cache.get_all!() ``` ```diff - results = MyApp.Cache.all(my_query) + results = MyApp.Cache.get_all!(query: my_query) ``` ```diff - results = MyApp.Cache.all(nil, return: :value) + results = MyApp.Cache.get_all!(select: :value) ``` -------------------------------- ### Configure Telemetry for Cache Stats Source: https://github.com/elixir-nebulex/nebulex/blob/main/guides/learning/info-api.md Sets up the Telemetry supervisor to report cache statistics and memory usage periodically. Requires adding MyApp.Telemetry to your application's supervision tree. ```elixir defmodule MyApp.Telemetry do use Supervisor import Telemetry.Metrics def start_link(arg) do Supervisor.start_link(__MODULE__, arg, name: __MODULE__) end def init(_arg) do children = [ # Configure `:telemetry_poller` for reporting the cache stats {:telemetry_poller, measurements: periodic_measurements(), period: 10_000}, # For example, we use the console reporter, but you can change it. # See `:telemetry_metrics` for more information. {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} ] Supervisor.init(children, strategy: :one_for_one) end defp metrics do [ # Stats last_value("my_app.cache.info.stats.hits", tags: [:cache]), last_value("my_app.cache.info.stats.misses", tags: [:cache]), last_value("my_app.cache.info.stats.writes", tags: [:cache]), last_value("my_app.cache.info.stats.evictions", tags: [:cache]), # Memory last_value("my_app.cache.info.memory.used", tags: [:cache]), last_value("my_app.cache.info.memory.total", tags: [:cache]) ] end defp periodic_measurements do [ {__MODULE__, :cache_stats, []}, {__MODULE__, :cache_memory, []} ] end def cache_stats do with {:ok, info} <- MyApp.Cache.info([:server, :stats]) do :telemetry.execute( [:my_app, :cache, :info, :stats], info.stats, %{cache: info.server[:cache_name]} ) end :ok end def cache_memory do with {:ok, info} <- MyApp.Cache.info([:server, :memory]) do :telemetry.execute( [:my_app, :cache, :info, :memory], info.memory, %{cache: info.server[:cache_name]} ) end :ok end end ```