So, I have built a library to make HTTP requests with a circuit breaker (https://github.com/awochna/breaker).
The question I have is about maintaining the state and configuration of that breaker. I want to do this in a way that is both efficient and idiomatic. Currently, it looks something like this:
# Builds a new request circuit breaker for making requests to "http://example.com"
iex> Breaker.new(%{url: "http://example.com"})
%Breaker{headers: [], status: #PID<0.67.0>, timeout: 3000, url: "http://example.com"}
I understand that making the argument passed to Breaker.new/1
a Keyword List is probably more idiomatic than using a Map, but is returning a %Breaker{}
struct something that would be idiomatic in this case?
The status
property there is an Agent
process and it holds the current counts and error threshold for determining the open or closed status for the breaker. Here, I chose an agent because it seemed like an efficient interface for holding state that would be constantly changing. The information I chose to store in the state of the agent process is only what is constantly updated or required to calculate the breaker’s status. All other, more “static” information (such as headers
sent with every request, the default timeout
, etc) are stored in the %Breaker{}
struct.
Is this separation (using an Agent for storing the necessary and changing parts of the breaker, using a map for the unchanging parts) either idiomatic or necessarily more efficient?
In a way, since the Agent seems to be the limiting component of this system, it would make sense to say that the less state it needs to hold/compute the better. Then again, having all of the information for a single breaker in separate locations seem to make things unnecessarily complicated.
Would the whole system be better served if the library simply started a new GenServer that held all of these state components? That seems to be far more idiomatic, but could that slow things down too much? Should I try to keep all unnecessary calculations out of the GenServer callbacks so that the calling process is calculating as much as it can without potentially blocking other processes that need access to the breaker’s GenServer?
Any help for some of these questions would be greatly appreciated. As would any resources (books, screencasts, etc) about how to weigh these options or about Elixir application design.