Nebulex v3 has several breaking changes, including in the Cache API itself. This guide covers the changes most likely to affect your application code. It does not list every change, and it is not a line-by-line translation guide; it points you to the areas where you should read the new documentation.


Built-In Adapters

All previously built-in adapters (Nebulex.Adapters.Local, Nebulex.Adapters.Partitioned, Nebulex.Adapters.Replicated, and Nebulex.Adapters.Multilevel) have been moved to separate repositories. Therefore, you must add the adapter dependency to the list of dependencies in your mix.exs file.

For example, if you are using the local adapter:

defp deps do
  [
    {:nebulex, "~> 3.0"},
    {:nebulex_local, "~> 3.0"},
    ...
  ]
end

Update Adapter Compile-Time Options

Some adapters support compile-time options like :primary_storage_adapter for configuring the underlying storage adapter. In v3, these options must be wrapped in the :adapter_opts keyword list.

For example, if you're using Nebulex.Adapters.Partitioned with a custom primary storage adapter:

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

Update Cache API Calls

The most significant change is in the Cache API. Nebulex v3 has a new API based on ok/error tuples.

Nebulex v3 brings a new API with two flavors:

  • An ok/error tuple API for all cache functions. Use it when you want to handle the different outcomes with pattern matching.
  • The same functions with a trailing bang (!). Use them when you expect the call to succeed and want it to raise otherwise.

So there are two ways to update your code:

  1. Review each cache call and handle the new ok/error tuple response.
  2. Replace each cache call with its trailing-bang (!) version.

Let's look at both.

Using the Ok/Error Tuple API

As mentioned above, all the Cache API callbacks can return :ok or {:ok, result} on success, or {:error, reason} otherwise. Hence, you have three options:

  • Ignore the result. This only works where the result doesn't matter; when you fetch a key you need its value, so you can't ignore it there.
  • Pattern-match the success case. If you go this way, consider the trailing-bang (!) functions in the next section instead.
  • Handle the success and error cases explicitly (the preferred option).

If you go for the first option of ignoring the result, you can replace your cache calls like this:

- :ok = MyApp.Cache.put("key", "value")
+ _ignore = MyApp.Cache.put("key", "value")

On the other hand, if you go for option two to pattern-match the success case:

- value = MyApp.Cache.get("key")
+ {:ok, value} =  MyApp.Cache.get("key")

Finally, if you go for the third option, update the cache calls to handle the success and error cases:

- :ok = MyApp.Cache.put("key", "value")
- value = MyApp.Cache.get("key")

New code may look like this:

case MyApp.Cache.put("key", "value") do
  :ok ->
    #=> your logic handling success

  {:error, reason} ->
    #=> your logic handling the error
end

case MyApp.Cache.fetch("key") do
  {:ok, value} ->
    #=> your logic handling success

  {:error, %Nebulex.KeyError{}} ->
    #=> your logic handling when the key is not found

  {:error, reason} ->
    #=> your logic handling other errors
end

Notice that the fetch function was introduced in v3. However, the get function is still available, but it has slightly different semantics. See the Cache API docs for more information.

Migrating to ok/error tuple API

You can apply the same idea in the examples above to migrate ALL the Cache API calls.

Using API version with trailing bang (!) functions

The second way to address the API changes (and perhaps the easiest one) is to replace your cache calls by using the function version with the trailing bang (!). For example:

- :ok = MyApp.Cache.put("key", "value")
+ :ok = MyApp.Cache.put!("key", "value")

Bang functions cover most cases, but a few calls behave differently or have no bang version. The next sections cover those.

Update get calls

get/2 changed a bit beyond the ok/error tuple. It used to return nil when the key wasn't in the cache; now it takes a default value to return in that case (nil by default).

The quickest way to update get calls is the trailing-bang version:

- value = MyApp.Cache.get("key")
+ value = MyApp.Cache.get!("key")

Other alternatives:

# Using the default
value = MyApp.Cache.get!("key", "default")

# 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

Update has_key? calls

The new API does not have a "trailing bang function (!)" version for the has_key? callback. Therefore, you must work with the ok/error tuple API. For example:

