Upgrading to v3.0

View Source

For the v3, Nebulex introduces several breaking changes, including the Cache API itself. This guide aims to highlight most of these changes to make easier the transition to v3. Be aware this guide won't focus on every change, just the most significant ones that can affect how your application code interacts with the cache. Also, it is not a detailed guide about how to translate the current code from older versions to v3, just pointing out the areas the new documentation should be consulted on.

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 Cache API calls

The most significant change is on 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. This new approach is preferred when you want to handle different outcomes using pattern-matching.
  • An alternative API version with trailing bang (!) functions. This approach is preferred if you expect the outcome to always be successful.

Therefore, there are two ways to address the API changes. The first is to review your code's cache calls and handle the new ok/error tuple response. The second is to replace your cache calls using the function version with the trailing bang (!). But let's take a closer look at them.

Using 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. However, there will be some cases where you don't want (or can't) ignore the result. For example, when fetching a key, it is intended to use its value, hence, it doesn't make sense to ignore the result.
  • Pattern-match the success case. If you decide to take this path, perhaps you should consider using the trailing bang (!) functions mentioned in the next section.
  • 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 since 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")

Despite this fix of using bang functions (!) may work for most cases, there may be a few where the outcome is not the same or the patch is just not applicable. See the next sections to learn more about it.

Update get calls

The previous callback get/2 has changed the semantics a bit (aside from the ok/error tuple API). Previously, returned nil when the given key wasn't in the cache. Now, the callback accepts an argument to specify the default value when the key is not found (defaults to nil).

The easiest and quickest way to update the get calls is using the new version of it with the trailing bang, like so:

- 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)

Update flush calls

The callback flush is deprecated, you should use delete_all instead:

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

Storing nil values

Previously, Nebulex used to skip storing nil values in the cache. The main reason was the semantics assumed for the nil value, being used to validate whether a key existed in the cache or not. However, this could be a limitation.

Since Nebulex v3, any Elixir term can be stored in the cache (including nil), Nebulex doesn't perform any validation whatsoever. Any meaning or semantics behind nil (or any other term) is up to the user.

Additionally, a new callback fetch/2 is introduced, which is the base or main function for retrieving a key from the cache; in fact, the get callback is implemented using fetch underneath.

Deprecated Stats API

The Nebulex.Adapter.Stats behaviour has been deprecated. 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)

Since Nebulex v3, the adapter's Info API is introduced. This is a more generic API to get information about the cache, including the stats. Adapters are responsible for implementing the Info API and are also free to add the information specification keys they want. See c:Nebulex.Cache.info/2 and the "Cache Info Guide" for more information.

Deprecated Persistence API

Persistence can be implemented in many different ways depending on the use case. A persistence API with a specific implementation may not be very useful because it won't cover all possible use cases. For example, if your application is on AWS, perhaps it makes more sense to use S3 as persistent storage, not the local file system. Since Nebulex v3, persistence is an add-on that can be provided by the backend (e.g., Redis), another library, or the application itself. And it should be configured via the adapter's configuration (or maybe directly with the backend).

Therefore, the Nebulex.Adapter.Persistence behaviour has been deprecated, 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 is deprecated. You may consider using the :select option of the query-spec. 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.

Update caching decorators

Nebulex v3 introduces some changes and new features to the Declarative Caching API (a.k.a caching decorators). We will highlight mostly the changes and perhaps a few new features. However, it is highly recommended you check the documentation for more information about all the new features and changes.

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 option :keys is deprecated. Instead, consider using the option :key like this:

- @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 deprecated. Instead, you can use the :key option with an anonymous function that optionally receives the decorator's context as an argument 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 is deprecated. 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

Decorators may become verbose sometimes, since you have to provide options like the :cache on each decorator definition. Therefore, Nebulex supports adding some of the common options globally when using use Nebulex.Caching.

You can update your modules using declarative caching 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.

:references option and dynamic caches

The option :references in the cacheable decorator supports referencing a dynamic cache. 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.