When Rate limit strikes!

Well that was quick… Github API warned me that I’ve spent all my Rate limiting due to the fact that I’m using their Public API without Authorization and well that is expected… To handle this problem I’m going to use a really basic solution: I will cache whatever information I want from their API and store it in memory and I will use only what Elixir has without extra dependencies to do so.

Do not forget that all of the code from this post and others is in this repository

Setting up my cache

Since I need to deploy the application each time I’m posting new content it’s not a problem to have no TTL functionality and let it live for as long as it wants.

As such, I’m going to use Agent .

On my lib/blog/posts.ex I will start my Agent with some minimum state

defmodule Blog.Posts do
  use Agent

  ## Starts the agent with the name of Blog.Posts and state of %{titles: [], posts: %{}}
  def start_link(_) do
    Agent.start_link(fn -> %{titles: [], posts: %{}} end, name: __MODULE__)
  end
end

Then I start it on my lib/blog/application.ex

def start(_type, _args) do
    children = [
      BlogWeb.Telemetry,
      {Phoenix.PubSub, name: Blog.PubSub},
      BlogWeb.Endpoint,
      # Start it as any other child process
      Blog.Posts
    ]

    opts = [strategy: :one_for_one, name: Blog.Supervisor]
    Supervisor.start_link(children, opts)
  end

Using my cache

With the Agent processes started I can now query and update it as I need to. We’ll start by checking if we have titles. On my lib/blog/posts.ex file I will change my functions to check the state of my agent and fetch only if required:

defmodule Blog.Posts do
  # ...

  # Fetches list of posts from memory otherwise makes a request
  def list_post do
    Agent.get_and_update(__MODULE__, fn
      %{titles: []} = state ->
        titles = fetch_titles()
        {titles, %{state | titles: titles}}

      %{titles: titles} = state ->
        {titles, state}
    end)
  end

  defp fetch_titles do
    @url
    |> Req.get!(headers: headers())
    |> then(& &1.body)
    |> Enum.map(& &1["name"])
  end

  defp headers(), do: [{"Authorization", "Bearer #{token()}"}]

  # Going to use a token for good measure
  defp token, do: Application.get_env(:blog, :github_token)
end

As you can see I match against an empty list on the state, meaning nothing was fetched yet, and only run a request at that point in time, updating my state with the response.

I follow the same approach for my posts in lib/blog/posts.ex:

defmodule Blog.Posts do
  ## ...
  ## Fetches post from memory otherwise makes a request
  def get_post!(title) do
    Agent.get_and_update(__MODULE__, fn
      %{posts: posts} = state when not is_map_key(posts, title) ->
        post = fetch_post(title)
        {post, %{state | posts: %{title => post}}}

      %{posts: %{^title => post}} = state ->
        {post, state}
    end)
  end

  defp fetch_post(title) do
    "#{@url}/#{title}.md"
    |> Req.get!(headers: headers())
    |> then(& &1.body["download_url"])
    |> Req.get!(headers: headers())
    |> then(&Earmark.as_html!(&1.body))
  end

  defp headers(), do: [{"Authorization", "Bearer #{token()}"}]

  # Going to use a token for good measure
  defp token, do: Application.get_env(:blog, :github_token)
end

In this one we match again but use the is_map_key/2 guard to check of the state has the title we want to fetch.

And that is it! Without extra dependencies we added a simple caching system that does what we need.

Caveats

This is a really simplistic approach that does not handle cache invalidation. We can still implement that mechanism on top of an Agent where we can have a process checking the state and deleting the state when approriate but for this type of scenario I highly advice to check more solid solutions like ConCache.

Conclusion

Back
Total readers: 392