fireproofsocks

fireproofsocks

Periodically clearing out values store in a process? (i.e. enforcing TTL, e.g. in an Agent)

I’m trying to learn more about use processes to store state (e.g. following this page), and I’m wondering how one might implement a TTL in a value. For example, if I wanted to cache a web request for a short period of time specified at runtime, what strategies could be employed? Sorry, I know this is kind of open ended…

Marked As Solved

lucaong

lucaong

Here is a minimal example of the first strategy. It does not handle situations like setting the same key more than once, but it should give you some inspiration:

defmodule Cache do
  use GenServer

  def start_link() do
    GenServer.start_link(__MODULE__, [], [])
  end

  def put(pid, key, value, ttl \\ 10_000) do
    GenServer.call(pid, {:put, key, value, ttl})
  end

  def get(pid, key) do
    GenServer.call(pid, {:get, key})
  end

  # GenServer callbacks

  def init(_) do
    state = %{}
    {:ok, state}
  end

  def handle_call({:put, key, value, ttl}, _from, state) do
    Process.send_after(self(), {:expire, key}, ttl)
    {:reply, :ok, Map.put(state, key, value)}
  end

  def handle_call({:get, key}, _from, state) do
    {:reply, Map.get(state, key), state}
  end

  def handle_info({:expire, key}, state) do
    {:noreply, Map.delete(state, key)}
  end
end

You can verify in the console that the TTL works:

{:ok, pid} = Cache.start_link()

# Store something in the cache
Cache.put(pid, :foo, 123)
#=> :ok

# Get the stored value:
Cache.get(pid, :foo)
#=> 123

# Wait more than 10 seconds, then try again:
Cache.get(pid, :foo)
#=> nil

Also Liked

lucaong

lucaong

One common pattern to implement TTL is the following: the process that maintains the state (usually a GenServer) upon setting some data in the state would also send a delayed message to itself with Process.send_after/4 with a timeout equal to the TTL. The delayed message would contain some reference to the data to expire (for example the key, if it is some sort of key/value cache). Upon receiving the message, the GenServer would clear the expired data.

Another common pattern is to invalidate the cache lazily: the cached data would contain a timestamp, and upon retrieval the process would check if the cached value is stale, and if so evict it and consider it a cache miss. This would mean that a value could stay in cache forever if it’s never requested, so often one would implement some sort of fixed-capacity cache (like an LRU cache), or periodically clean up the expired values by enumerating or sampling the entries.

shanesveller

shanesveller

Along with the the start_link alternative Luca presented, if you need more than one running copy of a process within a single BEAM node that you can still reference from elsewhere “by name”, take a look at stdlib’s Registry and via-tuples.

From there, another refinement of Luca’s later suggestions would be to brush up on child specifications and child_spec/1 which give you finer control over the relationship between “how I supervise this process” and “how it receives arguments”. You’re not limited to a single catch-all argument if you don’t want to be or if it doesn’t make sense for your purposes. This technique translates to many OTP process types and isn’t limited to just GenServers, either.

dimitarvp

dimitarvp

Everything you described should be doable with Cachex. It also has a TTL implementation. You don’t have to reinvent it unless it’s a learning project.

fireproofsocks

fireproofsocks

That’s really helpful, thank you!

lucaong

lucaong

If the GenServer is started “statically” as part of the supervision tree, it is often convenient to give it a name. In my example above, we could change Cache.start_link to set a name for the GenServer:

def start_link(options \\ []) do
  {name, options} = Keyword.pop(options, :name, __MODULE__)
  GenServer.start_link(__MODULE__, options, name: name)
end

Here I am forwarding the options as the second argument of GenServer.start_link/3 after extracting the name, so they will be passed to init: that’s not relevant for our case, but it is a common way to pass other options to the GenServer process.

Defining start_link to take a single argument is useful, because in the Supervisor we can specify children as tuples of {module, arg}, and the Supervisor will start them by calling module.start_link(arg). So we can add this to the list of children:

children = [
  # Here we specify children as tuple of {module, argument_of_start_link}
  {Cache, []}
]

And we could then refer to it by name (which in start_link we set by default to the module name, if not explicitly passed):

Cache.get(Cache, :foo)

Or, if we prefer to assign a different name to it (for example if we want to start more than one processes from the same module):

# Supervisor:
children = [
  # ...
  {Cache, name: :my_cache}
]

# usage:
Cache.get(:my_cache, :foo)

Where Next?

Popular in Questions Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
chewm
Hi guys, nice to meet you to the whole forum, I’m new here, I’m trying to configure visual studio code for elixir, right now the intellis...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

Other popular topics Top

shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement