Rate Limiting in Lua
Here’s the translation of the Go rate limiting example to Lua, formatted in Markdown suitable for Hugo:
Rate limiting is an important mechanism for controlling resource utilization and maintaining quality of service. Lua can support rate limiting using coroutines and timers.
local clock = os.clock
-- Simulate a simple channel
local function make_channel(size)
local channel = {}
local queue = {}
local closed = false
function channel.send(value)
if closed then error("send on closed channel") end
if #queue < size then
table.insert(queue, value)
else
coroutine.yield()
end
end
function channel.receive()
if #queue == 0 and closed then return nil end
while #queue == 0 do
coroutine.yield()
end
return table.remove(queue, 1)
end
function channel.close()
closed = true
end
return channel
end
-- Simulate time.Tick
local function tick(interval)
local channel = make_channel(1)
local function ticker()
while true do
channel.send(clock())
coroutine.yield(interval)
end
end
coroutine.wrap(ticker)()
return channel
end
local function 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.
local requests = make_channel(5)
for i = 1, 5 do
requests.send(i)
end
requests.close()
-- This limiter channel will receive a value
-- every 200 milliseconds. This is the regulator in
-- our rate limiting scheme.
local limiter = tick(0.2)
-- By blocking on a receive from the limiter channel
-- before serving each request, we limit ourselves to
-- 1 request every 200 milliseconds.
while true do
local req = requests.receive()
if not req then break end
limiter.receive()
print(string.format("request %d %f", req, clock()))
end
-- 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.
local burstyLimiter = make_channel(3)
-- Fill up the channel to represent allowed bursting.
for i = 1, 3 do
burstyLimiter.send(clock())
end
-- Every 200 milliseconds we'll try to add a new
-- value to burstyLimiter, up to its limit of 3.
coroutine.wrap(function()
for t in coroutine.wrap(function() while true do coroutine.yield(tick(0.2).receive()) end end) do
burstyLimiter.send(t)
end
end)()
-- Now simulate 5 more incoming requests. The first
-- 3 of these will benefit from the burst capability
-- of burstyLimiter.
local burstyRequests = make_channel(5)
for i = 1, 5 do
burstyRequests.send(i)
end
burstyRequests.close()
while true do
local req = burstyRequests.receive()
if not req then break end
burstyLimiter.receive()
print(string.format("request %d %f", req, clock()))
end
end
main()
Running our program we see the first batch of requests handled once every ~200 milliseconds as desired.
$ lua rate-limiting.lua
request 1 0.000039
request 2 0.200161
request 3 0.400284
request 4 0.600407
request 5 0.800530
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 0.800568
request 2 0.800576
request 3 0.800582
request 4 1.000705
request 5 1.200828
This Lua implementation simulates the behavior of Go’s channels and goroutines using coroutines and custom channel implementations. The tick
function mimics Go’s time.Tick
, and the make_channel
function creates a simple channel-like structure. The overall logic of rate limiting remains the same, demonstrating both basic and bursty rate limiting.