Skip to content

Repository files navigation

ElixirCache

Hex version badgeTestCredoDialyzerCoverage

The goal of this project is to unify Cache APIs and make Strategies easy to implement and sharable across all storage types/adapters

The second goal is to make sure testing of all cache related funciions is easy, meaning caches should be isolated per test and not leak their state to outside tests

Installation

The package can be installed by adding elixir_cache to your list of dependencies in mix.exs:

defdepsdo[{:elixir_cache,"~> 0.1.0"}]end

The docs can be found at https://hexdocs.pm/elixir_cache.

Usage

defmoduleMyModuledouseCache,adapter: Cache.Redis,name: :my_name,sandbox?: Mix.env()===:test,opts: [...opts]end

In our application.ex

children=[{Cache,[MyModule]}]

Now we can use MyModule to call various Cache apis

MyModule.get("key")#> {:ok, nil}MyModule.put("key","value")#> :okMyModule.get("key")#> {:ok, "value"}

Compression

Values are serialised with :erlang.term_to_binary/1 for adapters that cannot hold an Erlang term. Pass a compression_level (1..9) to compress them on the way in:

defmoduleMyModuledouseCache,adapter: Cache.Redis,name: :my_name,compression_level: 6,opts: [...opts]end

The level belongs to the encoder rather than to the store, so it is never handed to the adapter. Setting it also forces encoding on adapters that would otherwise hold the term as it is (Cache.ETS, Cache.Agent, Cache.PersistentTerm, Cache.ConCache) — asking for compression is asking for bytes. It is not supported on a cache using a strategy adapter, which encodes through what it wraps; set it there instead.

Adapters

  • Cache.Agent - Simple agent based caching
  • Cache.DETS - Disk persisted caching with dets
  • Cache.ETS - Super quick in-memory cache with ets
  • Cache.Redis - Caching adapter using Redix & Poolboy, supports Redis JSON and Redis Hashes
  • Cache.ConCache - Wrapper around ConCache library
  • Cache.PersistentTerm - Zero-latency reads for rarely-written data via Erlang's :persistent_term
  • Cache.Counter - Lock-free atomic integer counters via Erlang's :counters module

Adapter Specific Functions

Some adapters have specific functions such as redis which has hash functions and pipeline functions to make calls easier.

These adapters when used will add extra commands to your cache module.

Cache.Counter adds increment/1,2 and decrement/1,2 to your module:

defmoduleMyApp.CounterCachedouseCache,adapter: Cache.Counter,name: :my_app_counter_cache,opts: []endMyApp.CounterCache.increment(:page_views)MyApp.CounterCache.decrement(:active_users){:ok,count}=MyApp.CounterCache.get(:page_views)

Strategy Adapters

Strategy adapters compose over existing adapters to provide higher-level caching patterns. Use a two-element tuple as the adapter option:

useCache,adapter: {StrategyModule,UnderlyingAdapterOrConfig},name: :my_cache,opts: [strategy_opt: value,underlying_adapter_opt: value]
  • Cache.HashRing - Distribute keys across Erlang cluster nodes via consistent hashing (libring)
  • Cache.MultiLayer - Cascade reads/writes through multiple cache layers (e.g. ETS → Redis) with automatic backfill
  • Cache.RefreshAhead - Proactively refresh hot keys in the background before they expire

See the Using Strategy Adapters guide for full details.

Sandboxing

Our cache config accepts a sandbox?: boolean. In sandbox mode, the Cache.Sandbox adapter will be used, which is just a simple Agent cache unique to the root process. The Cache.SandboxRegistry is responsible for registering test processes to a unique instance of the Sandbox adapter cache. This makes it safe in test mode to run all your tests asynchronously!

For test isolation via the Cache.SandboxRegistry to work, you must start the registry in your test/test_helper.exs:

Cache.SandboxRegistry.start_link()ExUnit.start()

Then inside a setup block:

Cache.SandboxRegistry.start([MyCache,CacheItem])

Cache.CaseTemplate

For applications with many test files, use Cache.CaseTemplate to define a single CacheCase module that automatically starts sandboxed caches in every test that uses it.

Create a CacheCase module in your test support directory:

# test/support/cache_case.exdefmoduleMyApp.CacheCasedouseCache.CaseTemplate,default_caches: [MyApp.UserCache,MyApp.SessionCache]end

Or discover caches automatically from a running supervisor:

defmoduleMyApp.CacheCasedouseCache.CaseTemplate,supervisors: [MyApp.Supervisor]end

Then use it in any test file:

defmoduleMyApp.SomeTestdouseExUnit.Case,async: trueuseMyApp.CacheCase# optionally add extra caches just for this file:# use MyApp.CacheCase, caches: [MyApp.ExtraCache]end

Creating Adapters

Adapters are very easy to create in this model and are basically just a module that implement the @behaviour Cache

This behaviour adds the following callbacks

put(cache_name, key, ttl, value, opts \\ [])
get(cache_name, key, opts \\ [])
delete(cache_name, key, opts \\ [])
opts_definition() # NimbleOptions definition map
child_spec({cache_name, cache_opts})

Cache.ETS is probably the easiest adapter to follow as a guide as it's a simple Task

Runtime Configuration

Adapter configuration can also be specified at runtime. These options are first passed to the adapter child_spec when starting the adapter and then passed to all runtime function calls.

For example:

# Configure with Module FunctiondefmoduleCache.ExampledouseCache,adapter: Cache.Redis,name: :test_cache_redis,opts: {Cache.Example,:opts,[]}defopts,do: [host: "localhost",port: 6379]end# Configure with callback functiondefmoduleCache.ExampledouseCache,adapter: Cache.Redis,name: :test_cache_redis,opts: &Cache.Example.opts/0defopts,do: [host: "localhost",port: 6379]end# Fetch from application config# config :elixir_cache, Cache.Example, []defmoduleCache.ExampledouseCache,adapter: Cache.Redis,name: :test_cache_redis,opts: :elixir_cacheend# Fetch from application config# config :elixir_cache, :cache_opts, []defmoduleCache.ExampledouseCache,adapter: Cache.Redis,name: :test_cache_redis,opts: {:elixir_cache,:cache_opts}end

Runtime options can be configured in one of the following formats:

  • {module, function, args} - Module, function, args
  • {application_name, key} - Application name. This is called as Application.fetch_env!(application_name, key).
  • application_name - Application name as an atom. This is called as Application.fetch_env!(application_name, cache_module}).
  • function - Zero arity callback function. For eg. &YourModule.options/0
  • [key: value_type] - Keyword list of options.

About

Caching in elixir with testing as a first priority, don't let caching bog down your tests!

Resources

Stars

16 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages