Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/lua/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ export async function evalScript<T = any>(
argv: Array<string>,
numKeys: number,
): Promise<T> {
const sha = await loadScript(client, name);
return (client as any).evalsha(sha, numKeys, ...argv);
const runEvalSha = async () => {
const sha = await loadScript(client, name);
return (client as any).evalsha(sha, numKeys, ...argv) as Promise<T>;
};

try {
return await runEvalSha();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!message.includes('NOSCRIPT')) {
throw error;
}

cacheByClient.get(client)?.delete(name);
return await runEvalSha();
}
}
49 changes: 49 additions & 0 deletions test/lua-loader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest';
import { evalScript } from '../src/lua/loader';

describe('Lua script loader', () => {
it('reloads and retries once when Redis returns NOSCRIPT', async () => {
let loadCount = 0;
const evalshaCalls: string[] = [];
const redis = {
script: async (command: string) => {
expect(command).toBe('load');
loadCount += 1;
return `sha-${loadCount}`;
},
evalsha: async (sha: string) => {
evalshaCalls.push(sha);
if (evalshaCalls.length === 1) {
throw new Error('NOSCRIPT No matching script. Please use EVAL.');
}
return 'ok';
},
};

await expect(
evalScript(redis as any, 'enqueue', ['test-namespace'], 1),
).resolves.toBe('ok');

expect(loadCount).toBe(2);
expect(evalshaCalls).toEqual(['sha-1', 'sha-2']);
});

it('does not retry non-NOSCRIPT Redis errors', async () => {
let loadCount = 0;
const redis = {
script: async () => {
loadCount += 1;
return `sha-${loadCount}`;
},
evalsha: async () => {
throw new Error('READONLY You cannot write against a read only replica.');
},
};

await expect(
evalScript(redis as any, 'enqueue', ['test-namespace'], 1),
).rejects.toThrow('READONLY');

expect(loadCount).toBe(1);
});
});
19 changes: 19 additions & 0 deletions test/queue.redis-disconnect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,25 @@ describe('Redis Disconnect/Reconnect Tests', () => {
await redis.quit();
});

it('should reload cached Lua scripts after Redis SCRIPT FLUSH', async () => {
const redis = new Redis(REDIS_URL);
const q = new Queue({ redis, namespace: `${namespace}:script-flush` });

await q.add({ groupId: 'script-flush-group', data: { phase: 'before' } });

// Redis does not persist its Lua script cache across restarts/failovers.
// SCRIPT FLUSH reproduces the NOSCRIPT condition without restarting Redis.
await (redis as any).script('flush');

await expect(
q.add({ groupId: 'script-flush-group', data: { phase: 'after' } }),
).resolves.toBeDefined();

expect(await q.getWaitingCount()).toBe(2);

await redis.quit();
});

it('should handle network partitions and blocking operations', async () => {
const redis = new Redis(REDIS_URL, {
connectTimeout: 1000,
Expand Down