diff --git a/CHANGELOG.md b/CHANGELOG.md index 363dbbd4ea..c8dba939e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#2129](https://github.com/valkey-io/valkey-glide/pull/2129)) * Node: Added ZUNIONSTORE command ([#2145](https://github.com/valkey-io/valkey-glide/pull/2145)) * Node: Added XREADGROUP command ([#2124](https://github.com/valkey-io/valkey-glide/pull/2124)) * Node: Added XINFO GROUPS command ([#2122](https://github.com/valkey-io/valkey-glide/pull/2122)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index ae26a5c985..276ebfa0b4 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -108,6 +108,7 @@ function initialize() { FunctionListOptions, FunctionListResponse, FunctionStatsResponse, + FunctionRestorePolicy, SlotIdTypes, SlotKeyTypes, TimeUnit, @@ -205,6 +206,7 @@ function initialize() { FunctionListOptions, FunctionListResponse, FunctionStatsResponse, + FunctionRestorePolicy, SlotIdTypes, SlotKeyTypes, StreamEntries, diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 4947164109..f5e5ac55a8 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -2293,6 +2293,44 @@ export function createFunctionKill(): command_request.Command { return createCommand(RequestType.FunctionKill, []); } +/** @internal */ +export function createFunctionDump(): command_request.Command { + return createCommand(RequestType.FunctionDump, []); +} + +/** + * Option for `FUNCTION RESTORE` command: {@link GlideClient.functionRestore} and + * {@link GlideClusterClient.functionRestore}. + * + * @see {@link https://valkey.io/commands/function-restore/"|valkey.io} for more details. + */ +export enum FunctionRestorePolicy { + /** + * Appends the restored libraries to the existing libraries and aborts on collision. This is the + * default policy. + */ + APPEND = "APPEND", + /** Deletes all existing libraries before restoring the payload. */ + FLUSH = "FLUSH", + /** + * Appends the restored libraries to the existing libraries, replacing any existing ones in case + * of name collisions. Note that this policy doesn't prevent function name collisions, only + * libraries. + */ + REPLACE = "REPLACE", +} + +/** @internal */ +export function createFunctionRestore( + data: Buffer, + policy?: FunctionRestorePolicy, +): command_request.Command { + return createCommand( + RequestType.FunctionRestore, + policy ? [data, policy] : [data], + ); +} + /** * 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 e63f7f8fb1..e5bfb9a1b4 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -16,6 +16,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, + FunctionRestorePolicy, FunctionStatsResponse, InfoOptions, LolwutOptions, @@ -33,10 +34,12 @@ import { createFlushAll, createFlushDB, createFunctionDelete, + createFunctionDump, createFunctionFlush, createFunctionKill, createFunctionList, createFunctionLoad, + createFunctionRestore, createFunctionStats, createInfo, createLastSave, @@ -174,26 +177,27 @@ export class GlideClient extends BaseClient { * * @see {@link https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#transaction|Valkey Glide Wiki} for details on Valkey Transactions. * - * @param transaction - A Transaction object containing a list of commands to be executed. - * @param decoder - (Optional) {@link Decoder} type which defines how to handle the responses. If not set, the default decoder from the client config will be used. + * @param transaction - A {@link Transaction} object containing a list of commands to be executed. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A list of results corresponding to the execution of each command in the transaction. - * If a command returns a value, it will be included in the list. If a command doesn't return a value, - * the list entry will be null. - * If the transaction failed due to a WATCH command, `exec` will return `null`. + * If a command returns a value, it will be included in the list. If a command doesn't return a value, + * the list entry will be `null`. + * If the transaction failed due to a `WATCH` command, `exec` will return `null`. */ public async exec( transaction: Transaction, - decoder: Decoder = this.defaultDecoder, + decoder?: Decoder, ): Promise { return this.createWritePromise( transaction.commands, { decoder: decoder }, - ).then((result: ReturnType[] | null) => { - return this.processResultWithSetCommands( + ).then((result) => + this.processResultWithSetCommands( result, transaction.setCommandsIndexes, - ); - }); + ), + ); } /** Executes a single command, without checking inputs. Every part of the command, including subcommands, @@ -643,9 +647,8 @@ export class GlideClient extends BaseClient { * Kills a function that is currently executing. * `FUNCTION KILL` terminates read-only functions only. * - * See https://valkey.io/commands/function-kill/ for details. - * - * since Valkey version 7.0.0. + * @see {@link https://valkey.io/commands/function-kill/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. * * @returns `OK` if function is terminated. Otherwise, throws an error. * @example @@ -657,6 +660,51 @@ export class GlideClient extends BaseClient { return this.createWritePromise(createFunctionKill()); } + /** + * Returns the serialized payload of all loaded libraries. + * + * @see {@link https://valkey.io/commands/function-dump/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @returns The serialized payload of all loaded libraries. + * + * @example + * ```typescript + * const data = await client.functionDump(); + * // data can be used to restore loaded functions on any Valkey instance + * ``` + */ + public async functionDump(): Promise { + return this.createWritePromise(createFunctionDump(), { + decoder: Decoder.Bytes, + }); + } + + /** + * Restores libraries from the serialized payload returned by {@link functionDump}. + * + * @see {@link https://valkey.io/commands/function-restore/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @param payload - The serialized data from {@link functionDump}. + * @param policy - (Optional) A policy for handling existing libraries, see {@link FunctionRestorePolicy}. + * {@link FunctionRestorePolicy.APPEND} is used by default. + * @returns `"OK"`. + * + * @example + * ```typescript + * await client.functionRestore(data, FunctionRestorePolicy.FLUSH); + * ``` + */ + public async functionRestore( + payload: Buffer, + policy?: FunctionRestorePolicy, + ): Promise<"OK"> { + return this.createWritePromise(createFunctionRestore(payload, policy), { + decoder: Decoder.String, + }); + } + /** * 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 0aeb430ae4..f4f36e51da 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -16,6 +16,7 @@ import { FlushMode, FunctionListOptions, FunctionListResponse, + FunctionRestorePolicy, FunctionStatsResponse, InfoOptions, LolwutOptions, @@ -35,10 +36,12 @@ import { createFlushAll, createFlushDB, createFunctionDelete, + createFunctionDump, createFunctionFlush, createFunctionKill, createFunctionList, createFunctionLoad, + createFunctionRestore, createFunctionStats, createInfo, createLastSave, @@ -368,16 +371,19 @@ export class GlideClusterClient extends BaseClient { /** * Execute a transaction by processing the queued commands. - * @see {@link https://redis.io/topics/Transactions/|Valkey Glide Wiki} for details on Redis Transactions. * - * @param transaction - A ClusterTransaction object containing a list of commands to be executed. - * @param route - If `route` is not provided, the transaction will be routed to the slot owner of the first key found in the transaction. - * If no key is found, the command will be sent to a random node. - * If `route` is provided, the client will route the command to the nodes defined by `route`. + * @see {@link https://github.com/valkey-io/valkey-glide/wiki/NodeJS-wrapper#transaction|Valkey Glide Wiki} for details on Valkey Transactions. + * + * @param transaction - A {@link ClusterTransaction} object containing a list of commands to be executed. + * @param route - (Optional) If `route` is not provided, the transaction will be routed to the slot owner of the first key found in the transaction. + * If no key is found, the command will be sent to a random node. + * If `route` is provided, the client will route the command to the nodes defined by `route`. + * @param decoder - (Optional) {@link Decoder} type which defines how to handle the response. + * If not set, the {@link BaseClientConfiguration.defaultDecoder|default decoder} will be used. * @returns A list of results corresponding to the execution of each command in the transaction. - * If a command returns a value, it will be included in the list. If a command doesn't return a value, - * the list entry will be null. - * If the transaction failed due to a WATCH command, `exec` will return `null`. + * If a command returns a value, it will be included in the list. If a command doesn't return a value, + * the list entry will be `null`. + * If the transaction failed due to a `WATCH` command, `exec` will return `null`. */ public async exec( transaction: ClusterTransaction, @@ -392,12 +398,12 @@ export class GlideClusterClient extends BaseClient { route: toProtobufRoute(options?.route), decoder: options?.decoder, }, - ).then((result: ReturnType[] | null) => { - return this.processResultWithSetCommands( + ).then((result) => + this.processResultWithSetCommands( result, transaction.setCommandsIndexes, - ); - }); + ), + ); } /** Ping the Redis server. @@ -748,7 +754,7 @@ export class GlideClusterClient extends BaseClient { func: string, args: string[], route?: Routes, - ): Promise { + ): Promise> { return this.createWritePromise(createFCall(func, [], args), { route: toProtobufRoute(route), }); @@ -777,7 +783,7 @@ export class GlideClusterClient extends BaseClient { func: string, args: string[], route?: Routes, - ): Promise { + ): Promise> { return this.createWritePromise(createFCallReadOnly(func, [], args), { route: toProtobufRoute(route), }); @@ -874,7 +880,7 @@ export class GlideClusterClient extends BaseClient { * * @param options - Parameters to filter and request additional info. * @param route - The client will route the command to the nodes defined by `route`. - * If not defined, the command will be routed to a random route. + * If not defined, the command will be routed to a random node. * @returns Info about all or selected libraries and their functions in {@link FunctionListResponse} format. * * @example @@ -983,6 +989,59 @@ export class GlideClusterClient extends BaseClient { }); } + /** + * Returns the serialized payload of all loaded libraries. + * + * @see {@link https://valkey.io/commands/function-dump/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @param route - (Optional) The client will route the command to the nodes defined by `route`. + * If not defined, the command will be routed a random node. + * @returns The serialized payload of all loaded libraries. + * + * @example + * ```typescript + * const data = await client.functionDump(); + * // data can be used to restore loaded functions on any Valkey instance + * ``` + */ + public async functionDump( + route?: Routes, + ): Promise> { + return this.createWritePromise(createFunctionDump(), { + decoder: Decoder.Bytes, + route: toProtobufRoute(route), + }); + } + + /** + * Restores libraries from the serialized payload returned by {@link functionDump}. + * + * @see {@link https://valkey.io/commands/function-restore/|valkey.io} for details. + * @remarks Since Valkey version 7.0.0. + * + * @param payload - The serialized data from {@link functionDump}. + * @param policy - (Optional) A policy for handling existing libraries, see {@link FunctionRestorePolicy}. + * {@link FunctionRestorePolicy.APPEND} is used by default. + * @param route - (Optional) The client will route the command to the nodes defined by `route`. + * If not defined, the command will be routed all primary nodes. + * @returns `"OK"`. + * + * @example + * ```typescript + * await client.functionRestore(data, { policy: FunctionRestorePolicy.FLUSH, route: "allPrimaries" }); + * ``` + */ + public async functionRestore( + payload: Buffer, + options?: { policy?: FunctionRestorePolicy; route?: Routes }, + ): Promise<"OK"> { + return this.createWritePromise( + createFunctionRestore(payload, options?.policy), + { decoder: Decoder.String, route: toProtobufRoute(options?.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 5f0a7b7bfb..dc168f95b1 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -4,6 +4,7 @@ import { BaseClient, // eslint-disable-line @typescript-eslint/no-unused-vars + Decoder, // eslint-disable-line @typescript-eslint/no-unused-vars GlideString, ReadFrom, // eslint-disable-line @typescript-eslint/no-unused-vars } from "./BaseClient"; diff --git a/node/tests/AsyncClient.test.ts b/node/tests/AsyncClient.test.ts index 762e33fede..6d0d3b4246 100644 --- a/node/tests/AsyncClient.test.ts +++ b/node/tests/AsyncClient.test.ts @@ -12,8 +12,6 @@ const FreePort = require("find-free-port"); const PORT_NUMBER = 4000; -type EmptyObject = Record; - describe("AsyncClient", () => { let server: RedisServer; let port: number; @@ -41,7 +39,7 @@ describe("AsyncClient", () => { server.close(); }); - runCommonTests({ + runCommonTests({ init: async () => { const client = await AsyncClient.CreateConnection( "redis://localhost:" + port, diff --git a/node/tests/GlideClient.test.ts b/node/tests/GlideClient.test.ts index 48d1035603..f5526104bb 100644 --- a/node/tests/GlideClient.test.ts +++ b/node/tests/GlideClient.test.ts @@ -21,7 +21,11 @@ import { Transaction, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode, SortOrder } from "../build-ts/src/Commands"; +import { + FlushMode, + FunctionRestorePolicy, + SortOrder, +} from "../build-ts/src/Commands"; import { command_request } from "../src/ProtobufMessage"; import { runBaseTests } from "./SharedTests"; import { @@ -41,12 +45,6 @@ import { waitForNotBusy, } from "./TestUtilities"; -/* eslint-disable @typescript-eslint/no-var-requires */ - -type Context = { - client: GlideClient; -}; - const TIMEOUT = 50000; describe("GlideClient", () => { @@ -136,11 +134,9 @@ describe("GlideClient", () => { "check that blocking commands returns never timeout_%p", async (protocol) => { client = await GlideClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - 300, - ), + getClientConfigurationOption(cluster.getAddresses(), protocol, { + requestTimeout: 300, + }), ); const promiseList = [ @@ -198,6 +194,7 @@ describe("GlideClient", () => { expect(await client.dbsize()).toBeGreaterThan(0); expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); expect(await client.dbsize()).toEqual(0); + client.close(); }, ); @@ -221,6 +218,7 @@ describe("GlideClient", () => { expect(await client.get(key)).toEqual(valueEncoded); expect(await client.get(key, Decoder.String)).toEqual(value); expect(await client.get(key, Decoder.Bytes)).toEqual(valueEncoded); + client.close(); }, ); @@ -244,6 +242,7 @@ describe("GlideClient", () => { expect(await client.get(key)).toEqual(value); expect(await client.get(key, Decoder.String)).toEqual(value); expect(await client.get(key, Decoder.Bytes)).toEqual(valueEncoded); + client.close(); }, ); @@ -263,6 +262,7 @@ describe("GlideClient", () => { expectedRes.push(["select(0)", "OK"]); validateTransactionResponse(result, expectedRes); + client.close(); }, ); @@ -279,6 +279,7 @@ describe("GlideClient", () => { expectedRes.push(["select(0)", "OK"]); validateTransactionResponse(result, expectedRes); + client.close(); }, ); @@ -302,6 +303,7 @@ describe("GlideClient", () => { expectedRes.push(["select(0)", "OK"]); validateTransactionResponse(result, expectedRes); + client.close(); }, ); @@ -326,6 +328,7 @@ describe("GlideClient", () => { expectedRes.push(["select(0)", "OK"]); validateTransactionResponse(result, expectedRes); + client.close(); }, ); @@ -840,7 +843,7 @@ describe("GlideClient", () => { const config = getClientConfigurationOption( cluster.getAddresses(), protocol, - 10000, + { requestTimeout: 10000 }, ); const client = await GlideClient.createClient(config); const testClient = await GlideClient.createClient(config); @@ -911,7 +914,7 @@ describe("GlideClient", () => { const config = getClientConfigurationOption( cluster.getAddresses(), protocol, - 10000, + { requestTimeout: 10000 }, ); const client = await GlideClient.createClient(config); const testClient = await GlideClient.createClient(config); @@ -981,6 +984,107 @@ describe("GlideClient", () => { }, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "function dump function restore %p", + async (protocol) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + const client = await GlideClient.createClient(config); + expect(await client.functionFlush()).toEqual("OK"); + + try { + // dumping an empty lib + expect( + (await client.functionDump()).byteLength, + ).toBeGreaterThan(0); + + const name1 = "Foster"; + const name2 = "Dogster"; + // function $name1 returns first argument + // function $name2 returns argument array len + let code = generateLuaLibCode( + name1, + new Map([ + [name1, "return args[1]"], + [name2, "return #args"], + ]), + false, + ); + expect(await client.functionLoad(code)).toEqual(name1); + + const flist = await client.functionList({ withCode: true }); + const dump = await client.functionDump(); + + // restore without cleaning the lib and/or overwrite option causes an error + await expect(client.functionRestore(dump)).rejects.toThrow( + `Library ${name1} already exists`, + ); + + // APPEND policy also fails for the same reason (name collision) + await expect( + client.functionRestore(dump, FunctionRestorePolicy.APPEND), + ).rejects.toThrow(`Library ${name1} already exists`); + + // REPLACE policy succeeds + expect( + await client.functionRestore( + dump, + FunctionRestorePolicy.REPLACE, + ), + ).toEqual("OK"); + // but nothing changed - all code overwritten + expect(await client.functionList({ withCode: true })).toEqual( + flist, + ); + + // create lib with another name, but with the same function names + expect(await client.functionFlush(FlushMode.SYNC)).toEqual( + "OK", + ); + code = generateLuaLibCode( + name2, + new Map([ + [name1, "return args[1]"], + [name2, "return #args"], + ]), + false, + ); + expect(await client.functionLoad(code)).toEqual(name2); + + // REPLACE policy now fails due to a name collision + await expect(client.functionRestore(dump)).rejects.toThrow( + new RegExp(`Function ${name1}|${name2} already exists`), + ); + + // FLUSH policy succeeds, but deletes the second lib + expect( + await client.functionRestore( + dump, + FunctionRestorePolicy.FLUSH, + ), + ).toEqual("OK"); + expect(await client.functionList({ withCode: true })).toEqual( + flist, + ); + + // call restored functions + expect(await client.fcall(name1, [], ["meow", "woem"])).toEqual( + "meow", + ); + expect(await client.fcall(name2, [], ["meow", "woem"])).toEqual( + 2, + ); + } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + } + }, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( "sort sortstore sort_store sortro sort_ro sortreadonly test_%p", async (protocol) => { @@ -1396,19 +1500,19 @@ describe("GlideClient", () => { TIMEOUT, ); - runBaseTests({ - init: async (protocol, clientName?) => { - const options = getClientConfigurationOption( + runBaseTests({ + init: async (protocol, configOverrides) => { + const config = getClientConfigurationOption( cluster.getAddresses(), protocol, + configOverrides, ); - options.protocol = protocol; - options.clientName = clientName; + testsFailed += 1; - client = await GlideClient.createClient(options); - return { client, context: { client }, cluster }; + client = await GlideClient.createClient(config); + return { client, cluster }; }, - close: (context: Context, testSucceeded: boolean) => { + close: (testSucceeded: boolean) => { if (testSucceeded) { testsFailed -= 1; } diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index 365f4b5cb6..65d39b1703 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -22,12 +22,14 @@ import { ListDirection, ProtocolVersion, RequestError, + ReturnType, Routes, ScoreFilter, } from ".."; import { RedisCluster } from "../../utils/TestUtils.js"; import { FlushMode, + FunctionRestorePolicy, FunctionStatsResponse, GeoUnit, SortOrder, @@ -50,9 +52,6 @@ import { validateTransactionResponse, waitForNotBusy, } from "./TestUtilities"; -type Context = { - client: GlideClusterClient; -}; const TIMEOUT = 50000; @@ -81,25 +80,22 @@ describe("GlideClusterClient", () => { } }); - runBaseTests({ - init: async (protocol, clientName?) => { - const options = getClientConfigurationOption( + runBaseTests({ + init: async (protocol, configOverrides) => { + const config = getClientConfigurationOption( cluster.getAddresses(), protocol, + configOverrides, ); - options.protocol = protocol; - options.clientName = clientName; + testsFailed += 1; - client = await GlideClusterClient.createClient(options); + client = await GlideClusterClient.createClient(config); return { - context: { - client, - }, client, cluster, }; }, - close: (context: Context, testSucceeded: boolean) => { + close: (testSucceeded: boolean) => { if (testSucceeded) { testsFailed -= 1; } @@ -1171,7 +1167,7 @@ describe("GlideClusterClient", () => { const config = getClientConfigurationOption( cluster.getAddresses(), protocol, - 10000, + { requestTimeout: 10000 }, ); const client = await GlideClusterClient.createClient(config); @@ -1262,6 +1258,178 @@ describe("GlideClusterClient", () => { }, TIMEOUT, ); + + it("function dump function restore %p", async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const config = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + const client = + await GlideClusterClient.createClient(config); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + expect( + await client.functionFlush(FlushMode.SYNC, route), + ).toEqual("OK"); + + try { + // dumping an empty lib + let response = await client.functionDump(route); + + if (singleNodeRoute) { + expect(response.byteLength).toBeGreaterThan(0); + } else { + Object.values(response).forEach((d: Buffer) => + expect(d.byteLength).toBeGreaterThan(0), + ); + } + + const name1 = "Foster"; + const name2 = "Dogster"; + // function $name1 returns first argument + // function $name2 returns argument array len + let code = generateLuaLibCode( + name1, + new Map([ + [name1, "return args[1]"], + [name2, "return #args"], + ]), + false, + ); + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(name1); + + const flist = await client.functionList( + { withCode: true }, + route, + ); + response = await client.functionDump(route); + const dump = ( + singleNodeRoute + ? response + : Object.values(response)[0] + ) as Buffer; + + // restore without cleaning the lib and/or overwrite option causes an error + await expect( + client.functionRestore(dump, { route: route }), + ).rejects.toThrow( + `Library ${name1} already exists`, + ); + + // APPEND policy also fails for the same reason (name collision) + await expect( + client.functionRestore(dump, { + policy: FunctionRestorePolicy.APPEND, + route: route, + }), + ).rejects.toThrow( + `Library ${name1} already exists`, + ); + + // REPLACE policy succeeds + expect( + await client.functionRestore(dump, { + policy: FunctionRestorePolicy.REPLACE, + route: route, + }), + ).toEqual("OK"); + // but nothing changed - all code overwritten + expect( + await client.functionList( + { withCode: true }, + route, + ), + ).toEqual(flist); + + // create lib with another name, but with the same function names + expect( + await client.functionFlush( + FlushMode.SYNC, + route, + ), + ).toEqual("OK"); + code = generateLuaLibCode( + name2, + new Map([ + [name1, "return args[1]"], + [name2, "return #args"], + ]), + false, + ); + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(name2); + + // REPLACE policy now fails due to a name collision + await expect( + client.functionRestore(dump, { route: route }), + ).rejects.toThrow( + new RegExp( + `Function ${name1}|${name2} already exists`, + ), + ); + + // FLUSH policy succeeds, but deletes the second lib + expect( + await client.functionRestore(dump, { + policy: FunctionRestorePolicy.FLUSH, + route: route, + }), + ).toEqual("OK"); + expect( + await client.functionList( + { withCode: true }, + route, + ), + ).toEqual(flist); + + // call restored functions + let res = await client.fcallWithRoute( + name1, + ["meow", "woem"], + route, + ); + + if (singleNodeRoute) { + expect(res).toEqual("meow"); + } else { + Object.values( + res as Record, + ).forEach((r) => expect(r).toEqual("meow")); + } + + res = await client.fcallWithRoute( + name2, + ["meow", "woem"], + route, + ); + + if (singleNodeRoute) { + expect(res).toEqual(2); + } else { + Object.values( + res as Record, + ).forEach((r) => expect(r).toEqual(2)); + } + } finally { + expect(await client.functionFlush()).toEqual("OK"); + client.close(); + } + }); }, ); it( @@ -1272,7 +1440,7 @@ describe("GlideClusterClient", () => { const config = getClientConfigurationOption( cluster.getAddresses(), protocol, - 10000, + { requestTimeout: 10000 }, ); const client = await GlideClusterClient.createClient(config); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 4428f6018b..6bd44bcadc 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -10,6 +10,7 @@ import { expect, it } from "@jest/globals"; import { v4 as uuidv4 } from "uuid"; import { + BaseClientConfiguration, BitFieldGet, BitFieldIncrBy, BitFieldOverflow, @@ -56,16 +57,18 @@ import { export type BaseClient = GlideClient | GlideClusterClient; -export function runBaseTests(config: { +// Same as `BaseClientConfiguration`, but all fields are optional +export type ClientConfig = Partial; + +export function runBaseTests(config: { init: ( protocol: ProtocolVersion, - clientName?: string, + configOverrides?: ClientConfig, ) => Promise<{ - context: Context; client: BaseClient; cluster: RedisCluster; }>; - close: (context: Context, testSucceeded: boolean) => void; + close: (testSucceeded: boolean) => void; timeout?: number; }) { runCommonTests({ @@ -77,11 +80,11 @@ export function runBaseTests(config: { const runTest = async ( test: (client: BaseClient, cluster: RedisCluster) => Promise, protocol: ProtocolVersion, - clientName?: string, + configOverrides?: ClientConfig, ) => { - const { context, client, cluster } = await config.init( + const { client, cluster } = await config.init( protocol, - clientName, + configOverrides, ); let testSucceeded = false; @@ -89,7 +92,7 @@ export function runBaseTests(config: { await test(client, cluster); testSucceeded = true; } finally { - config.close(context, testSucceeded); + config.close(testSucceeded); } }; @@ -161,7 +164,7 @@ export function runBaseTests(config: { expect(await client.clientGetName()).toBe("TEST_CLIENT"); }, protocol, - "TEST_CLIENT", + { clientName: "TEST_CLIENT" }, ); }, config.timeout, @@ -9673,20 +9676,20 @@ export function runBaseTests(config: { ); } -export function runCommonTests(config: { - init: () => Promise<{ context: Context; client: Client }>; - close: (context: Context, testSucceeded: boolean) => void; +export function runCommonTests(config: { + init: () => Promise<{ client: Client }>; + close: (testSucceeded: boolean) => void; timeout?: number; }) { const runTest = async (test: (client: Client) => Promise) => { - const { context, client } = await config.init(); + const { client } = await config.init(); let testSucceeded = false; try { await test(client); testSucceeded = true; } finally { - config.close(context, testSucceeded); + config.close(testSucceeded); } }; diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a987b285a5..2ac2cfda7a 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -338,7 +338,7 @@ export async function testTeardown( export const getClientConfigurationOption = ( addresses: [string, number][], protocol: ProtocolVersion, - timeout?: number, + configOverrides?: Partial, ): BaseClientConfiguration => { return { addresses: addresses.map(([host, port]) => ({ @@ -346,7 +346,7 @@ export const getClientConfigurationOption = ( port, })), protocol, - ...(timeout && { requestTimeout: timeout }), + ...configOverrides, }; }; @@ -357,7 +357,9 @@ export async function flushAndCloseClient( ) { await testTeardown( cluster_mode, - getClientConfigurationOption(addresses, ProtocolVersion.RESP3, 2000), + getClientConfigurationOption(addresses, ProtocolVersion.RESP3, { + requestTimeout: 2000, + }), ); // some tests don't initialize a client