sasa1977 / con_cache

ets based key/value cache with row level isolated writes and ttl support
MIT License
910 stars 71 forks source link

Support ttl: :keep to Not Update TTL on Updates #13

Closed kevinastone closed 8 years ago

kevinastone commented 8 years ago

It would be nice to prevent touching the TTL when updating. I'm using con_cache as a rate limit store, so it should simply expire after a duration after the key is first created. Updates wouldn't affect the ttl.

Looking for something like:

{limit, duration} = {15, :min}

used = ConCache.get_or_store(:ratelimit, key, fn ->
  %ConCache.Item{value: 0, ttl: ttl(limit, duration)}
end)

if used >= limit do
  conn = conn |> Plug.Conn.send_resp(429, "Too many requests") |> Plug.Conn.halt
else
  ConCache.update(:tally, key, fn old_value ->
    {:ok, %ConCache.Item{value: min(old_value + 1, limit), ttl: :keep}
  end)

  used = ConCache.get(:tally, key)
  conn = conn |> Plug.Conn.merge_resp_headers(%{"x-ratelimit-remaining" => to_string(max(limit - used, 0))})
end
sasa1977 commented 8 years ago

Hi,

Thank you for this idea. I'd like to take some time to think about it.

However, when it comes to your particular problem, I'd suggest using plain ETS. I recently implemented a basic rate limiter for my blog. You can find the source here.

The idea is to start a process which will create a named ETS table that is completely purged in regular interval:

Erlangelist.RateLimiter.start_link(:per_minute, :timer.minutes(1))

Then you can call allow? from any process to increase the count by 1 and immediately get the response whether you can carry on or not:

Erlangelist.RateLimiter.allow?(:per_minute, operation_key, max_rate)

The operation_key is arbitrary term that uniquely identifies your operation. You can use a single table to enforce different limits on different operations. However, if you want to use different intervals, you'll need to start one process per each interval.

Note that allow? is executed in the caller process, so there's no single process bottleneck. The owner process is only used to periodically purge the table.

Note: this code will work only in Erlang 18, because it relies on a new variant of :ets.update/4. It could be implemented for earlier versions as well, but some trade-offs must be made.

kevinastone commented 8 years ago

Awesome, thanks for the link.

sasa1977 commented 8 years ago

No problem. I didn't publish it as a lib, because it's so small, but the code is MIT licensed, so basically feel free to copy-paste it and do whatever you want with it. Of course, I hold no responsibility :-)

Feel free to ping me if you get stuck.

sasa1977 commented 8 years ago

This is now supported through #16