Skip to content

Commit 377f72c

Browse files
authored
Merge pull request #120 from upstash/DX-1116
Hardcode script hash values and deprecate cacheScripts
2 parents bd64e78 + 6477b03 commit 377f72c

File tree

6 files changed

+174
-95
lines changed

6 files changed

+174
-95
lines changed

src/hash.ts

Lines changed: 20 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,38 @@
1+
import { ScriptInfo } from "./lua-scripts/hash";
12
import { Context, RegionContext } from "./types"
23

3-
type ScriptKind = "limitHash" | "getRemainingHash" | "resetHash"
4-
5-
/**
6-
* Loads the scripts to redises with SCRIPT LOAD if the first region context
7-
* doesn't have the kind of script hash in it.
8-
*
9-
* @param ctx Regional or multi region context
10-
* @param script script to load
11-
* @param kind script kind
12-
*/
13-
const setHash = async (
14-
ctx: Context,
15-
script: string,
16-
kind: ScriptKind
17-
) => {
18-
const regionContexts = "redis" in ctx ? [ctx] : ctx.regionContexts
19-
const hashSample = regionContexts[0].scriptHashes[kind]
20-
if (!hashSample) {
21-
await Promise.all(regionContexts.map(async (context) => {
22-
context.scriptHashes[kind] = await context.redis.scriptLoad(script)
23-
}));
24-
};
25-
}
26-
274
/**
28-
* Runds the specified script with EVALSHA if ctx.cacheScripts or EVAL
29-
* otherwise.
5+
* Runs the specified script with EVALSHA using the scriptHash parameter.
306
*
31-
* If the script is not found when EVALSHA is used, it submits the script
32-
* with LOAD SCRIPT, then calls EVALSHA again.
7+
* If the EVALSHA fails, loads the script to redis and runs again with the
8+
* hash returned from Redis.
339
*
3410
* @param ctx Regional or multi region context
35-
* @param script script to run
36-
* @param kind script kind
37-
* @param keys
38-
* @param args
11+
* @param script ScriptInfo of script to run. Contains the script and its hash
12+
* @param keys eval keys
13+
* @param args eval args
3914
*/
4015
export const safeEval = async (
4116
ctx: RegionContext,
42-
script: string,
43-
kind: ScriptKind,
17+
script: ScriptInfo,
4418
keys: any[],
4519
args: any[],
4620
) => {
47-
if (!ctx.cacheScripts) {
48-
return await ctx.redis.eval(script, keys, args);
49-
};
50-
51-
await setHash(ctx, script, kind);
5221
try {
53-
return await ctx.redis.evalsha(ctx.scriptHashes[kind]!, keys, args)
22+
return await ctx.redis.evalsha(script.hash, keys, args)
5423
} catch (error) {
5524
if (`${error}`.includes("NOSCRIPT")) {
56-
console.log("Script with the expected hash was not found in redis db. It is probably flushed. Will load another scipt before continuing.");
57-
ctx.scriptHashes[kind] = undefined;
58-
await setHash(ctx, script, kind)
59-
console.log(" New script successfully loaded.")
60-
return await ctx.redis.evalsha(ctx.scriptHashes[kind]!, keys, args)
25+
const hash = await ctx.redis.scriptLoad(script.script)
26+
27+
if (hash !== script.hash) {
28+
console.warn(
29+
"Upstash Ratelimit: Expected hash and the hash received from Redis"
30+
+ " are different. Ratelimit will work as usual but performance will"
31+
+ " be reduced."
32+
);
33+
}
34+
35+
return await ctx.redis.evalsha(hash, keys, args)
6136
}
6237
throw error;
6338
}

src/lua-scripts/hash.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Redis } from "@upstash/redis";
2+
import { describe, expect, test } from "bun:test";
3+
import { RESET_SCRIPT, SCRIPTS } from "./hash";
4+
5+
describe("should use correct hash for lua scripts", () => {
6+
const redis = Redis.fromEnv();
7+
8+
const validateHash = async (script: string, expectedHash: string) => {
9+
const hash = await redis.scriptLoad(script)
10+
expect(hash).toBe(expectedHash)
11+
}
12+
13+
const algorithms = [
14+
...Object.entries(SCRIPTS.singleRegion), ...Object.entries(SCRIPTS.multiRegion)
15+
]
16+
17+
// for each algorithm (fixedWindow, slidingWindow etc)
18+
for (const [algorithm, scripts] of algorithms) {
19+
describe(`${algorithm}`, () => {
20+
// for each method (limit & getRemaining)
21+
for (const [method, scriptInfo] of Object.entries(scripts)) {
22+
test(method, async () => {
23+
await validateHash(scriptInfo.script, scriptInfo.hash)
24+
})
25+
}
26+
})
27+
}
28+
29+
test("reset script", async () => {
30+
await validateHash(RESET_SCRIPT.script, RESET_SCRIPT.hash)
31+
})
32+
})

