Skip to content

Commit

Permalink
Remove call to TIME in Lua scripts
Browse files Browse the repository at this point in the history
TIME is not deterministic and is rejected in some Redis deployments,
such as AWS ElasticCache Servless Redis.

Rather than calling TIME within the lua script, we pass the information
as arguments.
  • Loading branch information
klette committed Feb 8, 2024
1 parent 3e08cea commit af18fd5
Show file tree
Hide file tree
Showing 2 changed files with 22 additions and 4 deletions.
5 changes: 3 additions & 2 deletions limiters/token_bucket.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ redis.replicate_commands()
local capacity = tonumber(ARGV[1])
local refill_amount = tonumber(ARGV[2])
local time_between_slots = tonumber(ARGV[3]) * 1000 -- ms
local seconds = tonumber(ARGV[4])
local microseconds = tonumber(ARGV[5])

-- Keys
local data_key = KEYS[1]

-- Get current time (ms timestamp)
local redis_time = redis.call('TIME') -- Array of [seconds, microseconds]
local now = tonumber(redis_time[1]) * 1000 + (tonumber(redis_time[2]) / 1000)
local now = tonumber(seconds) * 1000 + (tonumber(microseconds) / 1000)

-- Instantiate default bucket values
-- These are only used if a bucket doesn't already exist
Expand Down
21 changes: 19 additions & 2 deletions limiters/token_bucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@
logger = logging.getLogger(__name__)


def create_redis_time_tuple() -> tuple[int, int]:
"""
Create a tuple of two integers representing the current time in seconds and microseconds.
This mimmicks the TIME command in Redis, which returns the current time in seconds and microseconds.
See: https://redis.io/commands/time/
"""
now = time.time()
seconds_part = int(now)
microseconds_part = int((now - seconds_part) * 1_000_000)
return seconds_part, microseconds_part


class TokenBucketBase(BaseModel):
name: str
capacity: int = Field(gt=0)
Expand Down Expand Up @@ -60,10 +73,12 @@ def __enter__(self) -> float:
Call the token bucket Lua script, receive a datetime for
when to wake up, then sleep up until that point in time.
"""

# Retrieve timestamp for when to wake up from Redis
seconds, microseconds = create_redis_time_tuple()
timestamp: int = self.script(
keys=[self.key],
args=[self.capacity, self.refill_amount, self.refill_frequency],
args=[self.capacity, self.refill_amount, self.refill_frequency, seconds, microseconds],
)

# Estimate sleep time
Expand Down Expand Up @@ -91,10 +106,12 @@ async def __aenter__(self) -> None:
Call the token bucket Lua script, receive a datetime for
when to wake up, then sleep up until that point in time.
"""

# Retrieve timestamp for when to wake up from Redis
seconds, microseconds = create_redis_time_tuple()
timestamp = await self.script(
keys=[self.key],
args=[self.capacity, self.refill_amount, self.refill_frequency],
args=[self.capacity, self.refill_amount, self.refill_frequency, seconds, microseconds],
)

# Estimate sleep time
Expand Down

0 comments on commit af18fd5

Please sign in to comment.