Nebulex.Adapters.Multilevel (Nebulex.Distributed v3.0.0-rc.1)

View Source

Adapter module for the multi-level cache topogy.

The Multi-level adapter is a simple layer that works on top of a local or distributed cache implementation, enabling a cache hierarchy by levels. Multi-level caches generally operate by checking the fastest, level 1 (L1) cache first; if it hits, the adapter proceeds at high speed. If that first cache misses, the next fastest cache (level 2, L2) is checked, and so on, before accessing external memory (that can be handled by a cacheable decorator).

For write functions, the "Write Through" policy is applied by default; this policy ensures that the data is stored safely as it is written throughout the hierarchy. However, it is possible to force the write operation in a specific level (although it is not recommended) via level option, where the value is a positive integer greater than 0.

We can define a multi-level cache as follows:

defmodule MyApp.Multilevel do
  use Nebulex.Cache,
    otp_app: :nebulex,
    adapter: Nebulex.Adapters.Multilevel

  defmodule L1 do
    use Nebulex.Cache,
      otp_app: :nebulex,
      adapter: Nebulex.Adapters.Local
  end

  defmodule L2 do
    use Nebulex.Cache,
      otp_app: :nebulex,
      adapter: Nebulex.Adapters.Partitioned
  end
end

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

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

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.Multilevel, []},
    ...
  ]

See Nebulex.Cache for more information.

Options

This adapter supports the following options and all of them can be given via the cache configuration:

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

  • :levels (non-empty keyword/0) - Required. This option is to define the levels, a list of tuples in the shape {cache_level :: Nebulex.Cache.t(), opts :: keyword()}, where the first element is the module that defines the cache for that level, and the second one is the options given to that level in the start_link/1. The multi-level cache adapter relies on the list order to determine the level hierarchy. For example, the first element in the list will be the L1 cache (level 1), and so on; the Nth element will be the LN cache. This option is required; if it is not set or empty, the adapter raises an exception.

  • :inclusion_policy - Specifies the cache inclusion policy: :inclusive or :exclusive.

    For an "inclusive" cache, the same data can be present in all cache levels. On the other hand, in an "exclusive" cache, the data can be present in only one cache level; the key cannot exist in the rest of the levels at the same time. This option applies to the callback get only; if the cache inclusion policy is :inclusive, when the key does exist in a level N, that entry is duplicated backward (to all previous levels: 1..N-1). However, when the mode is :inclusive, the get_all operation is translated into multiple get calls underneath (which may be a significant performance penalty) since it requires replicating the entries properly with their current TTLs. It is possible to skip the replication when calling get_all using the option :replicate.

    The default value is :inclusive.

Shared options

Almost all of the cache functions outlined in Nebulex.Cache module accept the following options:

  • :timeout (timeout/0) - The time in milliseconds to wait for a command to finish (:infinity to wait indefinitely). The default value is 5000.

  • :level (pos_integer/0) - Dictates the level where the cache command will take place. The evaluation is performed by default throughout the cache hierarchy (all levels).

Queryable API options

The following options apply to get_all, count_all, delete_all, and stream commands:

  • :replicate (boolean/0) - This option applies only to the get_all callback when using the inclusive policy. Determines whether the entries should be replicated to the backward levels or not. The default value is true.

  • :on_error (:raise | :nothing) - Indicates whether to raise an exception when an error occurs or do nothing (skip errors).

    When the stream is evaluated, the adapter attempts to execute the stream command on the different cache levels. Still, the execution could fail at any of the cache levels. If the option is set to :raise, the command will raise an exception when an error occurs on the stream evaluation. On the other hand, if it is set to :nothing, the error is skipped.

    The default value is :raise.

Telemetry events

