View Source Nebulex.Adapters.Multilevel (Nebulex v2.6.4)

Adapter module for Multi-level Cache.

This is just a simple layer on top of local or distributed cache implementations that enables to have 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,
  model: :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:

  • :levels - This option is to define the levels, a list of tuples {cache_level :: Nebulex.Cache.t(), opts :: Keyword.t()}, where the first element is the module that defines the cache for that level, and the second one is the options that will be passed to that level in the start/link/1 (which depends on the adapter this level is using). The order in which the levels are defined is the same the multi-level cache will use. For example, the first cache in the list will be the L1 cache (level 1) and so on; the Nth element will be the LN cache. This option is mandatory, if it is not set or empty, an exception will be raised.

  • :model - Specifies the cache model: :inclusive or :exclusive; defaults to :inclusive. In an inclusive cache, the same data can be present in all caches/levels. In an exclusive cache, data can be present in only one cache/level and a key cannot be found in the rest of caches at the same time. This option applies to the get callback only; if the cache :model is :inclusive, when the key is found in a level N, that entry is duplicated backwards (to all previous levels: 1..N-1). However, when the mode is set to :inclusive, the get_all operation is translated into multiple get calls underneath (which may be a significant performance penalty) since is required to replicate the entries properly with their current TTLs. It is possible to skip the replication when calling get_all using the option :replicate.

  • :replicate - This option applies only to the get_all callback. Determines whether the entries should be replicated to the backward levels or not. Defaults to true.

Shared options

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

  • :level - It may be an integer greater than 0 that specifies the cache level where the operation will take place. By default, the evaluation is performed throughout the whole cache hierarchy (all levels).

Telemetry events

This adapter emits all recommended Telemetry events, and documented in Nebulex.Cache module (see "Adapter-specific events" section).

Since the multi-level adapter is a layer/wrapper on top of other existing adapters, each cache level may Telemetry emit events independently. For example, for the cache defined before MyApp.Multilevel, the next events will be emitted for the main multi-level cache:

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

For the L1 (configured with the local adapter):

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

For the L2 (configured with the partitioned adapter):

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

See also the Telemetry guide for more information and examples.

Stats

Since the multi-level adapter works as a wrapper for the configured cache levels, the support for stats depends on the underlying levels. Also, the measurements are consolidated per level, they are not aggregated. For example, if we enable the stats for the multi-level cache defined previously and run:

MyApp.Multilevel.stats()

The returned stats will look like:

%Nebulex.Stats{
  measurements: %{
    l1: %{evictions: 0, expirations: 0, hits: 0, misses: 0, writes: 0},
    l2: %{evictions: 0, expirations: 0, hits: 0, misses: 0, writes: 0}
  },
  metadata: %{
    l1: %{
      cache: NMyApp.Multilevel.L1,
      started_at: ~U[2021-01-10 13:06:04.075084Z]
    },
    l2: %{
      cache: MyApp.Multilevel.L2.Primary,
      started_at: ~U[2021-01-10 13:06:04.089888Z]
    },
    cache: MyApp.Multilevel,
    started_at: ~U[2021-01-10 13:06:04.066750Z]
  }
}

IMPORTANT: Those cache levels with stats disabled won't be included into the returned stats (they are skipped). If a cache level is using an adapter that does not support stats, you may get unexpected errors. Therefore, and as overall recommendation, check out the documentation for adapters used by the underlying cache levels and ensure they implement the Nebulex.Adapter.Stats behaviour.

Stats with Telemetry

In case you are using Telemetry metrics, you can define the metrics per level, for example:

last_value("nebulex.cache.stats.l1.hits",
  event_name: "nebulex.cache.stats",
  measurement: &get_in(&1, [:l1, :hits]),
  tags: [:cache]
)
last_value("nebulex.cache.stats.l1.misses",
  event_name: "nebulex.cache.stats",
  measurement: &get_in(&1, [:l1, :misses]),
  tags: [:cache]
)

See the section "Instrumenting Multi-level caches" in the Telemetry guide for more information.

Extended API

This adapter provides one additional convenience function for retrieving the cache model for the given cache name:

MyCache.model()
MyCache.model(:cache_name)

Caveats of multi-level adapter

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.