Rate Limiting in Lisp

Here’s the translation of the rate limiting example from Go to Lisp, formatted in Markdown suitable for Hugo:

Rate limiting is an important mechanism for controlling resource utilization and maintaining quality of service. Common Lisp supports rate limiting through the use of threads and timing functions.

(defpackage :rate-limiting
  (:use :cl :bordeaux-threads :local-time))

(in-package :rate-limiting)

(defun main ()
  ;; First we'll look at basic rate limiting. Suppose
  ;; we want to limit our handling of incoming requests.
  ;; We'll serve these requests from a queue.
  (let ((requests (make-queue)))
    (dotimes (i 5)
      (enqueue (1+ i) requests))

  ;; This limiter function will sleep for 200 milliseconds
  ;; between each request. This is the regulator in
  ;; our rate limiting scheme.
  (defun limiter ()
    (sleep 0.2))

  ;; By calling the limiter function before serving each request,
  ;; we limit ourselves to 1 request every 200 milliseconds.
  (loop while (not (queue-empty-p requests))
        do (progn
             (limiter)
             (format t "request ~a ~a~%" 
                     (dequeue requests) 
                     (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
  ;; using a token bucket algorithm.
  (let* ((burst-limit 3)
         (tokens burst-limit)
         (last-token-time (now)))

    (defun add-token ()
      (let ((current-time (now)))
        (when (> (timestamp-difference current-time last-token-time)
                 0.2)
          (setf tokens (min burst-limit (1+ tokens)))
          (setf last-token-time current-time))))

    (defun take-token ()
      (add-token)
      (when (> tokens 0)
        (decf tokens)
        t))

    ;; Create a thread to continuously add tokens
    (make-thread
     (lambda ()
       (loop
         (sleep 0.2)
         (add-token))))

    ;; Now simulate 5 more incoming requests. The first
    ;; 3 of these will benefit from the burst capability.
    (let ((bursty-requests (make-queue)))
      (dotimes (i 5)
        (enqueue (1+ i) bursty-requests))

      (loop while (not (queue-empty-p bursty-requests))
            do (progn
                 (loop until (take-token)
                       do (sleep 0.01))
                 (format t "request ~a ~a~%" 
                         (dequeue bursty-requests) 
                         (now))))))))

(main)

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

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

This Lisp implementation uses the bordeaux-threads library for threading and the local-time library for timestamp operations. The make-queue, enqueue, dequeue, and queue-empty-p functions are assumed to be implemented elsewhere or could be replaced with a standard Lisp list for simplicity.

The token bucket algorithm is used to implement the bursty limiter, which allows for short bursts of requests while maintaining the overall rate limit. This approach is more idiomatic in Lisp compared to the channel-based approach in the original example.

Note that the exact timing and output format may differ slightly from the original due to differences in how Lisp and Go handle concurrency and time, but the core concepts of rate limiting are preserved.