Skip to content

Instantly share code, notes, and snippets.

@cloud8421
Created July 13, 2016 20:54
Show Gist options
  • Save cloud8421/34bcc0d5be7c3f2d2626d17562ace4f2 to your computer and use it in GitHub Desktop.
Save cloud8421/34bcc0d5be7c3f2d2626d17562ace4f2 to your computer and use it in GitHub Desktop.

A few considerations in case you need to work on performance even further (you may not even need them, but it's worth knowing).

  • Using the GenServer flow to read data may become a bottleneck (as it happens inside the GenServer process)
  • If you don't care about duplicates, you may set the table as duplicate_bag. This speeds up writes, as it doesn't have to check for dupes.

It may also be interesting to read directly from the table instead of going through the GenServer. That's possible by creating the table as public and named_table, giving it __MODULE__ as name. In addition, as the table would be mostly reads, it can be marked as read_concurrency (which optimizes reads over writes).

ets.new(__MODULE__, [:named_table, :duplicate_bag, :public, :read_concurrency])

This setup allows reading directly from it in fetch/2 by referencing the table by name without needing call/2. The read therefore happens in the calling process. A side effect of this is that it also reduces the possibility of a table crash (the owner is still the GenServer).

defp get(slug) do
  case :ets.lookup(__MODULE__, slug) do
    [] -> {:not_found}
    [{_slug, result}] -> {:found, result}
  end
end

Note that the table is now public, so any process can read it (so no private data).

@joshuaclayton
Copy link

@cloud8421 I tried this out and pushed it up to Heroku. Is this what you were thinking, in terms of code change to LinkCache.Cache?

Same stats (500 concurrent users, free hardware/db, over 60 seconds), and came away with:

Transactions:                  58763 hits
Availability:                 100.00 %
Elapsed time:                  59.35 secs
Data transferred:               5.32 MB
Response time:                  0.25 secs
Transaction rate:             990.11 trans/sec
Throughput:                     0.09 MB/sec
Concurrency:                  242.91
Successful transactions:       58763
Failed transactions:               0
Longest transaction:            2.69
Shortest transaction:           0.05

Absurdly fast. 2x requests served and 1/3 the avg. response time.

@cloud8421
Copy link
Author

Great results!

I've looked at the linked commit and here are my thoughts:

  • Returning {:ok, self()} on start_link/1 means that the controller process (in our case the Supervisor) owns the table. Not only that, it ends up supervising itself. I'm not sure how that works, I should check it but I don't think that a process can trap its own exit. In other words, the LinkCache.Supervisor tree doesn't give you much.
  • All computation now happens in the RedirectController, so any failure would cause the request to fail. So if you fail to write to the cache, your request fails.

Considering the above, I would test an approach where:

  • The table is owned by the Supervisor. That doesn't need to change. To do that, the table gets created during the tree setup in LinkCache.Supervisor.
  • LinkCache comes back to be a GenServer. set/2 invokes a cast that writes to the table asynchronously. This is because in the current implementation there's no check at all on the success of the insertion, so there's no downside to make it asynchronous. It's a cache after all, so worst case scenario we lose that value (and logs would catch that failure).
  • With the two above you end up having a supervised writer process that can't crash the table and whose failure doesn't cause a request failure (you would only get some performance degradation if you consistently fail writing to the cache).

Hope it's clear!

Keep up the good work :)

@cloud8421
Copy link
Author

@adamkittelson
Copy link

Arriving via https://twitter.com/joshuaclayton/status/753746518725816320

This is pretty much exactly what I was going to say, looks like @cloud8421 has you covered. Thanks for all the Elixir coverage lately @joshuaclayton

@imranismail
Copy link

@cloud8421 why does LinkCache.Cache has to remain a GenServer if we can just write/read directly and supervisor already owns the table?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment