From 3958238b426ab786dfe2bb7915bf97d6bb7b4f9c Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 2 Aug 2024 14:04:43 -0700 Subject: [PATCH 1/2] Add `FUNCTION STATS` command. Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/Commands.ts | 13 ++++++ node/src/GlideClient.ts | 54 +++++++++++++++++++++++ node/src/GlideClusterClient.ts | 63 ++++++++++++++++++++++++++- node/src/Transaction.ts | 19 ++++++++ node/tests/GlideClient.test.ts | 11 ++++- node/tests/GlideClusterClient.test.ts | 50 ++++++++++++++++++++- node/tests/TestUtilities.ts | 48 ++++++++++++++++++++ 9 files changed, 257 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d81b0401..d7aadd9ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added FUNCTION STATS commands ([#2081](https://github.com/valkey-io/valkey-glide/pull/2081)) * Node: Added SORT commands ([#2028](https://github.com/valkey-io/valkey-glide/pull/2028)) * Node: Added LASTSAVE command ([#2059](https://github.com/valkey-io/valkey-glide/pull/2059)) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 2a0a57eb2b..c611d7d184 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -106,6 +106,7 @@ function initialize() { GlideClientConfiguration, FunctionListOptions, FunctionListResponse, + FunctionStatsResponse, SlotIdTypes, SlotKeyTypes, RouteByAddress, @@ -193,6 +194,7 @@ function initialize() { GlideClientConfiguration, FunctionListOptions, FunctionListResponse, + FunctionStatsResponse, SlotIdTypes, SlotKeyTypes, RouteByAddress, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 5416883d9d..c0a1a39019 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2120,6 +2120,19 @@ export function createFunctionList( return createCommand(RequestType.FunctionList, args); } +/** Type of the response of `FUNCTION STATS` command. */ +export type FunctionStatsResponse = Record< + string, + | null + | Record + | Record> +>; + +/** @internal */ +export function createFunctionStats(): command_request.Command { + return createCommand(RequestType.FunctionStats, []); +} + /** * Represents offsets specifying a string interval to analyze in the {@link BaseClient.bitcount|bitcount} command. The offsets are * zero-based indexes, with `0` being the first index of the string, `1` being the next index and so on. diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 582b9cd3b5..8e269e6fec 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -14,6 +14,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, + FunctionStatsResponse, InfoOptions, LolwutOptions, SortOptions, @@ -33,6 +34,7 @@ import { createFunctionFlush, createFunctionList, createFunctionLoad, + createFunctionStats, createInfo, createLastSave, createLolwut, @@ -544,6 +546,58 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createFunctionList(options)); } + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * See https://valkey.io/commands/function-stats/ for details. + * + * since Valkey version 7.0.0. + * + * @returns A `Record` with two keys: + * - `"running_script"` with information about the running script. + * - `"engines"` with information about available engines and their stats. + * + * See example for more details. + * + * @example + * ```typescript + * const response = await client.functionStats(); + * console.log(response); // Output: + * // { + * // "running_script": + * // { + * // "name": "deep_thought", + * // "command": ["fcall", "deep_thought", "0"], + * // "duration_ms": 5008 + * // }, + * // "engines": + * // { + * // "LUA": + * // { + * // "libraries_count": 2, + * // "functions_count": 3 + * // } + * // } + * // } + * // Output if no scripts running: + * // { + * // "running_script": null + * // "engines": + * // { + * // "LUA": + * // { + * // "libraries_count": 2, + * // "functions_count": 3 + * // } + * // } + * // } + * ``` + */ + public async functionStats(): Promise { + return this.createWritePromise(createFunctionStats()); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 8f1c9ad99c..46b5b31da8 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -14,6 +14,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, + FunctionStatsResponse, InfoOptions, LolwutOptions, SortClusterOptions, @@ -35,6 +36,7 @@ import { createFunctionFlush, createFunctionList, createFunctionLoad, + createFunctionStats, createInfo, createLastSave, createLolwut, @@ -831,7 +833,7 @@ export class GlideClusterClient extends BaseClient { * since Valkey version 7.0.0. * * @param mode - The flushing mode, could be either {@link FlushMode.SYNC} or {@link FlushMode.ASYNC}. - * @param route - The command will be routed to all primary node, unless `route` is provided, in which + * @param route - The command will be routed to all primary nodes, unless `route` is provided, in which * case the client will route the command to the nodes defined by `route`. * @returns A simple OK response. * @@ -889,6 +891,65 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * See https://valkey.io/commands/function-stats/ for details. + * + * since Valkey version 7.0.0. + * + * @param route - The client will route the command to the nodes defined by `route`. + * If not defined, the command will be routed to all primary nodes. + * @returns A `Record` with two keys: + * - `"running_script"` with information about the running script. + * - `"engines"` with information about available engines and their stats. + * + * See example for more details. + * + * @example + * ```typescript + * const response = await client.functionStats("randomNode"); + * console.log(response); // Output: + * // { + * // "running_script": + * // { + * // "name": "deep_thought", + * // "command": ["fcall", "deep_thought", "0"], + * // "duration_ms": 5008 + * // }, + * // "engines": + * // { + * // "LUA": + * // { + * // "libraries_count": 2, + * // "functions_count": 3 + * // } + * // } + * // } + * // Output if no scripts running: + * // { + * // "running_script": null + * // "engines": + * // { + * // "LUA": + * // { + * // "libraries_count": 2, + * // "functions_count": 3 + * // } + * // } + * // } + * ``` + */ + public async functionStats( + route?: Routes, + ): Promise> { + return this.createWritePromise( + createFunctionStats(), + toProtobufRoute(route), + ); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 61f03233cf..812d2c93ad 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -24,6 +24,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, // eslint-disable-line @typescript-eslint/no-unused-vars + FunctionStatsResponse, // eslint-disable-line @typescript-eslint/no-unused-vars GeoAddOptions, GeoBoxShape, // eslint-disable-line @typescript-eslint/no-unused-vars GeoCircleShape, // eslint-disable-line @typescript-eslint/no-unused-vars @@ -84,6 +85,7 @@ import { createFunctionFlush, createFunctionList, createFunctionLoad, + createFunctionStats, createGeoAdd, createGeoDist, createGeoHash, @@ -2334,6 +2336,23 @@ export class BaseTransaction> { return this.addAndReturn(createFunctionList(options)); } + /** + * Returns information about the function that's currently running and information about the + * available execution engines. + * + * See https://valkey.io/commands/function-stats/ for details. + * + * since Valkey version 7.0.0. + * + * Command Response - A `Record` of type {@link FunctionStatsResponse} with two keys: + * + * - `"running_script"` with information about the running script. + * - `"engines"` with information about available engines and their stats. + */ + public functionStats(): T { + return this.addAndReturn(createFunctionStats()); + } + /** * Deletes all the keys of all the existing databases. This command never fails. * diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 8893c81cf2..c25750762c 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -19,6 +19,7 @@ import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { checkFunctionListResponse, + checkFunctionStatsResponse, convertStringArrayToBuffer, flushAndCloseClient, generateLuaLibCode, @@ -501,7 +502,7 @@ describe("GlideClient", () => { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "function load test_%p", + "function load function list function stats test_%p", async (protocol) => { if (cluster.checkIfServerVersionLessThan("7.0.0")) return; @@ -528,6 +529,9 @@ describe("GlideClient", () => { await client.fcallReadonly(funcName, [], ["one", "two"]), ).toEqual("one"); + let functionStats = await client.functionStats(); + checkFunctionStatsResponse(functionStats, [], 1, 1); + let functionList = await client.functionList({ libNamePattern: libName, }); @@ -586,6 +590,9 @@ describe("GlideClient", () => { newCode, ); + functionStats = await client.functionStats(); + checkFunctionStatsResponse(functionStats, [], 1, 2); + expect( await client.fcall(func2Name, [], ["one", "two"]), ).toEqual(2); @@ -594,6 +601,8 @@ describe("GlideClient", () => { ).toEqual(2); } finally { expect(await client.functionFlush()).toEqual("OK"); + const functionStats = await client.functionStats(); + checkFunctionStatsResponse(functionStats, [], 0, 0); client.close(); } }, diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 19d7b91c7b..20a2ab4c04 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -25,11 +25,16 @@ import { ScoreFilter, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode, SortOrder } from "../build-ts/src/Commands"; +import { + FlushMode, + FunctionStatsResponse, + SortOrder, +} from "../build-ts/src/Commands"; import { runBaseTests } from "./SharedTests"; import { checkClusterResponse, checkFunctionListResponse, + checkFunctionStatsResponse, flushAndCloseClient, generateLuaLibCode, getClientConfigurationOption, @@ -727,7 +732,7 @@ describe("GlideClusterClient", () => { "Single node route = %s", (singleNodeRoute) => { it( - "function load and function list", + "function load function list function stats", async () => { if (cluster.checkIfServerVersionLessThan("7.0.0")) return; @@ -763,6 +768,21 @@ describe("GlideClusterClient", () => { singleNodeRoute, (value) => expect(value).toEqual([]), ); + + let functionStats = + await client.functionStats(route); + checkClusterResponse( + functionStats as object, + singleNodeRoute, + (value) => + checkFunctionStatsResponse( + value as FunctionStatsResponse, + [], + 0, + 0, + ), + ); + // load the library expect(await client.functionLoad(code)).toEqual( libName, @@ -791,6 +811,19 @@ describe("GlideClusterClient", () => { expectedFlags, ), ); + functionStats = + await client.functionStats(route); + checkClusterResponse( + functionStats as object, + singleNodeRoute, + (value) => + checkFunctionStatsResponse( + value as FunctionStatsResponse, + [], + 1, + 1, + ), + ); // call functions from that library to confirm that it works let fcall = await client.fcallWithRoute( @@ -869,6 +902,19 @@ describe("GlideClusterClient", () => { newCode, ), ); + functionStats = + await client.functionStats(route); + checkClusterResponse( + functionStats as object, + singleNodeRoute, + (value) => + checkFunctionStatsResponse( + value as FunctionStatsResponse, + [], + 1, + 2, + ), + ); fcall = await client.fcallWithRoute( func2Name, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 34d79f3f05..d2af17c52d 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -19,6 +19,7 @@ import { ClusterTransaction, FlushMode, FunctionListResponse, + FunctionStatsResponse, GeoUnit, GeospatialData, GlideClient, @@ -396,6 +397,43 @@ export function checkFunctionListResponse( expect(hasLib).toBeTruthy(); } +/** + * Validate whether `FUNCTION STATS` response contains required info. + * + * @param response - The response from server. + * @param runningFunction - Command line of running function expected. Empty, if nothing expected. + * @param libCount - Expected libraries count. + * @param functionCount - Expected functions count. + */ +export function checkFunctionStatsResponse( + response: FunctionStatsResponse, + runningFunction: string[], + libCount: number, + functionCount: number, +) { + if (response.running_script === null && runningFunction.length > 0) { + fail("No running function info"); + } + + if (response.running_script !== null && runningFunction.length == 0) { + fail( + "Unexpected running function info: " + + (response.running_script.command as string[]).join(" "), + ); + } + + if (response.running_script !== null) { + expect(response.running_script.command).toEqual(runningFunction); + // command line format is: + // fcall|fcall_ro * * + expect(response.running_script.name).toEqual(runningFunction[1]); + } + + expect(response.engines).toEqual({ + LUA: { libraries_count: libCount, functions_count: functionCount }, + }); +} + /** * Check transaction response. * @param response - Transaction result received from `exec` call. @@ -1085,6 +1123,8 @@ export async function transactionTest( ); if (gte(version, "7.0.0")) { + baseTransaction.functionFlush(); + responseData.push(["functionFlush()", "OK"]); baseTransaction.functionLoad(code); responseData.push(["functionLoad(code)", libName]); baseTransaction.functionLoad(code, true); @@ -1098,6 +1138,14 @@ export async function transactionTest( 'fcallReadonly(funcName, [], ["one", "two"]', "one", ]); + baseTransaction.functionStats(); + responseData.push([ + "functionStats()", + { + running_script: null, + engines: { LUA: { libraries_count: 1, functions_count: 1 } }, + }, + ]); baseTransaction.functionDelete(libName); responseData.push(["functionDelete(libName)", "OK"]); baseTransaction.functionFlush(); From b964ecd7695960d2cf2975bce5c1256c25805662 Mon Sep 17 00:00:00 2001 From: Yury-Fridlyand Date: Fri, 2 Aug 2024 14:05:38 -0700 Subject: [PATCH 2/2] Signed-off-by: Yury-Fridlyand --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7aadd9ff8..f6ba8093b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes -* Node: Added FUNCTION STATS commands ([#2081](https://github.com/valkey-io/valkey-glide/pull/2081)) +* Node: Added FUNCTION STATS commands ([#2082](https://github.com/valkey-io/valkey-glide/pull/2082)) * Node: Added SORT commands ([#2028](https://github.com/valkey-io/valkey-glide/pull/2028)) * Node: Added LASTSAVE command ([#2059](https://github.com/valkey-io/valkey-glide/pull/2059)) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049))