Cachex with Phoenix

A tutorial on how to setup Cachex within your Phoenix application. This tutorial walks through setting up a basic cache for some expensive queries.

Cachex is an extremely fast in-memory key/value store with support for many useful features. You can read the documentation to get into the details of all the features.

This blog post will walk through setting up a simple cache for a Phoenix project. This example works on Elixir 1.8 and greater and Phoenix 1.4 and greater.

Installing the Dependencies

Add this at the mix.exs file

# mix.exs

  defp deps do
    [
       ...
      {:cachex, "~> 3.2"}
    ]
  end

Now we are going to add a worker.

Edit your application file at lib/yourapp/application.ex and add the following:

Don’t forget to add the comma before you paste your code in or you will get a syntax error before: worker. This means your missing a comma.

Just make sure it looks like what I have outlined below.

defmodule YourApp.Application do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec 
    children = [
      # Start the Ecto repository
      YourApp.Repo,
      # Start the endpoint when the application starts
      YourAppWeb.Endpoint,
      # Worker
      worker(Cachex, [ :my_cache, [] ]) 
    ]
    opts = [strategy: :one_for_one, name: YourApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

end

Now run in the console mix deps.get.

~/Desktop/your_app_name > mix deps.get
...
New:
  cachex 3.2.0
  eternal 1.2.1
  jumper 1.0.1
  sleeplocks 1.1.1
  unsafe 1.0.1
* Getting cachex (Hex package)
* Getting eternal (Hex package)
* Getting jumper (Hex package)
* Getting sleeplocks (Hex package)
* Getting unsafe (Hex package)

Restart the server if it was running before.

To verify that Cachex is working you can always run iex -S mix and then type in Cachex. and then tab. You should see the following in your tab.

iex(1)> Cachex.
Actions             Application         Disk                
Errors              ExecutionError      Hook                
Options             Policy              Query               
Router              Services            Spec                
Stats               Warmer              child_spec/1        
clear/1             clear/2             count/1             
count/2             decr/2              decr/3              
decr/4              del/2               del/3               
dump/2              dump/3              empty?/1            
empty?/2            execute/2           execute/3           
exists?/2           exists?/3           expire/3            
expire/4            expire_at/3         expire_at/4         
export/1            export/2            fetch/2             
fetch/3             fetch/4             get/2               
get/3               get_and_update/3    get_and_update/4    
import/2            import/3            incr/2              
incr/3              incr/4              inspect/2           
inspect/3           invoke/3            invoke/4            
keys/1              keys/2              load/2              
load/3              persist/2           persist/3           
purge/1             purge/2             put/3               
put/4               put_many/2          put_many/3          
refresh/2           refresh/3           reset/1             
reset/2             set/3               set/4               
set_many/2          set_many/3          size/1              
size/2              start/1             start/2             
start_link/1        start_link/2        stats/1             
stats/2             stream/1            stream/2            
stream/3            take/2              take/3              
touch/2             touch/3             transaction/3       
transaction/4       ttl/2               ttl/3               
update/3            update/4    

After the setup

The name :my_cache is just a name to access the cache. You can name it whatever you would like. For demo purposes this is what I called it.

Creating Cache Warmer

Now what we want is the ability to create a cache when we first launch the application. In this example we are going to use create a module that is a GenServer and fire off a function that will create our cache.

Step 1

Create a module called CacheWarmer and follow the details in the example below. This module will have one job and that will be to fire a function that will prepopulate our cache after 1 second on inital boot up.

defmodule YourApp.CacheWarmer do
    use GenServer
  
    def start_link, do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
  
    def init(_) do
      start_timer()
      {:ok, nil}
    end
  
    def handle_info(:warm, _state) do
      warm_up_cache()
      {:noreply, nil}
    end
  
    def start_timer, do: Process.send_after(self(), :warm, 1000) # send after 1 sec

    defp warm_up_cache() do
        Enum.each(["tony", "sam", "angela"], fn(slug) -> 
          result = YourApp.Repo.find_by_slug(slug)
          Cachex.put(:my_cache, slug, result)
        end)
    end

  end

This Module will:

Example

This example is querying the DB by a slug name for some users. We take the result of that querying and then add that to the cache by using the function Cachex.put(:my_cache, slug, result).

The cache follows a key, value look up system.

So in ths example the slug tony is our key. And the value is what we got out the Repo

With that done we can now add this module as a worker within our application.ex.

Worker

The CacheWarmer module needs to be added it as a worker/2 within the start function. To do this we need to go back to our application.ex file and added it as a worker

defmodule YourApp.Application do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec
    children = [
      YourApp.Repo,
      YourAppWeb.Endpoint,
      worker(Cachex, [ :my_cache, [] ]),
      # ADD: Your worker
      worker(YourApp.CacheWarmer, [])
    ]
    opts = [strategy: :one_for_one, name: YourApp.Supervisor]
    Supervisor.start_link(children, opts)
  end

end

Stop the server in the console and then start it up again. If it works you will definitely see some SQL log files getting generated during boot up.

Accessing Cachex

Within your controller you can write the following code for retrieval of your cache

defmodule YourApp.HomeController do
    # omitting some functions for clarity
    def show(conn, %{"slug" => slug}) do
      {:ok, results} = Cachex.get(:my_cache, slug)
      render(conn, "show.html", page_title: "Hello", results: results)
    end 
end

Cachex.get(:my_cache, slug) will return a tuple. Pattern match that to get the results back. Pass that into your template and your done.

Performance Improvement

The performance improvement should be noticable depending on the content. In one of my applications I had a page that would take 150ms to fetch and render. Once I used Cachex I reduced it down to 1ms.

Troubleshooting

Other sources