Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 7.5.0 - 2026-09-02

- Fix race in Atomic `TokenBucket` and `LeakyBucket` where a newly created bucket was published in ETS before its atomic was initialized, so a concurrent hit could read an uninitialized bucket. With `refill_rate: 0` this lost a hit. The atomic is now initialized before it is inserted. (#202)
- Fix `TokenBucket` ETS backend discarding the sub-token refill remainder on every allowed hit. Callers hitting faster than one token period never refilled, and callers paced at exactly the refill rate slowly starved. The stored clock now advances only by the time whose tokens were credited, so the remainder carries into the next hit. (#197)
- Fix `TokenBucket` ETS backend returning a flat `{:deny, 1000}`; it now returns the real time in milliseconds until the bucket holds enough tokens to pay the cost, consistent with the other algorithms. **Behaviour change:** denied callers that sleep on this value will now retry much sooner (e.g. 19ms at `refill_rate: 55` instead of 1000ms). Code that converts it to a `retry-after` header should round up and floor at 1 second; the tutorial examples were updated accordingly. (#198)
- Add `mix hammer.install` Igniter task (`mix igniter.install hammer`) that generates a `MyApp.RateLimit` module and adds it to the supervision tree. Supports `--backend ets|atomic|redis`; `redis` also adds the `hammer_backend_redis` dependency. Igniter is an optional dependency. (#157)
Expand Down
12 changes: 6 additions & 6 deletions lib/hammer/atomic/leaky_bucket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,14 @@ defmodule Hammer.Atomic.LeakyBucket do
do_hit(atomic, now, leak_rate, capacity, cost)

[] ->
# Initialize the atomic (empty bucket at current time) before
# publishing it in ETS, so a concurrent lookup never sees an
# uninitialized zero value.
atomic = :atomics.new(2, signed: false)
:atomics.put(atomic, 1, pack(now, 0))
:atomics.put(atomic, 2, now)

if :ets.insert_new(table, {key, atomic}) do
# Initialize with empty bucket at current time
initial_packed = pack(now, 0)
:atomics.put(atomic, 1, initial_packed)
:atomics.put(atomic, 2, now)
end
:ets.insert_new(table, {key, atomic})

hit(table, key, leak_rate, capacity, cost)
end
Expand Down
12 changes: 7 additions & 5 deletions lib/hammer/atomic/token_bucket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,13 +136,15 @@ defmodule Hammer.Atomic.TokenBucket do
do_hit(atomic, now, refill_rate, capacity, cost)

[] ->
# Initialize the atomic before publishing it in ETS. Once insert_new
# succeeds, other processes can look it up immediately, and reading
# an uninitialized (zero) packed value would look like an empty bucket
# at timestamp 0.
atomic = :atomics.new(2, signed: false)
:atomics.put(atomic, 1, pack(now, capacity))
:atomics.put(atomic, 2, now)

if :ets.insert_new(table, {key, atomic}) do
initial_packed = pack(now, capacity)
:atomics.put(atomic, 1, initial_packed)
:atomics.put(atomic, 2, now)
end
:ets.insert_new(table, {key, atomic})

hit(table, key, refill_rate, capacity, cost)
end
Expand Down