Live data from Hacker News

Building a Cache in Elixir

openmymind.net

21–27 of 27 posts

Re: Building a Cache in Elixir

#21
post #4

You can even get a simple cache running quite quickly with a Genserver and ETS. About the post, I'm not sure what dcache offers over Nebulex though.

If you want a simple, easily understood cache and didn't want to write all the logic using GenServers and ETS you could use dcache. Nebulex looks more complicated than dcache, just comparing the getting started info: "In order to give more flexibility and loading only needed dependencies, Nebulex makes all its dependencies as optional." Then it goes on to describing shards, decorators, telemetry and external adapters…

> I prefer picking simple dependencies and add complexity later on if needed.

That's exactly what having optional dependencies does, though. Except that when you inevitably realize that e.g. you need to monitor your cache-hit ratio in production, enablement for that means adding a suggested dep and then flipping a config flag to use it, rather than finding your own separate dep that does that thing, and then gluing the two "simple" components together using code that is both only maintained by you (rather than the upstream), and which must necessarily only touch the outside interface of both of the components, and so may be many times less efficient than code that hooks into one component or the other.

Re: Building a Cache in Elixir

#22

The purging is a scan with `next()` and a `lookup()` for each item. Since the logic is a simple `<` comparison, could this be done with a single `match_delete` instead? (Unfortunately, ETS match specifications are quite wonky, so without a lot of fiddling myself, I can't suggest exactly what the comparison logic would look like, but I know that you can express certain things like comparisons with it.) That would cut…

Yes, match_delete would work. Even a little better, select_delete will return the number of entries that were purged. To delete everything where the third element in a tuple is greater than a constant, should be something like:

  :ets.select_delete(table, [{{:_, :_, :"$1"}, [], [{:>, :"$1", {:const, expires}}]}])

Re: Building a Cache in Elixir

#23
post #16
post #6

One thing you can do is use :ets.slot/2 to do random probing of the cache and evict entries. The way to do so is simple, just have a process every few milliseconds check a random set of keys (say, 50 keys), and check if they expire. If >25% of keys were about to expire, repeat this process again instantly, until the % of keys expired in the batch is This is how Redis versions prior to 6.0 implemented key expiry. It's…

Why not use a min heap to track expiry times? (Or a doubly-linked list, if lifetimes are uniform?)

The min heap would need to live in the process heap of a particular process. It would only be accessible by that process, so other processes would need to interact with it by sending messages to the "min heap" process and waiting for responses. The min heap process would became a massive throughput bottleneck at much lower QPS than ETS, just due to the overhead of processing messages. This would be exacerbated by GC pauses as the min heap grows larger than around 100k or 1M entries, whereas an ETS table can handle 100M entries or more (never hit a limit actually).

Large, mutable, shared state like a cache works much better if it lives in ETS. Any process can read/write to a properly configured table in a few microseconds. Scales very well to large data sizes and high QPS.

The random probing approach has the disadvantage that it stores expired entries longer than necessary. This either wastes memory or reduces cache hit rate. Either of these seem preferable to limiting throughput and maximum entry count, as the min heap would.

Re: Building a Cache in Elixir

#24
post #5

Earlier quoted context omitted.

On my mobile phone the font is stupid large though, making it impossible to read.

Good thing browser agents are just that, agents for the user. Most (if not all) have controls over the font-size, so you can make it larger/smaller at will, unless the website actively tries to defeat zooming, which this one doesn't. For me, the font was also too large, but 0.5 seconds later, it wasn't. But no need to comment about something like this (same with the parent) as it's not actually about the article, jus…

How many people actually use a mobile browser to make things smaller. Usage is probably 1 percent. He has a valid issue

Re: Building a Cache in Elixir

#25
post #21

Earlier quoted context omitted.

If you want a simple, easily understood cache and didn't want to write all the logic using GenServers and ETS you could use dcache. Nebulex looks more complicated than dcache, just comparing the getting started info: "In order to give more flexibility and loading only needed dependencies, Nebulex makes all its dependencies as optional." Then it goes on to describing shards, decorators, telemetry and external adapters…

> I prefer picking simple dependencies and add complexity later on if needed. That's exactly what having optional dependencies does, though. Except that when you inevitably realize that e.g. you need to monitor your cache-hit ratio in production, enablement for that means adding a suggested dep and then flipping a config flag to use it, rather than finding your own separate dep that does that thing, and then gluing t…

In my experience people get wooed by libraries that have a lot of configurations, settings, optional features, etc. and then rarely use them. Just pick a simpler tool that does specifically what you need at the time. Also, modularize it so your entire code base isn’t impacted by swapping out a cache tool (or http, etc.)

Re: Building a Cache in Elixir

#26
post #21

Earlier quoted context omitted.

> I prefer picking simple dependencies and add complexity later on if needed. That's exactly what having optional dependencies does, though. Except that when you inevitably realize that e.g. you need to monitor your cache-hit ratio in production, enablement for that means adding a suggested dep and then flipping a config flag to use it, rather than finding your own separate dep that does that thing, and then gluing t…

In my experience people get wooed by libraries that have a lot of configurations, settings, optional features, etc. and then rarely use them. Just pick a simpler tool that does specifically what you need at the time. Also, modularize it so your entire code base isn’t impacted by swapping out a cache tool (or http, etc.)

I agree but setting up a basic Nebulex cache is on par with most elixir "app wide" libraries.

But yeah I guess the docs can be a bit overwhelming at first glance.

Personally I like the decorators, especially as Ecto does not provide a cache solution.

Re: Building a Cache in Elixir

#27
post #21

Earlier quoted context omitted.

> I prefer picking simple dependencies and add complexity later on if needed. That's exactly what having optional dependencies does, though. Except that when you inevitably realize that e.g. you need to monitor your cache-hit ratio in production, enablement for that means adding a suggested dep and then flipping a config flag to use it, rather than finding your own separate dep that does that thing, and then gluing t…

In my experience people get wooed by libraries that have a lot of configurations, settings, optional features, etc. and then rarely use them. Just pick a simpler tool that does specifically what you need at the time. Also, modularize it so your entire code base isn’t impacted by swapping out a cache tool (or http, etc.)

I do know what you mean, but the Elixir ecosystem is fairly exceptional in this regard. (It helps that Elixir is a compiled language with macros; many libraries implement optional-dep support via the compile-time-macro equivalent of the strategy pattern, where you only get the runtime code you ask for, rather than a bunch of useless calls to hooks that have no subscribers in your codebase. And if you're imagining that results in awful codegen code, it doesn't; in many cases, this is just as simple as putting an if statement directly into a module body, with method definitions inside the if branches.)

It's also helped by an eye toward low coupling of these support libraries. Elixir's Phoenix web framework, for example, is "batteries included" in some senses; but all those batteries are only included because your own generated skeleton code pulls them in; not because the framework itself depends on them in any way. So if you don't need e.g. i18n, or views, or really anything beyond an API that responds with JSON; then you can delete ~five lines from your skeleton project, drop the relevant deps, and the resulting project will be a lightweight web framework that just serves API calls. (This is why there isn't really a "lightweight" Elixir web framework ala Flask/Sinatra/etc.; Phoenix is both Elixir's batteries-included web framework, and its lightweight/minimal web framework, depending on how you use it.)

Post reply on HN