- bool = MyApp.Cache.has_key?("key")
+ {:ok, bool} = MyApp.Cache.has_key?("key")

Update Transaction API calls

The new API does not have a "trailing bang function (!)" version for the Transaction API callbacks. Therefore, you must work with the ok/error tuple API. For example:

- bool = MyApp.Cache.in_transaction?()
+ {:ok, bool} = MyApp.Cache.in_transaction?()

- value = MyApp.Cache.transaction(fn -> MyApp.Cache.get("key") end)
+ {:ok, value} = MyApp.Cache.transaction(fn -> MyApp.Cache.get!("key") end)

Adapter Transaction Implementation

Since v3.0.0, the default transaction implementation that Nebulex.Adapter.Transaction used to provide is gone. The behaviour itself remains; each adapter implements it according to its topology and locking strategy (for example, local ETS-based locks or distributed locks).

If your adapter does not implement transactions, review your usage of transaction/2 and related callbacks, and adapt your code accordingly.

Update flush calls

The flush callback was removed; use delete_all instead:

- MyApp.Cache.flush()
+ MyApp.Cache.delete_all()

Storing nil values

Nebulex used to skip storing nil values, because nil was how it told a missing key apart from a present one. That was a limitation.

Since v3, any Elixir term can be stored, including nil; Nebulex does no validation. What nil (or any other term) means is up to you.

v3 also adds fetch/2, the base function for reading a key; get is implemented on top of it.

Removed Stats API

The Nebulex.Adapter.Stats behaviour was removed. Therefore, the callbacks stats/0 and dispatch_stats/1 are no longer available and must be removed from your code.

As a quick fix, you can update the stats calls using the new Info API, like this:

- :ok = MyApp.Cache.stats()
+ stats = MyApp.Cache.info!(:stats)

The Info API, new in v3, is a more general way to get information about the cache, including stats. Each adapter implements it and chooses which information keys it returns. See Nebulex.Cache.info/2 and the "Cache Info Guide".

Removed Persistence API

Persistence can be done in many ways depending on the use case, and a single built-in implementation would not cover them. For example, an application on AWS may want S3 as persistent storage rather than the local file system. Since v3, persistence is an add-on provided by the backend (e.g., Redis), another library, or the application itself, and configured through the adapter's configuration or the backend directly.

Therefore, the Nebulex.Adapter.Persistence behaviour was removed, so the callbacks dump/2 and load/2 are no longer available and must be removed from your code.

- MyApp.Cache.dump("my_backup")
- MyApp.Cache.load("my_backup")

Update Query API calls

The old Query API received as an argument the query to run against the adapter or backend, which could be any Elixir term (based on the adapter). Now, the Query API expects a "Query Spec" as an argument to work.

Updating all calls

The callback all/2 was merged into get_all/2; only the latter can be used now. The new version of get_all/2 accepts a query-spec as a first argument.

Instead of using all, you'll call get_all:

- results = MyApp.Cache.all()
+ results = MyApp.Cache.get_all!()

Passing a query as an argument:

- results = MyApp.Cache.all(my_query)
+ results = MyApp.Cache.get_all!(query: my_query)

Besides, the option :return was removed; passing it now raises a NimbleOptions.ValidationError. Use the :select option of the query-spec instead. For example:

- results = MyApp.Cache.all(nil, return: :value)
+ results = MyApp.Cache.get_all!(select: :value)

stream function

The same changes and examples apply to the stream function.

Updating get_all calls

Similarly, you must update the get_all calls:

- results = MyApp.Cache.get_all([k1, k2, ...])
+ results = MyApp.Cache.get_all!(in: [k1, k2, ...])

Updating count_all and delete_all calls

Similarly to get_all, you must use a query-spec as the first argument when calling count_all or delete_all.

- count = MyApp.Cache.count_all()
+ count = MyApp.Cache.count_all!()

- count = MyApp.Cache.delete_all()
+ count = MyApp.Cache.delete_all!()

- count = MyApp.Cache.delete_all(my_query)
+ count = MyApp.Cache.delete_all!(query: my_query)

See Cache API Docs for more information and examples.

New read-through helpers

After migration, consider using fetch_or_store/3 and get_or_store/3 for common read-through caching patterns.