src/lua-scripts/hash.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as Single from "./single"
2+
import * as Multi from "./multi"
3+
import { resetScript } from "./reset"
4+
5+
export type ScriptInfo = {
6+
script: string,
7+
hash: string
8+
}
9+
10+
type Algorithm = {
11+
limit: ScriptInfo,
12+
getRemaining: ScriptInfo,
13+
}
14+
15+
type AlgorithmKind =
16+
| "fixedWindow"
17+
| "slidingWindow"
18+
| "tokenBucket"
19+
| "cachedFixedWindow"
20+
21+
export const SCRIPTS: {
22+
singleRegion: Record<AlgorithmKind, Algorithm>,
23+
multiRegion: Record<Exclude<AlgorithmKind, "tokenBucket" | "cachedFixedWindow">, Algorithm>,
24+
} = {
25+
singleRegion: {
26+
fixedWindow: {
27+
limit: {
28+
script: Single.fixedWindowLimitScript,
29+
hash: "b13943e359636db027ad280f1def143f02158c13"
30+
},
31+
getRemaining: {
32+
script: Single.fixedWindowRemainingTokensScript,
33+
hash: "8c4c341934502aee132643ffbe58ead3450e5208"
34+
},
35+
},
36+
slidingWindow: {
37+
limit: {
38+
script: Single.slidingWindowLimitScript,
39+
hash: "e1391e429b699c780eb0480350cd5b7280fd9213"
40+
},
41+
getRemaining: {
42+
script: Single.slidingWindowRemainingTokensScript,
43+
hash: "65a73ac5a05bf9712903bc304b77268980c1c417"
44+
},
45+
},
46+
tokenBucket: {
47+
limit: {
48+
script: Single.tokenBucketLimitScript,
49+
hash: "5bece90aeef8189a8cfd28995b479529e270b3c6"
50+
},
51+
getRemaining: {
52+
script: Single.tokenBucketRemainingTokensScript,
53+
hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0"
54+
},
55+
},
56+
cachedFixedWindow: {
57+
limit: {
58+
script: Single.cachedFixedWindowLimitScript,
59+
hash: "c26b12703dd137939b9a69a3a9b18e906a2d940f"
60+
},
61+
getRemaining: {
62+
script: Single.cachedFixedWindowRemainingTokenScript,
63+
hash: "8e8f222ccae68b595ee6e3f3bf2199629a62b91a"
64+
},
65+
}
66+
},
67+
multiRegion: {
68+
fixedWindow: {
69+
limit: {
70+
script: Multi.fixedWindowLimitScript,
71+
hash: "a8c14f3835aa87bd70e5e2116081b81664abcf5c"
72+
},
73+
getRemaining: {
74+
script: Multi.fixedWindowRemainingTokensScript,
75+
hash: "8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd"
76+
},
77+
},
78+
slidingWindow: {
79+
limit: {
80+
script: Multi.slidingWindowLimitScript,
81+
hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b"
82+
},
83+
getRemaining: {
84+
script: Multi.slidingWindowRemainingTokensScript,
85+
hash: "558c9306b7ec54abb50747fe0b17e5d44bd24868"
86+
},
87+
},
88+
}
89+
}
90+
91+
/** COMMON */
92+
export const RESET_SCRIPT: ScriptInfo = {
93+
script: resetScript,
94+
hash: "54bd274ddc59fb3be0f42deee2f64322a10e2b50"
95+
}

src/multi.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Cache } from "./cache";
22
import type { Duration } from "./duration";
33
import { ms } from "./duration";
44
import { safeEval } from "./hash";
5+
import { RESET_SCRIPT, SCRIPTS } from "./lua-scripts/hash";
56
import {
67
fixedWindowLimitScript,
78
fixedWindowRemainingTokensScript,
@@ -115,8 +116,6 @@ export class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
115116
ctx: {
116117
regionContexts: config.redis.map(redis => ({
117118
redis: redis,
118-
scriptHashes: {},
119-
cacheScripts: config.cacheScripts ?? true,
120119
})),
121120
cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : undefined,
122121
},
@@ -178,8 +177,7 @@ export class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
178177
redis: regionContext.redis,
179178
request: safeEval(
180179
regionContext,
181-
fixedWindowLimitScript,
182-
"limitHash",
180+
SCRIPTS.multiRegion.fixedWindow.limit,
183181
[key],
184182
[requestId, windowDuration, incrementBy],
185183
) as Promise<string[]>,
@@ -284,8 +282,7 @@ export class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
284282
redis: regionContext.redis,
285283
request: safeEval(
286284
regionContext,
287-
fixedWindowRemainingTokensScript,
288-
"getRemainingHash",
285+
SCRIPTS.multiRegion.fixedWindow.getRemaining,
289286
[key],
290287
[null]
291288
) as Promise<string[]>,
@@ -316,8 +313,7 @@ export class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
316313
await Promise.all(ctx.regionContexts.map((regionContext) => {
317314
safeEval(
318315
regionContext,
319-
resetScript,
320-
"resetHash",
316+
RESET_SCRIPT,
321317
[pattern],
322318
[null]
323319
);
@@ -385,8 +381,7 @@ export class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
385381
redis: regionContext.redis,
386382
request: safeEval(
387383
regionContext,
388-
slidingWindowLimitScript,
389-
"limitHash",
384+
SCRIPTS.multiRegion.slidingWindow.limit,
390385
[currentKey, previousKey],
391386
[tokens, now, windowDuration, requestId, incrementBy],
392387
// lua seems to return `1` for true and `null` for false
@@ -508,8 +503,7 @@ export class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
508503
redis: regionContext.redis,
509504
request: safeEval(
510505
regionContext,
511-
slidingWindowRemainingTokensScript,
512-
"getRemainingHash",
506+
SCRIPTS.multiRegion.slidingWindow.getRemaining,
513507
[currentKey, previousKey],
514508
[now, windowSize],
515509
// lua seems to return `1` for true and `null` for false
@@ -532,8 +526,7 @@ export class MultiRegionRatelimit extends Ratelimit<MultiRegionContext> {
532526
await Promise.all(ctx.regionContexts.map((regionContext) => {
533527
safeEval(
534528
regionContext,
535-
resetScript,
536-
"resetHash",
529+
RESET_SCRIPT,
537530
[pattern],
538531
[null]
539532
);

0 commit comments

Comments
 (0)