-
Notifications
You must be signed in to change notification settings - Fork 45
Closed
Description
While running the following script (backed by Upstash Redis) I'm getting a NOSCRIPT error on a flushed Redis instance (SCRIPT FLUSH):
const redis = Redis.fromEnv();
redis.scriptFlush(); // only for debugging
const ratelimit = new Ratelimit({
redis,
prefix: 'auth',
limiter: Ratelimit.slidingWindow(5, '15 m'),
analytics: true,
enableProtection: true,
ephemeralCache: false,
});
try {
const { success, pending, reason, deniedValue } = await ratelimit.limit(user.email, {
ip,
userAgent,
country,
});
await pending;
if (!success) {
log({
message: 'Rate Limit Exceeded',
level: 'warn',
meta: {
ip,
userAgent,
country,
reason,
deniedValue,
},
});
}
} catch (error) {
console.log(error); // Command 1 [ evalsha ] failed: NOSCRIPT No matching script. Please use EVAL.
}I'm aware of the fallback mechanism, so it might be the second evalsha call causing it:
Lines 21 to 38 in 1901192
| try { | |
| return await ctx.redis.evalsha(script.hash, keys, args) | |
| } catch (error) { | |
| if (`${error}`.includes("NOSCRIPT")) { | |
| const hash = await ctx.redis.scriptLoad(script.script) | |
| if (hash !== script.hash) { | |
| console.warn( | |
| "Upstash Ratelimit: Expected hash and the hash received from Redis" | |
| + " are different. Ratelimit will work as usual but performance will" | |
| + " be reduced." | |
| ); | |
| } | |
| return await ctx.redis.evalsha(hash, keys, args) | |
| } | |
| throw error; | |
| } |
Redis log (MONITOR) looks like this:
1728062526.773431 [0 140.82.121.4:58234] "EVALSHA" "e1391e429b699c780eb0480350cd5b7280fd9213" "2" "auth:[email protected]:1920069" "auth:[email protected]:1920068" "5" "1728062526414" "900000" "1"
1728062526.773911 [0 140.82.121.4:58234] "EVAL" "\n -- Checks if values provideed in ARGV are present in the deny lists.\n -- This is done using the allDenyListsKey below.\n\n -- Additionally, checks the status of the ip deny list using the\n -- ipDenyListStatusKey below. Here are the possible states of the\n -- ipDenyListStatusKey key:\n -- * status == -1: set to \"disabled\" with no TTL\n -- * status == -2: not set, meaning that is was set before but expired\n -- * status > 0: set to \"valid\", with a TTL\n --\n -- In the case of status == -2, we set the status to \"pending\" with\n -- 30 second ttl. During this time, the process which got status == -2\n -- will update the ip deny list.\n\n local allDenyListsKey = KEYS[1]\n local ipDenyListStatusKey = KEYS[2]\n\n local results = redis.call('SMISMEMBER', allDenyListsKey, unpack(ARGV))\n local status = redis.call('TTL', ipDenyListStatusKey)\n if status == -2 then\n redis.call('SETEX', ipDenyListStatusKey, 30, \"pending\")\n end\n\n return { results, status }\n" "2" "auth:denyList:all" "auth:ipDenyListStatus" "[email protected]" "127.0.0.1"
1728062526.774460 [0 lua] "SMISMEMBER" "auth:denyList:all" "[email protected]" "127.0.0.1"
1728062526.774815 [0 lua] "TTL" "auth:ipDenyListStatus"
1728062526.775166 [0 lua] "SETEX" "auth:ipDenyListStatus" "30" "pending"
1728062526.876307 [0 140.82.121.4:58234] "SCRIPT" "load" "\n local currentKey = KEYS[1] -- identifier including prefixes\n local previousKey = KEYS[2] -- key of the previous bucket\n local tokens = tonumber(ARGV[1]) -- tokens per window\n local now = ARGV[2] -- current timestamp in milliseconds\n local window = ARGV[3] -- interval in milliseconds\n local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1\n\n local requestsInCurrentWindow = redis.call(\"GET\", currentKey)\n if requestsInCurrentWindow == false then\n requestsInCurrentWindow = 0\n end\n\n local requestsInPreviousWindow = redis.call(\"GET\", previousKey)\n if requestsInPreviousWindow == false then\n requestsInPreviousWindow = 0\n end\n local percentageInCurrent = ( now % window ) / window\n -- weighted requests to consider from the previous window\n requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow)\n if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then\n return -1\n end\n\n local newValue = redis.call(\"INCRBY\", currentKey, incrementBy)\n if newValue == tonumber(incrementBy) then\n -- The first time this key is set, the value will be equal to incrementBy.\n -- So we only need the expire command once\n redis.call(\"PEXPIRE\", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second\n end\n return tokens - ( newValue + requestsInPreviousWindow )\n"
1728062526.908374 [0 140.82.121.4:58234] "EVALSHA" "e1391e429b699c780eb0480350cd5b7280fd9213" "2" "auth:[email protected]:1920069" "auth:[email protected]:1920068" "5" "1728062526414" "900000" "1"
1728062526.908798 [0 lua] "GET" "auth:[email protected]:1920069"
1728062526.909129 [0 lua] "GET" "auth:[email protected]:1920068"
1728062526.909450 [0 lua] "INCRBY" "auth:[email protected]:1920069" "1"
1728062526.909835 [0 lua] "PEXPIRE" "auth:[email protected]:1920069" "1801000"
Metadata
Metadata
Assignees
Labels
No labels