Since the multi-level adapter works as a wrapper for the configured cache levels, these will emit the Nebulex Telemetry events. Therefore, there will be events emitted for each cache level. For example, the cache defined before MyApp.Multilevel will emit the following events:

  • Top level cache:

    • [:my_app, :multilevel, :command, :start]
    • [:my_app, :multilevel, :command, :stop]
    • [:my_app, :multilevel, :command, :exception]
  • L1 cache:

    • [:my_app, :multilevel, :command, :start]
    • [:my_app, :multilevel, :command, :stop]
    • [:my_app, :multilevel, :command, :exception]
  • L2 cache:

    • [:my_app, :multilevel, :command, :start]
    • [:my_app, :multilevel, :primary, :command, :start]
    • [:my_app, :multilevel, :command, :stop]
    • [:my_app, :multilevel, :primary, :command, :stop]
    • [:my_app, :multilevel, :command, :exception]
    • [:my_app, :multilevel, :primary, :command, :exception]

As you may notice, the telemetry prefix by default for all cache levels is [:my_app, :multilevel], but you can get the details about the cache from the metadata. Alternatively, you could specify the :telemetry_prefix for each cache level within the :levels option. For example:

config :my_app, MyApp.Multilevel,
  levels: [
    {MyApp.Multilevel.L1, telemetry_prefix: [:my_app, :multilevel, :l1]},
    {MyApp.Multilevel.L2, telemetry_prefix: [:my_app, :multilevel, :l2]}
  ]

In this case, the telemetry prefix for the L1 cache will be [:my_app, :multilevel, :l1] and the L2 cache will be [:my_app, :multilevel, :l2].

See also the Telemetry guide for more information.

Info API

As explained above, the multi-level adapter uses the configured cache levels. Therefore, the information provided by the info command will depend on the adapters configured for each level. The Nebulex built-in adapters support the recommended keys :server, :memory, and :stats. Additionally, the multi-level adapter supports:

  • :levels_info - A list with the info map for each cache level.

For example, the info for MyApp.Multilevel may look like this:

iex> MyApp.Multilevel.info!()
%{
  memory: %{total: nil, used: 206760},
  server: %{
    cache_module: MyApp.Multilevel,
    cache_name: :multilevel_inclusive,
    cache_adapter: Nebulex.Adapters.Multilevel,
    cache_pid: #PID<0.998.0>,
    nbx_version: "3.0.0"
  },
  stats: %{
    hits: 0,
    misses: 0,
    writes: 0,
    evictions: 0,
    expirations: 0,
    deletions: 0,
    updates: 0
  },
  levels_info: [
    %{
      memory: %{total: nil, used: 68920},
      server: %{
        cache_module: MyApp.Multilevel.L1,
        cache_name: MyApp.Multilevel.L1,
        cache_adapter: Nebulex.Adapters.Local,
        cache_pid: #PID<0.1000.0>,
        nbx_version: "3.0.0"
      },
      stats: %{
        hits: 0,
        misses: 0,
        writes: 0,
        evictions: 0,
        expirations: 0,
        deletions: 0,
        updates: 0
      }
    },
    %{
      memory: %{total: nil, used: 68920},
      nodes: [:"node1@127.0.0.1"],
      server: %{
        cache_module: MyApp.Multilevel.L2,
        cache_name: MyApp.Multilevel.L2,
        cache_adapter: Nebulex.Adapters.Partitioned,
        cache_pid: #PID<0.1015.0>,
        nbx_version: "3.0.0"
      },
      stats: %{
        hits: 0,
        misses: 0,
        writes: 0,
        evictions: 0,
        expirations: 0,
        deletions: 0,
        updates: 0
      },
      nodes_info: %{
        "node1@127.0.0.1": %{
          memory: %{total: nil, used: 68920},
          server: %{
            cache_module: MyApp.Multilevel.L2.Primary,
            cache_name: MyApp.Multilevel.L2.Primary,
            cache_adapter: Nebulex.Adapters.Local,
            cache_pid: #PID<0.1017.0>,
            nbx_version: "3.0.0"
          },
          stats: %{
            hits: 0,
            misses: 0,
            writes: 0,
            evictions: 0,
            expirations: 0,
            deletions: 0,
            updates: 0
          }
        }
      }
    }
  ]
}

Extended API

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

inclusion_policy/0,1

Returns the inclusion policy of the cache.

iex> MyCache.inclusion_policy()
:inclusive

CAVEATS

Because this adapter reuses other existing/configured adapters, it inherits all their limitations too. Therefore, it is highly recommended to check the documentation of the adapters to use.