Rate Limiting in Racket

Here’s the translation of the Go rate limiting example to Racket, along with explanations in Markdown format suitable for Hugo:

#lang racket

(require racket/async-channel)

(define (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.
  (define requests (make-async-channel 5))
  (for ([i (in-range 1 6)])
    (async-channel-put requests i))
  (async-channel-put requests eof)

  ; This `limiter` thread will send a value
  ; every 200 milliseconds. This is the regulator in
  ; our rate limiting scheme.
  (define limiter (thread
                   (lambda ()
                     (let loop ()
                       (sleep 0.2)
                       (sync (thread-send-evt (current-thread) 'tick))
                       (loop)))))

  ; By blocking on a receive from the `limiter` thread
  ; before serving each request, we limit ourselves to
  ; 1 request every 200 milliseconds.
  (let loop ()
    (define req (async-channel-get requests))
    (when (not (eof-object? req))
      (thread-receive)
      (printf "request ~a ~a\n" req (current-seconds))
      (loop)))

  ; 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 `bursty-limiter`
  ; channel will allow bursts of up to 3 events.
  (define bursty-limiter (make-async-channel 3))

  ; Fill up the channel to represent allowed bursting.
  (for ([_ (in-range 3)])
    (async-channel-put bursty-limiter (current-seconds)))

  ; Every 200 milliseconds we'll try to add a new
  ; value to `bursty-limiter`, up to its limit of 3.
  (thread
   (lambda ()
     (let loop ()
       (sleep 0.2)
       (async-channel-put bursty-limiter (current-seconds))
       (loop))))

  ; Now simulate 5 more incoming requests. The first
  ; 3 of these will benefit from the burst capability
  ; of `bursty-limiter`.
  (define bursty-requests (make-async-channel 5))
  (for ([i (in-range 1 6)])
    (async-channel-put bursty-requests i))
  (async-channel-put bursty-requests eof)

  (let loop ()
    (define req (async-channel-get bursty-requests))
    (when (not (eof-object? req))
      (async-channel-get bursty-limiter)
      (printf "request ~a ~a\n" req (current-seconds))
      (loop))))

(main)

Running our program we see the first batch of requests handled once every ~200 milliseconds as desired.

$ racket rate-limiting.rkt
request 1 1621234567
request 2 1621234567
request 3 1621234568
request 4 1621234568
request 5 1621234569

For the second batch of requests we serve the first 3 immediately because of the burstable rate limiting, then serve the remaining 2 with ~200ms delays each.

request 1 1621234569
request 2 1621234569
request 3 1621234569
request 4 1621234570
request 5 1621234570

In this Racket implementation:

  1. We use async-channel to simulate channels.
  2. The thread function is used to create new threads, similar to goroutines.
  3. The sleep function is used to introduce delays.
  4. We use thread-send-evt and thread-receive to simulate tickers.
  5. The current-seconds function is used instead of time.Now() for simplicity.

Note that Racket doesn’t have built-in rate limiting or ticker functionality like Go, so we’ve simulated these concepts using threads and sleep. The overall structure and logic of the program remain similar to the original Go version.