Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node: Add SCRIPT commands #2267

Merged
merged 5 commits into from
Sep 11, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
* Node: Added binary variant to stream commands ([#2200](https://github.com/valkey-io/valkey-glide/pull/2200), [#2222](https://github.com/valkey-io/valkey-glide/pull/2222))
* Python, Node, Java: change BITCOUNT end param to optional ([#2248](https://github.com/valkey-io/valkey-glide/pull/2248))
* Python: Add Script commands ([#2208](https://github.com/valkey-io/valkey-glide/pull/2208))
* Node: Added Script commands ([#2267](https://github.com/valkey-io/valkey-glide/pull/2267))

#### Breaking Changes
* Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005))
Expand Down
3 changes: 2 additions & 1 deletion node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3644,7 +3644,8 @@ export class BaseClient {
return this.createWritePromise(createTTL(key));
}

/** Invokes a Lua script with its keys and arguments.
/**
* Invokes a Lua script with its keys and arguments.
* This method simplifies the process of invoking scripts on a Valkey server by using an object that represents a Lua script.
* The script loading, argument preparation, and execution will all be handled internally. If the script has not already been loaded,
* it will be loaded automatically using the `SCRIPT LOAD` command. After that, it will be invoked using the `EVALSHA` command.
Expand Down
25 changes: 25 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4081,3 +4081,28 @@ export function createXGroupSetid(

return createCommand(RequestType.XGroupSetId, args);
}

/**
* @internal
*/
export function createScriptExists(
sha1s: GlideString[],
): command_request.Command {
return createCommand(RequestType.ScriptExists, sha1s);
}

/**
* @internal
*/
export function createScriptFlush(mode?: FlushMode): command_request.Command {
if (mode) {
return createCommand(RequestType.ScriptFlush, [mode.toString()]);
} else {
return createCommand(RequestType.ScriptFlush, []);
}
}

/** @internal */
export function createScriptKill(): command_request.Command {
return createCommand(RequestType.ScriptKill, []);
}
60 changes: 60 additions & 0 deletions node/src/GlideClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ import {
createPing,
createPublish,
createRandomKey,
createScriptExists,
createScriptFlush,
createScriptKill,
createSelect,
createSort,
createSortReadOnly,
Expand Down Expand Up @@ -1020,4 +1023,61 @@ export class GlideClient extends BaseClient {
decoder: Decoder.String,
});
}

/**
* Check existence of scripts in the script cache by their SHA1 digest.
*
* @see {@link https://valkey.io/commands/script-exists/|valkey.io} for more details.
*
* @param sha1s - List of SHA1 digests of the scripts to check.
* @returns A list of boolean values indicating the existence of each script.
*
* @example
* ```typescript
* console result = await client.scriptExists(["sha1_digest1", "sha1_digest2"]);
* console.log(result); // Output: [true, false]
* ```
*/
public async scriptExists(sha1s: GlideString[]): Promise<boolean[]> {
return this.createWritePromise(createScriptExists(sha1s));
}

/**
* Flush the Lua scripts cache.
*
* @see {@link https://valkey.io/commands/script-flush/|valkey.io} for more details.
*
* @param mode - (Optional) The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}.
* @returns A simple `"OK"` response.
*
* @example
* ```typescript
* console result = await client.scriptFlush(FlushMode.SYNC);
* console.log(result); // Output: "OK"
* ```
*/
public async scriptFlush(mode?: FlushMode): Promise<"OK"> {
return this.createWritePromise(createScriptFlush(mode), {
decoder: Decoder.String,
});
}

/**
* Kill the currently executing Lua script, assuming no write operation was yet performed by the script.
*
* @see {@link https://valkey.io/commands/script-kill/|valkey.io} for more details.
*
* @returns A simple `"OK"` response.
*
* @example
* ```typescript
* console result = await client.scriptKill();
* console.log(result); // Output: "OK"
* ```
*/
public async scriptKill(): Promise<"OK"> {
return this.createWritePromise(createScriptKill(), {
decoder: Decoder.String,
});
}
}
78 changes: 76 additions & 2 deletions node/src/GlideClusterClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
BaseClientConfiguration,
Decoder,
DecoderOption,
GlideRecord,
GlideRecord, // eslint-disable-line @typescript-eslint/no-unused-vars
GlideReturnType,
GlideString,
PubSubMsg,
ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars
GlideReturnType,
convertGlideRecordToRecord,
} from "./BaseClient";
import {
Expand Down Expand Up @@ -54,6 +54,9 @@ import {
createPublish,
createPubsubShardChannels,
createRandomKey,
createScriptExists,
createScriptFlush,
createScriptKill,
createSort,
createSortReadOnly,
createTime,
Expand Down Expand Up @@ -1426,4 +1429,75 @@ export class GlideClusterClient extends BaseClient {
...options,
});
}

/**
* Check existence of scripts in the script cache by their SHA1 digest.
*
* @see {@link https://valkey.io/commands/script-exists/|valkey.io} for more details.
*
* @param sha1s - List of SHA1 digests of the scripts to check.
* @param options - (Optional) See {@link RouteOption}.
* @returns A list of boolean values indicating the existence of each script.
*
* @example
* ```typescript
* console result = await client.scriptExists(["sha1_digest1", "sha1_digest2"]);
* console.log(result); // Output: [true, false]
* ```
*/
public async scriptExists(
sha1s: GlideString[],
options?: RouteOption,
): Promise<ClusterResponse<boolean[]>> {
return this.createWritePromise(createScriptExists(sha1s), options);
}

/**
* Flush the Lua scripts cache.
*
* @see {@link https://valkey.io/commands/script-flush/|valkey.io} for more details.
*
* @param options - (Optional) Additional parameters:
* - (Optional) `mode`: the flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}.
* - (Optional) `route`: see {@link RouteOption}.
* @returns A simple `"OK"` response.
*
* @example
* ```typescript
* console result = await client.scriptFlush(FlushMode.SYNC);
* console.log(result); // Output: "OK"
* ```
*/
public async scriptFlush(
options?: {
mode?: FlushMode;
} & RouteOption,
): Promise<"OK"> {
return this.createWritePromise(createScriptFlush(options?.mode), {
decoder: Decoder.String,
...options,
});
}

/**
* Kill the currently executing Lua script, assuming no write operation was yet performed by the script.
*
* @see {@link https://valkey.io/commands/script-kill/|valkey.io} for more details.
* @remarks The command is routed to all nodes, and aggregates the response to a single array.
*
* @param options - (Optional) See {@link RouteOption}.
* @returns A simple `"OK"` response.
*
* @example
* ```typescript
* console result = await client.scriptKill();
* console.log(result); // Output: "OK"
* ```
*/
public async scriptKill(options?: RouteOption): Promise<"OK"> {
return this.createWritePromise(createScriptKill(), {
decoder: Decoder.String,
...options,
});
}
}
130 changes: 130 additions & 0 deletions node/tests/GlideClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
HashDataType,
ProtocolVersion,
RequestError,
Script,
SortOrder,
Transaction,
} from "..";
Expand All @@ -33,6 +34,7 @@ import {
checkFunctionListResponse,
checkFunctionStatsResponse,
convertStringArrayToBuffer,
createLongRunningLuaScript,
createLuaLibWithLongRunningFunction,
DumpAndRestureTest,
encodableTransactionTest,
Expand All @@ -45,6 +47,7 @@ import {
transactionTest,
validateTransactionResponse,
waitForNotBusy,
waitForScriptNotBusy,
} from "./TestUtilities";

const TIMEOUT = 50000;
Expand Down Expand Up @@ -1621,6 +1624,133 @@ describe("GlideClient", () => {
TIMEOUT,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
"script kill unkillable test_%p",
async (protocol) => {
const config = getClientConfigurationOption(
cluster.getAddresses(),
protocol,
{ requestTimeout: 10000 },
);
const client1 = await GlideClient.createClient(config);
const client2 = await GlideClient.createClient(config);

// Verify that script kill raises an error when no script is running
await expect(client1.scriptKill()).rejects.toThrow(
"No scripts in execution right now",
);

// Create a long-running script
const longScript = new Script(createLongRunningLuaScript(5, true));
let promise = null;

try {
// call the script without await
promise = client2.invokeScript(longScript, {
keys: ["{key}-" + uuidv4()],
});

let foundUnkillable = false;
let timeout = 4000;
await new Promise((resolve) => setTimeout(resolve, 1000));

while (timeout >= 0) {
try {
// keep trying to kill until we get an "OK"
await client1.scriptKill();
} catch (err) {
// a RequestError may occur if the script is not yet running
// sleep and try again
if (
(err as Error).message
.toLowerCase()
.includes("unkillable")
) {
foundUnkillable = true;
break;
}
}

await new Promise((resolve) => setTimeout(resolve, 500));
timeout -= 500;
}

expect(foundUnkillable).toBeTruthy();
} finally {
// If script wasn't killed, and it didn't time out - it blocks the server and cause the
// test to fail. Wait for the script to complete (we cannot kill it)
expect(await promise).toContain("Timed out");
client1.close();
client2.close();
}
},
TIMEOUT,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
"script kill killable test_%p",
async (protocol) => {
const config = getClientConfigurationOption(
cluster.getAddresses(),
protocol,
{ requestTimeout: 10000 },
);
const client1 = await GlideClient.createClient(config);
const client2 = await GlideClient.createClient(config);

try {
// Verify that script kill raises an error when no script is running
await expect(client1.scriptKill()).rejects.toThrow(
"No scripts in execution right now",
);

// Create a long-running script
const longScript = new Script(
createLongRunningLuaScript(5, false),
);

try {
// call the script without await
const promise = client2
.invokeScript(longScript)
.catch((e) =>
expect((e as Error).message).toContain(
"Script killed",
),
);

let killed = false;
let timeout = 4000;
await new Promise((resolve) => setTimeout(resolve, 1000));

while (timeout >= 0) {
try {
expect(await client1.scriptKill()).toEqual("OK");
killed = true;
break;
} catch {
// do nothing
}

await new Promise((resolve) =>
setTimeout(resolve, 500),
);
timeout -= 500;
}

expect(killed).toBeTruthy();
await promise;
} finally {
await waitForScriptNotBusy(client1);
}
} finally {
expect(await client1.scriptFlush()).toEqual("OK");
client1.close();
client2.close();
}
},
);

runBaseTests({
init: async (protocol, configOverrides) => {
const config = getClientConfigurationOption(
Expand Down
Loading
Loading