···7788## [Unreleased]
991010-### Changed
1010+### Breaking changes
11111212-- Deprecated `glimit.handler` and renamed it to the more descriptive `glimit.on_limit_exceeded`.
1212+- Refactored the code to use a Token Bucket algorithm instead of a Sliding Window algorithm. This has removed some of the library features/API, such as `glimit.applyX` to apply a rate limiter on a function with multiple arguments.
1313+1414+### Added
1515+1616+- Added a `burst_limit` setting to the limiter configuration. This setting allows the user to set the maximum number of tokens that the bucket can hold.
131714181519## v0.1.3 - 2024-09-04
+13-7
README.md
···44[](https://hexdocs.pm/glimit/)
55[](https://github.com/nootr/glimit/actions/workflows/test.yml)
6677-A framework-agnostic rate limiter for Gleam. 💫
77+A simple, framework-agnostic, in-memory rate limiter for Gleam. 💫
8899> ⚠️ This library is still in development, use at your own risk.
1010+1111+1212+## Features
1313+1414+* ✨ Simple and easy to use.
1515+* 📏 Rate limits based on any key (e.g. IP address, or user ID).
1616+* 🪣 Uses a distributed Token Bucket algorithm to rate limit requests.
1717+* 🗄️ No back-end service needed; stores rate limit stats in-memory.
101811191220## Usage
···7381 let limiter =
7482 glimit.new()
7583 |> glimit.per_second(10)
7676- |> glimit.per_minute(100)
7777- |> glimit.per_hour(1000)
7884 |> glimit.identifier(get_identifier)
7985 |> glimit.on_limit_exceeded(rate_limit_reached)
8086 |> glimit.build
···9197```
929893999494-## How it works
100100+## Constraints
951019696-Once v1.0 is reached, `glimit` will use a distributed Token Bucket algorithm to rate limit requests. It will support multiple backend storage systems, such as Redis and in-memory storage.
102102+While the in-memory rate limiter is simple and easy to use, it does have an important constraint: it is scoped to the BEAM VM cluster it runs in. This means that if your application is running across multiple BEAM VM clusters, the rate limiter will not be shared between them.
971039898-However, at the moment, `glimit` uses a simple Sliding Window algorithm with in-memory storage. This means that the rate limiter is not memory efficient and is not ready for production use.
104104+There are plans to add support for a centralized data store using Redis in the future.
99105100106101107## Documentation
102108103103-Further documentation can be found at <https://hexdocs.pm/glimit>.
109109+Further documentation can be found at <https://hexdocs.pm/glimit/glimit.html>.
104110105111106112## Contributing
+4-1
gleam.toml
···11name = "glimit"
22version = "0.1.3"
3344-description = "A framework-agnostic rate limiter for Gleam."
44+description = "A simple, framework-agnostic, in-memory rate limiter for Gleam."
55licences = ["MIT"]
66repository = { type = "github", user = "nootr", repo = "glimit" }
77target = "erlang"
88+internal_modules = [
99+ "glimit/*",
1010+]
811912[dependencies]
1013gleam_stdlib = ">= 0.34.0 and < 2.0.0"
+52-126
src/glimit.gleam
···11-//// A framework-agnostic rate limiter for Gleam. 💫
11+//// This module provides a distributed rate limiter that can be used to limit the
22+//// number of requests or function calls per second for a given identifier.
23////
33-//// This module provides a rate limiter that can be used to limit the number of
44-//// requests that can be made to a given function or handler within a given
55-//// time frame.
44+//// A single actor is used to assign one rate limiter actor per identifier. The
55+//// rate limiter actor then uses a Token Bucket algorithm to determine if a
66+//// request or function call should be allowed to proceed. A separate process is
77+//// polling the rate limiters to remove full buckets to reduce unnecessary memory
88+//// usage.
99+////
1010+//// The rate limits are configured using the following two options:
611////
77-//// The rate limiter is implemented as an actor that keeps track of the number
88-//// of hits for a given identifier within the last second, minute, and hour.
99-//// When a hit is received, the actor checks the rate limits and either allows
1010-//// the hit to pass or rejects it.
1212+//// - `per_second`: The rate of new available tokens per second. Think of this
1313+//// as the steady state rate limit.
1414+//// - `burst_limit`: The maximum number of available tokens. Think of this as
1515+//// the burst rate limit. The default value is the `per_second` rate limit.
1116////
1212-//// The rate limiter can be configured with rate limits per second, minute, and
1313-//// hour, and a handler function that is called when the rate limit is reached.
1417//// The rate limiter can be applied to a function or handler using the `apply`
1518//// function, which returns a new function that checks the rate limit before
1619//// calling the original function.
···2326//// let limiter =
2427//// glimit.new()
2528//// |> glimit.per_second(10)
2626-//// |> glimit.per_minute(100)
2727-//// |> glimit.per_hour(1000)
2829//// |> glimit.identifier(fn(request) { request.ip })
2930//// |> glimit.on_limit_exceeded(fn(_request) { "Rate limit reached" })
3031//// |> glimit.build()
···3536//// ```
3637////
37383838-import gleam/erlang/process.{type Subject}
3939import gleam/option.{type Option, None, Some}
4040import gleam/result
4141-import glimit/actor
4141+import glimit/rate_limiter
4242+import glimit/registry.{type RateLimiterRegistryActor}
42434344/// A rate limiter.
4445///
4546pub type RateLimiter(a, b, id) {
4647 RateLimiter(
4747- subject: Subject(actor.Message(id)),
4848+ rate_limiter_registry: RateLimiterRegistryActor(id),
4849 on_limit_exceeded: fn(a) -> b,
4950 identifier: fn(a) -> id,
5051 )
···5556pub type RateLimiterBuilder(a, b, id) {
5657 RateLimiterBuilder(
5758 per_second: Option(Int),
5858- per_minute: Option(Int),
5959- per_hour: Option(Int),
5959+ burst_limit: Option(Int),
6060 identifier: Option(fn(a) -> id),
6161 on_limit_exceeded: Option(fn(a) -> b),
6262 )
···6767pub fn new() -> RateLimiterBuilder(a, b, id) {
6868 RateLimiterBuilder(
6969 per_second: None,
7070- per_minute: None,
7171- per_hour: None,
7070+ burst_limit: None,
7271 identifier: None,
7372 on_limit_exceeded: None,
7473 )
···76757776/// Set the rate limit per second.
7877///
7878+/// The value is not only used for the rate at which tokens are added to the bucket, but
7979+/// also for the maximum number of available tokens. To set a different value fo the
8080+/// maximum number of available tokens, use the `burst_limit` function.
8181+///
7982pub fn per_second(
8083 limiter: RateLimiterBuilder(a, b, id),
8184 limit: Int,
···8386 RateLimiterBuilder(..limiter, per_second: Some(limit))
8487}
85888686-/// Set the rate limit per minute.
8989+/// Set the maximum number of available tokens.
8790///
8888-pub fn per_minute(
8989- limiter: RateLimiterBuilder(a, b, id),
9090- limit: Int,
9191-) -> RateLimiterBuilder(a, b, id) {
9292- RateLimiterBuilder(..limiter, per_minute: Some(limit))
9393-}
9494-9595-/// Set the rate limit per hour.
9191+/// The maximum number of available tokens is the maximum number of requests that can be
9292+/// made in a single second. The default value is the same as the rate limit per second.
9693///
9797-pub fn per_hour(
9494+pub fn burst_limit(
9895 limiter: RateLimiterBuilder(a, b, id),
9999- limit: Int,
9696+ burst_limit: Int,
10097) -> RateLimiterBuilder(a, b, id) {
101101- RateLimiterBuilder(..limiter, per_hour: Some(limit))
9898+ RateLimiterBuilder(..limiter, burst_limit: Some(burst_limit))
10299}
103100104101/// Set the handler to be called when the rate limit is reached.
105102///
106103pub fn on_limit_exceeded(
107107- limiter: RateLimiterBuilder(a, b, id),
108108- on_limit_exceeded: fn(a) -> b,
109109-) -> RateLimiterBuilder(a, b, id) {
110110- RateLimiterBuilder(..limiter, on_limit_exceeded: Some(on_limit_exceeded))
111111-}
112112-113113-@deprecated("Use `on_limit_exceeded` instead")
114114-pub fn handler(
115104 limiter: RateLimiterBuilder(a, b, id),
116105 on_limit_exceeded: fn(a) -> b,
117106) -> RateLimiterBuilder(a, b, id) {
···129118130119/// Build the rate limiter.
131120///
132132-/// Panics if the rate limiter actor cannot be started or if the identifier
133133-/// function or on_limit_exceeded function is missing.
121121+/// Panics if the rate limiter registry cannot be started or if the `identifier`
122122+/// function or `on_limit_exceeded` function is missing.
123123+///
124124+/// To handle errors instead of panicking, use `try_build`.
134125///
135126pub fn build(config: RateLimiterBuilder(a, b, id)) -> RateLimiter(a, b, id) {
136127 case try_build(config) {
···144135pub fn try_build(
145136 config: RateLimiterBuilder(a, b, id),
146137) -> Result(RateLimiter(a, b, id), String) {
147147- use subject <- result.try(
148148- actor.new(config.per_second, config.per_minute, config.per_hour)
149149- |> result.map_error(fn(_) { "Failed to start rate limiter actor" }),
138138+ use per_second <- result.try(case config.per_second {
139139+ Some(per_second) -> Ok(per_second)
140140+ None -> Error("`per_second` rate limit is required")
141141+ })
142142+ let burst_limit = case config.burst_limit {
143143+ Some(burst_limit) -> burst_limit
144144+ None -> per_second
145145+ }
146146+ use rate_limiter_registry <- result.try(
147147+ registry.new(per_second, burst_limit)
148148+ |> result.map_error(fn(_) { "Failed to start rate limiter registry" }),
150149 )
151150 use identifier <- result.try(case config.identifier {
152151 Some(identifier) -> Ok(identifier)
···158157 })
159158160159 Ok(RateLimiter(
161161- subject: subject,
160160+ rate_limiter_registry: rate_limiter_registry,
162161 on_limit_exceeded: on_limit_exceeded,
163162 identifier: identifier,
164163 ))
···169168pub fn apply(func: fn(a) -> b, limiter: RateLimiter(a, b, id)) -> fn(a) -> b {
170169 fn(input: a) -> b {
171170 let identifier = limiter.identifier(input)
172172- case actor.hit(limiter.subject, identifier) {
173173- Ok(Nil) -> func(input)
174174- Error(Nil) -> limiter.on_limit_exceeded(input)
175175- }
176176- }
177177-}
178178-179179-/// Apply the rate limiter to a request handler or function with two arguments.
180180-///
181181-/// Note: this function folds the two arguments into a tuple before passing them to the
182182-/// identifier or on_limit_exceeded functions.
183183-///
184184-/// # Example
185185-///
186186-/// ```gleam
187187-/// import glimit
188188-///
189189-/// let limiter =
190190-/// glimit.new()
191191-/// |> glimit.per_hour(1000)
192192-/// |> glimit.identifier(fn(i: #(String, String)) {
193193-/// let #(a, _) = i
194194-/// a
195195-/// })
196196-/// |> glimit.on_limit_exceeded(fn(_) { "Rate limit reached" })
197197-/// |> glimit.build()
198198-///
199199-/// let handler =
200200-/// fn(a, b) { a <> b }
201201-/// |> glimit.apply2(limiter)
202202-/// ```
203203-pub fn apply2(
204204- func: fn(a, b) -> c,
205205- limiter: RateLimiter(#(a, b), c, id),
206206-) -> fn(a, b) -> c {
207207- fn(a: a, b: b) -> c {
208208- let identifier = limiter.identifier(#(a, b))
209209- case actor.hit(limiter.subject, identifier) {
210210- Ok(Nil) -> func(a, b)
211211- Error(Nil) -> limiter.on_limit_exceeded(#(a, b))
212212- }
213213- }
214214-}
215215-216216-/// Apply the rate limiter to a request handler or function with three arguments.
217217-///
218218-/// Note: this function folds the three arguments into a tuple before passing them to the
219219-/// identifier or on_limit_exceeded functions.
220220-///
221221-pub fn apply3(
222222- func: fn(a, b, c) -> d,
223223- limiter: RateLimiter(#(a, b, c), d, id),
224224-) -> fn(a, b, c) -> d {
225225- fn(a: a, b: b, c: c) -> d {
226226- let identifier = limiter.identifier(#(a, b, c))
227227- case actor.hit(limiter.subject, identifier) {
228228- Ok(Nil) -> func(a, b, c)
229229- Error(Nil) -> limiter.on_limit_exceeded(#(a, b, c))
230230- }
231231- }
232232-}
233233-234234-/// Apply the rate limiter to a request handler or function with four arguments.
235235-///
236236-/// Note: this function folds the four arguments into a tuple before passing them to the
237237-/// identifier or on_limit_exceeded functions.
238238-///
239239-/// > ⚠️ For functions with more than four arguments, you'll need to write a custom
240240-/// > wrapper function that folds the arguments into a tuple before passing them to the
241241-/// > rate limiter. This is because Gleam does not support variadic functions, because
242242-/// > the BEAM VM identifies functions by their arity.
243243-///
244244-pub fn apply4(
245245- func: fn(a, b, c, d) -> e,
246246- limiter: RateLimiter(#(a, b, c, d), e, id),
247247-) -> fn(a, b, c, d) -> e {
248248- fn(a: a, b: b, c: c, d: d) -> e {
249249- let identifier = limiter.identifier(#(a, b, c, d))
250250- case actor.hit(limiter.subject, identifier) {
251251- Ok(Nil) -> func(a, b, c, d)
252252- Error(Nil) -> limiter.on_limit_exceeded(#(a, b, c, d))
171171+ case limiter.rate_limiter_registry |> registry.get_or_create(identifier) {
172172+ Ok(rate_limiter) -> {
173173+ case rate_limiter |> rate_limiter.hit {
174174+ Ok(Nil) -> func(input)
175175+ Error(Nil) -> limiter.on_limit_exceeded(input)
176176+ }
177177+ }
178178+ Error(_) -> panic as "Failed to get rate limiter"
253179 }
254180 }
255181}
-120
src/glimit/actor.gleam
···11-//// The rate limiter actor.
22-////
33-44-import gleam/dict
55-import gleam/erlang/process.{type Subject}
66-import gleam/list
77-import gleam/option.{type Option, None, Some}
88-import gleam/otp/actor
99-import gleam/result
1010-import glimit/utils
1111-1212-/// The messages that the actor can receive.
1313-///
1414-pub type Message(id) {
1515- /// Stop the actor.
1616- Shutdown
1717-1818- /// Mark a hit for a given identifier.
1919- Hit(identifier: id, reply_with: Subject(Result(Nil, Nil)))
2020-}
2121-2222-/// The actor state.
2323-///
2424-type State(a, b, id) {
2525- RateLimiterState(
2626- hit_log: dict.Dict(id, List(Int)),
2727- per_second: Option(Int),
2828- per_minute: Option(Int),
2929- per_hour: Option(Int),
3030- )
3131-}
3232-3333-fn handle_message(
3434- message: Message(id),
3535- state: State(a, b, id),
3636-) -> actor.Next(Message(id), State(a, b, id)) {
3737- case message {
3838- Shutdown -> actor.Stop(process.Normal)
3939- Hit(identifier, client) -> {
4040- // Update hit log
4141- let timestamp = utils.now()
4242- let hits =
4343- state.hit_log
4444- |> dict.get(identifier)
4545- |> result.unwrap([])
4646- |> list.filter(fn(hit) { hit >= timestamp - 60 * 60 })
4747- |> list.append([timestamp])
4848- let hit_log =
4949- state.hit_log
5050- |> dict.insert(identifier, hits)
5151- let state = RateLimiterState(..state, hit_log: hit_log)
5252-5353- // Check rate limits
5454- // TODO: optimize into a single loop
5555- let hits_last_hour = hits |> list.length()
5656-5757- let hits_last_minute =
5858- hits
5959- |> list.filter(fn(hit) { hit >= timestamp - 60 })
6060- |> list.length()
6161-6262- let hits_last_second =
6363- hits
6464- |> list.filter(fn(hit) { hit >= timestamp - 1 })
6565- |> list.length()
6666-6767- let limit_reached = {
6868- case state.per_hour {
6969- Some(limit) -> hits_last_hour > limit
7070- None -> False
7171- }
7272- || case state.per_minute {
7373- Some(limit) -> hits_last_minute > limit
7474- None -> False
7575- }
7676- || case state.per_second {
7777- Some(limit) -> hits_last_second > limit
7878- None -> False
7979- }
8080- }
8181-8282- case limit_reached {
8383- True -> process.send(client, Error(Nil))
8484- False -> process.send(client, Ok(Nil))
8585- }
8686-8787- actor.continue(state)
8888- }
8989- }
9090-}
9191-9292-/// Create a new rate limiter actor.
9393-///
9494-pub fn new(
9595- per_second: Option(Int),
9696- per_minute: Option(Int),
9797- per_hour: Option(Int),
9898-) -> Result(Subject(Message(id)), Nil) {
9999- let state =
100100- RateLimiterState(
101101- hit_log: dict.new(),
102102- per_second: per_second,
103103- per_minute: per_minute,
104104- per_hour: per_hour,
105105- )
106106- actor.start(state, handle_message)
107107- |> result.nil_error
108108-}
109109-110110-/// Log a hit for a given identifier.
111111-///
112112-pub fn hit(subject: Subject(Message(id)), identifier: id) -> Result(Nil, Nil) {
113113- actor.call(subject, Hit(identifier, _), 10)
114114-}
115115-116116-/// Stop the actor.
117117-///
118118-pub fn stop(subject: Subject(Message(id))) {
119119- actor.send(subject, Shutdown)
120120-}
+120
src/glimit/rate_limiter.gleam
···11+//// This module contains the implementation of a single rate limiter actor.
22+////
33+44+import gleam/erlang/process.{type Subject}
55+import gleam/int
66+import gleam/option.{type Option, None, Some}
77+import gleam/otp/actor
88+import gleam/result
99+import glimit/utils
1010+1111+type State {
1212+ State(
1313+ /// The maximum number of tokens.
1414+ ///
1515+ max_token_count: Int,
1616+ /// The rate of token generation per second.
1717+ ///
1818+ token_rate: Int,
1919+ /// The number of tokens available.
2020+ ///
2121+ token_count: Int,
2222+ /// Epoch timestamp of the last time the rate limiter was updated.
2323+ ///
2424+ last_update: Option(Int),
2525+ )
2626+}
2727+2828+/// Updates the state to reflect the passage of time.
2929+///
3030+fn refill_bucket(state: State) -> State {
3131+ let now = utils.now()
3232+ let time_diff = case state.last_update {
3333+ None -> 0
3434+ Some(last_update) -> now - last_update
3535+ }
3636+ let token_count =
3737+ state.token_count + state.token_rate * time_diff
3838+ |> int.min(state.max_token_count)
3939+ |> int.max(0)
4040+4141+ State(..state, token_count: token_count, last_update: Some(now))
4242+}
4343+4444+/// Updates the state to remove a token.
4545+///
4646+fn remove_token(state: State) -> State {
4747+ State(..state, token_count: state.token_count - 1)
4848+}
4949+5050+/// The message type for the rate limiter actor.
5151+///
5252+pub type Message {
5353+ /// Stop the actor.
5454+ ///
5555+ Shutdown
5656+5757+ /// Mark a hit.
5858+ ///
5959+ /// The actor will reply with the result of the hit.
6060+ ///
6161+ Hit(reply_with: Subject(Result(Nil, Nil)))
6262+6363+ /// Returns True if the token bucket is full.
6464+ ///
6565+ HasFullBucket(reply_with: Subject(Bool))
6666+}
6767+6868+fn handle_message(message: Message, state: State) -> actor.Next(Message, State) {
6969+ case message {
7070+ Shutdown -> actor.Stop(process.Normal)
7171+7272+ Hit(client) -> {
7373+ let state = refill_bucket(state)
7474+ let #(result, state) = case state.token_count {
7575+ 0 -> #(Error(Nil), state)
7676+ _ -> #(Ok(Nil), remove_token(state))
7777+ }
7878+7979+ actor.send(client, result)
8080+ actor.continue(state)
8181+ }
8282+8383+ HasFullBucket(client) -> {
8484+ let state = refill_bucket(state)
8585+ let result = state.token_count == state.max_token_count
8686+8787+ actor.send(client, result)
8888+ actor.continue(state)
8989+ }
9090+ }
9191+}
9292+9393+/// Create a new rate limiter actor.
9494+///
9595+pub fn new(
9696+ max_token_count: Int,
9797+ token_rate: Int,
9898+) -> Result(Subject(Message), Nil) {
9999+ let state =
100100+ State(
101101+ max_token_count: max_token_count,
102102+ token_rate: token_rate,
103103+ token_count: max_token_count,
104104+ last_update: None,
105105+ )
106106+ actor.start(state, handle_message)
107107+ |> result.nil_error
108108+}
109109+110110+/// Mark a hit on the rate limiter actor.
111111+///
112112+pub fn hit(rate_limiter: Subject(Message)) -> Result(Nil, Nil) {
113113+ actor.call(rate_limiter, Hit, 10)
114114+}
115115+116116+/// Returns True if the token bucket is full.
117117+///
118118+pub fn has_full_bucket(rate_limiter: Subject(Message)) -> Bool {
119119+ actor.call(rate_limiter, HasFullBucket, 10)
120120+}
+143
src/glimit/registry.gleam
···11+//// This module contains a registry which maps hit identifiers to rate limiter actors.
22+////
33+44+import gleam/dict.{type Dict}
55+import gleam/erlang/process.{type Subject}
66+import gleam/list
77+import gleam/otp/actor
88+import gleam/otp/task
99+import gleam/result
1010+import glimit/rate_limiter
1111+1212+pub type RateLimiterRegistryActor(id) =
1313+ Subject(Message(id))
1414+1515+/// The rate limiter registry state.
1616+type State(id) {
1717+ State(
1818+ /// The maximum number of tokens.
1919+ ///
2020+ max_token_count: Int,
2121+ /// The rate of token generation per second.
2222+ ///
2323+ token_rate: Int,
2424+ /// The registry of rate limiters.
2525+ ///
2626+ registry: Dict(id, Subject(rate_limiter.Message)),
2727+ )
2828+}
2929+3030+pub type Message(id) {
3131+ /// Get the rate limiter for the given id or create a new one if missing.
3232+ ///
3333+ GetOrCreate(
3434+ identifier: id,
3535+ reply_with: Subject(Result(Subject(rate_limiter.Message), Nil)),
3636+ )
3737+ Sweep
3838+}
3939+4040+fn handle_get_or_create(
4141+ identifier,
4242+ state: State(id),
4343+) -> Result(Subject(rate_limiter.Message), Nil) {
4444+ case state.registry |> dict.get(identifier) {
4545+ Ok(rate_limiter) -> {
4646+ Ok(rate_limiter)
4747+ }
4848+ Error(_) -> {
4949+ use rate_limiter <- result.try(rate_limiter.new(
5050+ state.max_token_count,
5151+ state.token_rate,
5252+ ))
5353+ Ok(rate_limiter)
5454+ }
5555+ }
5656+}
5757+5858+/// Shutdown and remove all rate limiters that are not alive.
5959+///
6060+fn handle_message(
6161+ message: Message(id),
6262+ state: State(id),
6363+) -> actor.Next(Message(id), State(id)) {
6464+ case message {
6565+ GetOrCreate(identifier, client) -> {
6666+ case handle_get_or_create(identifier, state) {
6767+ Ok(rate_limiter) -> {
6868+ let registry = state.registry |> dict.insert(identifier, rate_limiter)
6969+ let state = State(..state, registry: registry)
7070+ actor.send(client, Ok(rate_limiter))
7171+ actor.continue(state)
7272+ }
7373+ Error(_) -> {
7474+ actor.send(client, Error(Nil))
7575+ actor.continue(state)
7676+ }
7777+ }
7878+ }
7979+ Sweep -> {
8080+ let full_buckets =
8181+ state.registry
8282+ |> dict.to_list
8383+ |> list.filter(fn(pair) {
8484+ let #(_, rate_limiter) = pair
8585+ rate_limiter |> rate_limiter.has_full_bucket
8686+ })
8787+ |> list.map(fn(pair) {
8888+ let #(identifier, rate_limiter) = pair
8989+ actor.send(rate_limiter, rate_limiter.Shutdown)
9090+ identifier
9191+ })
9292+9393+ let registry = state.registry |> dict.drop(full_buckets)
9494+9595+ let state = State(..state, registry: registry)
9696+9797+ actor.continue(state)
9898+ }
9999+ }
100100+}
101101+102102+/// Create a new rate limiter registry.
103103+///
104104+pub fn new(
105105+ per_second: Int,
106106+ burst_limit: Int,
107107+) -> Result(RateLimiterRegistryActor(id), Nil) {
108108+ let state =
109109+ State(
110110+ max_token_count: burst_limit,
111111+ token_rate: per_second,
112112+ registry: dict.new(),
113113+ )
114114+ use registry <- result.try(
115115+ actor.start(state, handle_message)
116116+ |> result.nil_error,
117117+ )
118118+119119+ task.async(fn() { sweep_loop(registry) })
120120+121121+ Ok(registry)
122122+}
123123+124124+/// Get the rate limiter for the given id or create a new one if missing.
125125+///
126126+pub fn get_or_create(
127127+ registry: RateLimiterRegistryActor(id),
128128+ identifier: id,
129129+) -> Result(Subject(rate_limiter.Message), Nil) {
130130+ actor.call(registry, GetOrCreate(identifier, _), 10)
131131+}
132132+133133+fn sweep_loop(registry: RateLimiterRegistryActor(id)) {
134134+ process.sleep(10_000)
135135+ sweep(registry)
136136+ sweep_loop(registry)
137137+}
138138+139139+/// Sweep the registry and remove all rate limiters that have a full bucket.
140140+///
141141+pub fn sweep(registry: RateLimiterRegistryActor(id)) {
142142+ actor.send(registry, Sweep)
143143+}
+23
test/glimit_rate_limiter_test.gleam
···11+import gleeunit/should
22+import glimit/rate_limiter
33+44+// TODO: find a way to mock time so we can test the refilling of the rate limiter.
55+66+pub fn rate_limiter_test() {
77+ let limiter = case rate_limiter.new(2, 2) {
88+ Ok(limiter) -> limiter
99+ Error(_) -> panic as "Should be able to create rate limiter"
1010+ }
1111+1212+ limiter
1313+ |> rate_limiter.hit
1414+ |> should.be_ok
1515+1616+ limiter
1717+ |> rate_limiter.hit
1818+ |> should.be_ok
1919+2020+ limiter
2121+ |> rate_limiter.hit
2222+ |> should.be_error
2323+}
+68
test/glimit_registry_test.gleam
···11+import gleeunit/should
22+import glimit/rate_limiter
33+import glimit/registry
44+55+pub fn same_id_same_actor_test() {
66+ let registry = case registry.new(2, 2) {
77+ Ok(registry) -> registry
88+ Error(_) -> {
99+ panic as "Should be able to create a new registry"
1010+ }
1111+ }
1212+1313+ let assert Ok(rate_limiter) = registry |> registry.get_or_create("🚀")
1414+ let assert Ok(same_rate_limiter) = registry |> registry.get_or_create("🚀")
1515+1616+ rate_limiter
1717+ |> should.equal(same_rate_limiter)
1818+}
1919+2020+pub fn other_id_other_actor_test() {
2121+ let registry = case registry.new(2, 2) {
2222+ Ok(registry) -> registry
2323+ Error(_) -> {
2424+ panic as "Should be able to create a new registry"
2525+ }
2626+ }
2727+2828+ let assert Ok(rate_limiter) = registry |> registry.get_or_create("🚀")
2929+ let assert Ok(same_rate_limiter) = registry |> registry.get_or_create("💫")
3030+3131+ rate_limiter
3232+ |> should.not_equal(same_rate_limiter)
3333+}
3434+3535+pub fn sweep_full_bucket_test() {
3636+ let registry = case registry.new(2, 2) {
3737+ Ok(registry) -> registry
3838+ Error(_) -> {
3939+ panic as "Should be able to create a new registry"
4040+ }
4141+ }
4242+4343+ let assert Ok(rate_limiter) = registry |> registry.get_or_create("🚀")
4444+ registry |> registry.sweep
4545+ let assert Ok(new_rate_limiter) = registry |> registry.get_or_create("🚀")
4646+4747+ rate_limiter
4848+ |> should.not_equal(new_rate_limiter)
4949+}
5050+5151+pub fn sweep_not_full_bucket_test() {
5252+ let registry = case registry.new(2, 2) {
5353+ Ok(registry) -> registry
5454+ Error(_) -> {
5555+ panic as "Should be able to create a new registry"
5656+ }
5757+ }
5858+5959+ let assert Ok(rate_limiter) = registry |> registry.get_or_create("🚀")
6060+6161+ let _ = rate_limiter |> rate_limiter.hit
6262+ registry |> registry.sweep
6363+6464+ let assert Ok(new_rate_limiter) = registry |> registry.get_or_create("🚀")
6565+6666+ rate_limiter
6767+ |> should.equal(new_rate_limiter)
6868+}