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:

  1. We use clojure.core.async for channel operations and concurrency.

  2. The requests channel simulates incoming requests.

  3. The limiter is created using a/ticker, which sends a value every 200 milliseconds.

  4. We use doseq and a/<!! to iterate over the requests, waiting for the limiter before processing each one.

  5. For bursty rate limiting, we create a bursty-limiter channel with a buffer of 3.

  6. We use a/go-loop to continuously try to add new values to the bursty-limiter every 200 milliseconds.

  7. 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