Update caching decorators

Nebulex v3 changes the declarative caching API (the caching decorators) and adds new features. This section covers the changes and a few of the new features; see the Nebulex.Caching docs for the rest.

Update :cache option

The :cache option doesn't support MFA tuples anymore. The possible values are a cache module, a dynamic cache spec, or an anonymous function that optionally receives the decorator's context as an argument and must return the cache to use (a cache module or a dynamic cache spec).

- @decorate cacheable(cache: {MyApp.Caching, :get_cache, []})
+ @decorate cacheable(cache: &MyApp.Caching.get_cache/1)

Optionally, if you don't need the decorator context to compute the cache, use a 0-arity function instead:

- @decorate cacheable(cache: {MyApp.Caching, :get_cache, []})
+ @decorate cacheable(cache: &MyApp.Caching.get_cache/0)

The get_cache function may look like this:

defmodule MyApp.Caching do
  alias Nebulex.Caching.Decorators.Context

  def get_cache(%Context{} = context) do
    #=> return the cache
  end

  # Without the context
  def get_cache do
    #=> return the cache
  end
end

Update :keys option

The :keys option is no longer supported. Use :key instead:

- @decorate cacheable(cache: MyApp.Cache, keys: ["foo", "bar"])
+ @decorate cacheable(cache: MyApp.Cache, key: {:in, ["foo", "bar"]})

Update :key_generator option

The :key_generator option is no longer supported. Use :key with an anonymous function instead. The function optionally receives the decorator context and must return the key to use.

- @decorate cacheable(cache: MyApp.Cache, key_generator: CustomKeyGenerator)
+ @decorate cacheable(cache: MyApp.Cache, key: &MyApp.Caching.compute_key/1)

Optionally, if you don't need the decorator context to compute the key, use a 0-arity function instead:

- @decorate cacheable(cache: MyApp.Cache, key_generator: CustomKeyGenerator)
+ @decorate cacheable(cache: MyApp.Cache, key: &MyApp.Caching.compute_key/0)

The compute_key function may look like this:

defmodule MyApp.Caching do
  alias Nebulex.Caching.Decorators.Context

  def compute_key(%Context{} = context) do
    #=> return the key
  end

  # Without the context
  def compute_key do
    #=> return the key
  end
end

Update :default_key_generator option

The Nebulex.Caching.KeyGenerator behaviour was removed. You can use an anonymous function for the :default_key_generator option instead (the function must be provided in the format &Mod.fun/arity). Besides, the :default_key_generator option must be provided to use Nebulex.Caching.

First, remove :default_key_generator option from all places it is used:

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

  ...
end

Then, add it to the modules using use Nebulex.Caching:

defmodule MyApp.Books do
  use Nebulex.Caching,
+   default_key_generator: &MyApp.Keygen.generate/1

  ...
end

Global options

Repeating options like :cache on every decorator gets verbose, so some of the common options can be set once in use Nebulex.Caching.

You can update your modules like this:

defmodule MyApp.Books do
  use Nebulex.Caching,
+   cache: MyApp.Cache,
+   match: &__MODULE__.match/1,
+   on_error: :raise

- @decorate cacheable(cache: MyApp.Cache, key: id, match: &match/1, on_error: :raise)
+ @decorate cacheable(key: id)
  def get_book(id) do
    # ... logic to retrieve a book
  end

- @decorate cache_put(cache: MyApp.Cache, key: book.id, match: &match/1, on_error: :raise)
+ @decorate cache_put(key: book.id)
  def update_book(book, update_attrs) do
    # ... logic to update a book
  end

- @decorate cache_evict(cache: MyApp.Cache, key: book.id, on_error: :raise)
+ @decorate cache_evict(key: book.id)
  def delete_book(book) do
    # ... logic to delete a book
  end

  ...
end

See Nebulex.Caching options for more information.

Dynamic caches

The :cache option accepts a dynamic cache, built with dynamic_cache/2. For example:

defmodule MyApp.Books do
  use Nebulex.Caching

  @decorate cacheable(cache: dynamic_cache(MyApp.Cache, :books_cache))
  def find_book(isbn) do
    # your logic ...
  end
end

See "Caching Decorators" for more information.