Rate Limiting in Clojure
(ns rate-limiting
(:require [clojure.core.async :as a]
[clojure.java.io :as io]))
(defn now []
(java.time.LocalDateTime/now))
(defn main []
; First we'll look at basic rate limiting. Suppose
; we want to limit our handling of incoming requests.
; We'll serve these requests off a channel of the
; same name.
(let [requests (a/chan 5)]
(doseq [i (range 1 6)]
(a/>!! requests i))
(a/close! requests)
; This limiter channel will receive a value
; every 200 milliseconds. This is the regulator in
; our rate limiting scheme.
(let [limiter (a/ticker 200)]
; By blocking on a receive from the limiter channel
; before serving each request, we limit ourselves to
; 1 request every 200 milliseconds.
(doseq [req (a/<!! (a/into [] requests))]
(a/<!! limiter)
(println "request" req (now))))
; We may want to allow short bursts of requests in
; our rate limiting scheme while preserving the
; overall rate limit. We can accomplish this by
; buffering our limiter channel. This burstyLimiter
; channel will allow bursts of up to 3 events.
(let [bursty-limiter (a/chan 3)]
; Fill up the channel to represent allowed bursting.
(dotimes [_ 3]
(a/>!! bursty-limiter (now)))
; Every 200 milliseconds we'll try to add a new
; value to burstyLimiter, up to its limit of 3.
(a/go-loop []
(when (a/>! bursty-limiter (now))
(a/<! (a/timeout 200))
(recur)))
; Now simulate 5 more incoming requests. The first
; 3 of these will benefit from the burst capability
; of burstyLimiter.
(let [bursty-requests (a/chan 5)]
(doseq [i (range 1 6)]
(a/>!! bursty-requests i))
(a/close! bursty-requests)
(doseq [req (a/<!! (a/into [] bursty-requests))]
(a/<!! bursty-limiter)
(println "request" req (now))))))
(main)This Clojure code implements rate limiting using core.async channels, which provide similar functionality to Go’s channels and goroutines. Here’s an explanation of the key parts:
We use
clojure.core.asyncfor channel operations and concurrency.The
requestschannel simulates incoming requests.The
limiteris created usinga/ticker, which sends a value every 200 milliseconds.We use
doseqanda/<!!to iterate over the requests, waiting for the limiter before processing each one.For bursty rate limiting, we create a
bursty-limiterchannel with a buffer of 3.We use
a/go-loopto continuously try to add new values to thebursty-limiterevery 200 milliseconds.Finally, we simulate bursty requests and process them using the
bursty-limiter.
Running this program will show the first batch of requests handled once every ~200 milliseconds as desired. For the second batch of requests, the first 3 will be served immediately due to the burstable rate limiting, then the remaining 2 with ~200ms delays each.
Note that Clojure’s concurrency model is different from Go’s, but core.async provides similar abstractions that allow us to implement this rate limiting pattern effectively.
Comments powered by Disqus