Posted
I learned some tricks from an LLM today.
Suppose you want to build a "circuit breaker", which is "open" whenever something has failed enough times and "closes" when it succeeds. This pattern is used for things like "don't keep calling this service when we already know it's down", or "only notify the user the first few times a problem happens."
iex> Breaker.init() # :ok
iex> Breaker.open?() # false
iex> Breaker.failed() # 1
iex> Breaker.open?() # false
iex> Breaker.failed() # 2
iex> Breaker.open?() # false
iex> Breaker.failed() # 3
iex> Breaker.open?() # true
iex> Breaker.failed() # 4
iex> Breaker.open?() # true
iex> Breaker.succeeded() # :ok
iex> Breaker.open?() # false
iex> Breaker.failed() # 1
iex> Breaker.open?() # false
One way is to use these:
-
:atomicsis for arrays of integers that change often and are shared across processes. They can be mutated atomically from concurrent processes.add_get/3always succeeds andcompare_exchangeis for when only one caller should win. See the:atomicsdocs. -
:persistent_termis for values you write once and read many times. That value could be a reference to an atomics array. - Both of these are in-memory only - they don't survive VM restarts and aren't shared across a cluster.
Put together:
defmodule Breaker do
# node-wide name, so namespace it with the module
@key {__MODULE__, :failures}
@threshold 3
# Run once, at startup. Writing to :persistent_term
# is expensive, so store the reference once and
# never change it
def init do
# Create an atomically mutable 1-element array of integers
ref = :atomics.new(1, signed: false)
# Store the reference under a known key so we know how to
# get it and mutate it
:persistent_term.put(@key, ref)
end
# Reading from :persistent_term is cheap
defp get_atomics, do: :persistent_term.get(@key)
# We have a 1-slot atomic and we always work with that slot
@slot 1
# Increments the failure counter atomically.
def failed, do: :atomics.add_get(get_atomics(), @slot, 1)
# Resets the failure counter to 0
def succeeded, do: :atomics.put(get_atomics(), @slot, 0)
# True if we've had too many failures
def open?, do: :atomics.get(get_atomics(), @slot) >= @threshold
end