From 315a0a75f45fd0a6ec72814f3a2a616235f48586 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Tue, 30 Jul 2024 14:25:55 -0700 Subject: [PATCH 01/13] add baseclient for pexpiretime Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 29 +++++++++++++++++++++++++++++ node/src/Transaction.ts | 2 ++ 2 files changed, 31 insertions(+) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1c69b15643..45e9ee7ac8 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -59,6 +59,7 @@ import { createExists, createExpire, createExpireAt, + createExpireTime, createFCall, createFCallReadOnly, createGeoAdd, @@ -107,6 +108,7 @@ import { createObjectRefcount, createPExpire, createPExpireAt, + createPExpireTime, createPTTL, createPersist, createPfAdd, @@ -2418,6 +2420,33 @@ export class BaseClient { ); } + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. + * To get the expiration with millisecond precision, use `pexpiretime`. + * + * See https://valkey.io/commands/expiretime/ for details. + * + * @param key - The `key` to determine the expiration value of. + * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * + * @example + * ```typescript + * const result1 = client.expiretime("myKey"); + * console.log(result1); // Output: -2 - myKey doesn't exist. + * + * const result2 = client.set(myKey, "value"); + * console.log(result2); // Output: -1 - myKey has no associate expiration. + * + * client.expire(myKey, 60); + * const result3 = client.expireTime(myKey); + * console.log(result3); // Output: 718614954 + * ``` + * since - Redis version 7.0.0. + */ + public expiretime(key: string): Promise { + return this.createWritePromise(createExpireTime(key)); + } + /** Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index de661be35a..341cbc8404 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -67,6 +67,7 @@ import { createExists, createExpire, createExpireAt, + createExpireTime, createFCall, createFCallReadOnly, createFlushAll, @@ -123,6 +124,7 @@ import { createObjectRefcount, createPExpire, createPExpireAt, + createPExpireTime, createPTTL, createPersist, createPfAdd, From 464a18514c7bb061053db4e2bb96325a85a26336 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 10:25:27 -0700 Subject: [PATCH 02/13] current work Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 36 +++++++++++++++++++++++++++++++----- node/src/Commands.ts | 14 ++++++++++++++ node/src/Transaction.ts | 26 ++++++++++++++++++++++++++ node/tests/SharedTests.ts | 13 +++++++++++++ 4 files changed, 84 insertions(+), 5 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 45e9ee7ac8..1553968c69 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2421,13 +2421,13 @@ export class BaseClient { } /** - * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. - * To get the expiration with millisecond precision, use `pexpiretime`. + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. + * To get the expiration with millisecond precision, use `pexpiretime`. * - * See https://valkey.io/commands/expiretime/ for details. + * See https://valkey.io/commands/expiretime/ for details. * - * @param key - The `key` to determine the expiration value of. - * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * @param key - The `key` to determine the expiration value of. + * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * * @example * ```typescript @@ -2505,6 +2505,32 @@ export class BaseClient { ); } + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in milliseconds. + * + * See https://valkey.io/commands/pexpiretime/ for details. + * + * @param key - The `key` to determine the expiration value of. + * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * + * @example + * ```typescript + * const result1 = client.pexpiretime("myKey"); + * console.log(result1); // Output: -2 - myKey doesn't exist. + * + * const result2 = client.set(myKey, "value"); + * console.log(result2); // Output: -1 - myKey has no associate expiration. + * + * client.expire(myKey, 60); + * const result3 = client.pexpireTime(myKey); + * console.log(result3); // Output: 718614954 + * ``` + * since - Redis version 7.0.0. + */ + public pexpiretime(key: string): Promise { + return this.createWritePromise(createPExpireTime(key)); + } + /** Returns the remaining time to live of `key` that has a timeout. * See https://valkey.io/commands/ttl/ for details. * diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 44af182559..f957c9ad6a 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1265,6 +1265,13 @@ export function createExpireAt( return createCommand(RequestType.ExpireAt, args); } +/** + * @internal + */ +export function createExpireTime(key: string): command_request.Command { + return createCommand(RequestType.ExpireTime, [key]); +} + /** * @internal */ @@ -1295,6 +1302,13 @@ export function createPExpireAt( return createCommand(RequestType.PExpireAt, args); } +/** + * @internal + */ +export function createPExpireTime(key: string): command_request.Command { + return createCommand(RequestType.PExpireTime, [key]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 341cbc8404..021b8d9198 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1314,6 +1314,19 @@ export class BaseTransaction> { return this.addAndReturn(createExpireAt(key, unixSeconds, option)); } + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. + * To get the expiration with millisecond precision, use `pexpiretime`. + * + * See https://valkey.io/commands/expiretime/ for details. + * + * @param key - The `key` to determine the expiration value of. + * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + */ + public expireTime(key: string): T { + return this.addAndReturn(createExpireTime(key)); + } + /** Sets a timeout on `key` in milliseconds. After the timeout has expired, the key will automatically be deleted. * If `key` already has an existing expire set, the time to live is updated to the new value. * If `milliseconds` is non-positive number, the key will be deleted rather than expired. @@ -1358,6 +1371,19 @@ export class BaseTransaction> { ); } + /** + * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in milliseconds. + * + * See https://valkey.io/commands/pexpiretime/ for details. + * + * @param key - The `key` to determine the expiration value of. + * + * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + */ + public pexpireTime(key: string): T { + return this.addAndReturn(createExpireTime(key)); + } + /** Returns the remaining time to live of `key` that has a timeout. * See https://valkey.io/commands/ttl/ for details. * diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 2cfc25b4ca..59991a1e6e 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2778,6 +2778,19 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `expiretime and pexpiretime test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + return; + } + + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `script test_%p`, async (protocol) => { From b7f33bec45909bea5e6de4f5b93825c75d39205b Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:21:18 -0700 Subject: [PATCH 03/13] add transaction tests Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 4 +-- node/src/Transaction.ts | 1 + node/tests/SharedTests.ts | 51 ++++++++++++++++++++++++++++++++++++- node/tests/TestUtilities.ts | 3 +++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1553968c69..b63a8c608c 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2443,7 +2443,7 @@ export class BaseClient { * ``` * since - Redis version 7.0.0. */ - public expiretime(key: string): Promise { + public async expiretime(key: string): Promise { return this.createWritePromise(createExpireTime(key)); } @@ -2527,7 +2527,7 @@ export class BaseClient { * ``` * since - Redis version 7.0.0. */ - public pexpiretime(key: string): Promise { + public async pexpiretime(key: string): Promise { return this.createWritePromise(createPExpireTime(key)); } diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 021b8d9198..3ff8c58a2d 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1321,6 +1321,7 @@ export class BaseTransaction> { * See https://valkey.io/commands/expiretime/ for details. * * @param key - The `key` to determine the expiration value of. + * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. */ public expireTime(key: string): T { diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 59991a1e6e..c47b43453d 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2785,7 +2785,56 @@ export function runBaseTests(config: { if (cluster.checkIfServerVersionLessThan("7.0.0")) { return; } - + + const key1 = uuidv4(); + + expect(await client.set(key1, "foo")).toEqual("OK"); + expect(await client.ttl(key1)).toEqual(-1); + + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.expiretime(key1)).toEqual(-1); + expect(await client.pexpiretime(key1)).toEqual(-1); + } + + expect(await client.expire(key1, 10)).toEqual(true); + expect(await client.ttl(key1)).toBeLessThanOrEqual(10); + + // set command clears the timeout. + expect(await client.set(key1, "bar")).toEqual("OK"); + + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.pexpire(key1, 10000)).toEqual(true); + } else { + expect( + await client.pexpire( + key1, + 10000, + ExpireOptions.HasNoExpiry, + ), + ).toEqual(true); + } + + expect(await client.ttl(key1)).toBeLessThanOrEqual(10000); + + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.expire(key1, 15000)).toEqual(true); + } else { + expect( + await client.pexpire( + key1, + 15000, + ExpireOptions.HasNoExpiry, + ), + ).toEqual(false); + expect(await client.expiretime(key1)).toBeGreaterThan( + Math.floor(Date.now() / 1000), + ); + expect(await client.pexpiretime(key1)).toBeGreaterThan( + Date.now(), + ); + } + + expect(await client.ttl(key1)).toBeLessThan(15000); }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 0267e02263..6e4550857a 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -496,6 +496,9 @@ export async function transactionTest( responseData.push(["echo(value)", value]); baseTransaction.persist(key1); responseData.push(["persist(key1)", false]); + // baseTransaction.expireTime(key1); + // responseData.push(["expiretime(key1), "]) + baseTransaction.set(key2, "baz", { returnOldValue: true }); responseData.push(['set(key2, "baz", { returnOldValue: true })', null]); baseTransaction.customCommand(["MGET", key1, key2]); From a38d7e9c58bd6395dfe4b4f299aaee130b41a5be Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:21:41 -0700 Subject: [PATCH 04/13] update changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf092e926d..ba9ea15e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added EXPIRETIME and PEXPIRETIME commands ([]()) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) * Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) From d0366a3b2b7b2e0a393b0a9bf4a60e6396fa0aae Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:49:45 -0700 Subject: [PATCH 05/13] implement expiretime series Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 32 ++++++++++++++++---------------- node/tests/TestUtilities.ts | 10 ++++++++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index c47b43453d..301d3659a4 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2786,55 +2786,55 @@ export function runBaseTests(config: { return; } - const key1 = uuidv4(); + const key = uuidv4(); - expect(await client.set(key1, "foo")).toEqual("OK"); - expect(await client.ttl(key1)).toEqual(-1); + expect(await client.set(key, "foo")).toEqual("OK"); + expect(await client.ttl(key)).toEqual(-1); if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.expiretime(key1)).toEqual(-1); - expect(await client.pexpiretime(key1)).toEqual(-1); + expect(await client.expiretime(key)).toEqual(-1); + expect(await client.pexpiretime(key)).toEqual(-1); } - expect(await client.expire(key1, 10)).toEqual(true); - expect(await client.ttl(key1)).toBeLessThanOrEqual(10); + expect(await client.expire(key, 10)).toEqual(true); + expect(await client.ttl(key)).toBeLessThanOrEqual(10); // set command clears the timeout. - expect(await client.set(key1, "bar")).toEqual("OK"); + expect(await client.set(key, "bar")).toEqual("OK"); if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.pexpire(key1, 10000)).toEqual(true); + expect(await client.pexpire(key, 10000)).toEqual(true); } else { expect( await client.pexpire( - key1, + key, 10000, ExpireOptions.HasNoExpiry, ), ).toEqual(true); } - expect(await client.ttl(key1)).toBeLessThanOrEqual(10000); + expect(await client.ttl(key)).toBeLessThanOrEqual(10000); if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.expire(key1, 15000)).toEqual(true); + expect(await client.expire(key, 15000)).toEqual(true); } else { expect( await client.pexpire( - key1, + key, 15000, ExpireOptions.HasNoExpiry, ), ).toEqual(false); - expect(await client.expiretime(key1)).toBeGreaterThan( + expect(await client.expiretime(key)).toBeGreaterThan( Math.floor(Date.now() / 1000), ); - expect(await client.pexpiretime(key1)).toBeGreaterThan( + expect(await client.pexpiretime(key)).toBeGreaterThan( Date.now(), ); } - expect(await client.ttl(key1)).toBeLessThan(15000); + expect(await client.ttl(key)).toBeLessThan(15000); }, protocol); }, config.timeout, diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 6e4550857a..7d95623aa4 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -496,8 +496,14 @@ export async function transactionTest( responseData.push(["echo(value)", value]); baseTransaction.persist(key1); responseData.push(["persist(key1)", false]); - // baseTransaction.expireTime(key1); - // responseData.push(["expiretime(key1), "]) + + if (gte(version, "7.0.0")) { + baseTransaction.expireTime(key1); + responseData.push(["expiretime(key1)", -1]); + + baseTransaction.pexpireTime(key1); + responseData.push(["pexpiretime(key1)", -1]); + } baseTransaction.set(key2, "baz", { returnOldValue: true }); responseData.push(['set(key2, "baz", { returnOldValue: true })', null]); From f168b71f178efb4430a52b2300bf4b7b7bbe418f Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:51:42 -0700 Subject: [PATCH 06/13] update changelog Signed-off-by: Chloe Yip --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9ea15e55..d3ba6dd211 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ #### Changes -* Node: Added EXPIRETIME and PEXPIRETIME commands ([]()) +* Node: Added EXPIRETIME and PEXPIRETIME commands ([#2063](https://github.com/valkey-io/valkey-glide/pull/2063)) * Node: Added LCS command ([#2049](https://github.com/valkey-io/valkey-glide/pull/2049)) * Node: Added MSETNX command ([#2046](https://github.com/valkey-io/valkey-glide/pull/2046)) * Node: Added BLMOVE command ([#2027](https://github.com/valkey-io/valkey-glide/pull/2027)) From 29d30e39bc81f856f8423250627cce414964e22d Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Wed, 31 Jul 2024 15:57:09 -0700 Subject: [PATCH 07/13] update transaction Signed-off-by: Chloe Yip --- node/src/Transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index eebcd24331..09ebc0a2e9 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1385,7 +1385,7 @@ export class BaseTransaction> { * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. */ public pexpireTime(key: string): T { - return this.addAndReturn(createExpireTime(key)); + return this.addAndReturn(createPExpireTime(key)); } /** Returns the remaining time to live of `key` that has a timeout. From 6f621dada77e859da845c1d60a4020e9cd26dd5e Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Thu, 1 Aug 2024 12:55:30 -0700 Subject: [PATCH 08/13] address comments Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 24 ++++++++++++++---------- node/src/Transaction.ts | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 537e482f07..fc6f72aa7e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2424,26 +2424,28 @@ export class BaseClient { /** * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. - * To get the expiration with millisecond precision, use `pexpiretime`. + * To get the expiration with millisecond precision, use {@link pexpiretime}. * * See https://valkey.io/commands/expiretime/ for details. * * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * + * since - Redis version 7.0.0. + * * @example * ```typescript - * const result1 = client.expiretime("myKey"); + * const result1 = await client.expiretime("myKey"); * console.log(result1); // Output: -2 - myKey doesn't exist. * - * const result2 = client.set(myKey, "value"); - * console.log(result2); // Output: -1 - myKey has no associate expiration. + * const result2 = await client.set(myKey, "value"); + * const result3 = await client.expireTime(myKey); + * console.log(result2); // Output: -1 - myKey has no associated expiration. * * client.expire(myKey, 60); - * const result3 = client.expireTime(myKey); - * console.log(result3); // Output: 718614954 + * const result3 = await client.expireTime(myKey); + * console.log(result3); // Output: 123456 - the Unix timestamp (in seconds) when "myKey" will expire. * ``` - * since - Redis version 7.0.0. */ public async expiretime(key: string): Promise { return this.createWritePromise(createExpireTime(key)); @@ -2515,19 +2517,21 @@ export class BaseClient { * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * + * since - Redis version 7.0.0. + * * @example * ```typescript * const result1 = client.pexpiretime("myKey"); * console.log(result1); // Output: -2 - myKey doesn't exist. * * const result2 = client.set(myKey, "value"); - * console.log(result2); // Output: -1 - myKey has no associate expiration. + * const result3 = client.pexpireTime(myKey); + * console.log(result2); // Output: -1 - myKey has no associated expiration. * * client.expire(myKey, 60); * const result3 = client.pexpireTime(myKey); - * console.log(result3); // Output: 718614954 + * console.log(result3); // Output: 123456789 - the Unix timestamp (in milliseconds) when "myKey" will expire. * ``` - * since - Redis version 7.0.0. */ public async pexpiretime(key: string): Promise { return this.createWritePromise(createPExpireTime(key)); diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 09ebc0a2e9..05d3452ab7 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1319,7 +1319,7 @@ export class BaseTransaction> { /** * Returns the absolute Unix timestamp (since January 1, 1970) at which the given `key` will expire, in seconds. - * To get the expiration with millisecond precision, use `pexpiretime`. + * To get the expiration with millisecond precision, use {@link pexpiretime}. * * See https://valkey.io/commands/expiretime/ for details. * From ed7c26df2c92f37c87efae3a84dde59f07583ab1 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Thu, 1 Aug 2024 17:46:44 -0700 Subject: [PATCH 09/13] change test name Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index db68d0a9eb..332497db22 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2807,7 +2807,7 @@ export function runBaseTests(config: { ); it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `expiretime and pexpiretime test_%p`, + `expireTime, pexpireTime, expire, pexpire, expireAt and pexpireAt with timestamp in the past or negative timeout test_%p`, async (protocol) => { await runTest(async (client: BaseClient, cluster) => { if (cluster.checkIfServerVersionLessThan("7.0.0")) { From b1431d4101231f47e5fb2919bff16866e423af8c Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 2 Aug 2024 11:49:18 -0700 Subject: [PATCH 10/13] fix shared test Signed-off-by: Chloe Yip --- node/tests/SharedTests.ts | 78 +++++++++------------------------------ 1 file changed, 17 insertions(+), 61 deletions(-) diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 332497db22..d2f7dacb9c 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2688,6 +2688,12 @@ export function runBaseTests(config: { ExpireOptions.HasExistingExpiry, ), ).toEqual(true); + expect(await client.expiretime(key)).toBeGreaterThan( + Math.floor(Date.now() / 1000), + ); + expect(await client.pexpiretime(key)).toBeGreaterThan( + Date.now(), + ); } expect(await client.ttl(key)).toBeLessThanOrEqual(15); @@ -2751,7 +2757,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `expire, pexpire, expireAt and pexpireAt with timestamp in the past or negative timeout_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); expect(await client.set(key, "foo")).toEqual("OK"); expect(await client.ttl(key)).toEqual(-1); @@ -2769,6 +2775,13 @@ export function runBaseTests(config: { ).toEqual(true); expect(await client.ttl(key)).toEqual(-2); expect(await client.set(key, "foo")).toEqual("OK"); + + // no timeout set yet + if (cluster.checkIfServerVersionLessThan("7.0.0")) { + expect(await client.expiretime(key)).toEqual(-1); + expect(await client.pexpiretime(key)).toEqual(-1); + } + expect( await client.pexpireAt( key, @@ -2784,7 +2797,7 @@ export function runBaseTests(config: { it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `expire, pexpire, expireAt, pexpireAt and ttl with non-existing key_%p`, async (protocol) => { - await runTest(async (client: BaseClient) => { + await runTest(async (client: BaseClient, cluster) => { const key = uuidv4(); expect(await client.expire(key, 10)).toEqual(false); expect(await client.pexpire(key, 10000)).toEqual(false); @@ -2801,68 +2814,11 @@ export function runBaseTests(config: { ), ).toEqual(false); expect(await client.ttl(key)).toEqual(-2); - }, protocol); - }, - config.timeout, - ); - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `expireTime, pexpireTime, expire, pexpire, expireAt and pexpireAt with timestamp in the past or negative timeout test_%p`, - async (protocol) => { - await runTest(async (client: BaseClient, cluster) => { if (cluster.checkIfServerVersionLessThan("7.0.0")) { - return; + expect(await client.expiretime(key)).toEqual(-2); + expect(await client.pexpiretime(key)).toEqual(-2); } - - const key = uuidv4(); - - expect(await client.set(key, "foo")).toEqual("OK"); - expect(await client.ttl(key)).toEqual(-1); - - if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.expiretime(key)).toEqual(-1); - expect(await client.pexpiretime(key)).toEqual(-1); - } - - expect(await client.expire(key, 10)).toEqual(true); - expect(await client.ttl(key)).toBeLessThanOrEqual(10); - - // set command clears the timeout. - expect(await client.set(key, "bar")).toEqual("OK"); - - if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.pexpire(key, 10000)).toEqual(true); - } else { - expect( - await client.pexpire( - key, - 10000, - ExpireOptions.HasNoExpiry, - ), - ).toEqual(true); - } - - expect(await client.ttl(key)).toBeLessThanOrEqual(10000); - - if (cluster.checkIfServerVersionLessThan("7.0.0")) { - expect(await client.expire(key, 15000)).toEqual(true); - } else { - expect( - await client.pexpire( - key, - 15000, - ExpireOptions.HasNoExpiry, - ), - ).toEqual(false); - expect(await client.expiretime(key)).toBeGreaterThan( - Math.floor(Date.now() / 1000), - ); - expect(await client.pexpiretime(key)).toBeGreaterThan( - Date.now(), - ); - } - - expect(await client.ttl(key)).toBeLessThan(15000); }, protocol); }, config.timeout, From 8cd4620b07177b6e3cb2f21dcf266d71b8e803ab Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 2 Aug 2024 12:02:27 -0700 Subject: [PATCH 11/13] address new comments Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 2 +- node/src/Transaction.ts | 4 + node/tests/RedisClusterClient.test.ts | 985 ++++++++++++++++++++++++++ node/tests/SharedTests.ts | 4 +- 4 files changed, 992 insertions(+), 3 deletions(-) create mode 100644 node/tests/RedisClusterClient.test.ts diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 1a2d9d12a3..bb5c9ce8ef 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2436,7 +2436,7 @@ export class BaseClient { * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * - * since - Redis version 7.0.0. + * since - Valkey version 7.0.0. * * @example * ```typescript diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 7d544ebd87..8a301c053a 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -1341,6 +1341,8 @@ export class BaseTransaction> { * @param key - The `key` to determine the expiration value of. * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * + * since Valkey version 7.0.0. */ public expireTime(key: string): T { return this.addAndReturn(createExpireTime(key)); @@ -1398,6 +1400,8 @@ export class BaseTransaction> { * @param key - The `key` to determine the expiration value of. * * Command Response - The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. + * + * since Valkey version 7.0.0. */ public pexpireTime(key: string): T { return this.addAndReturn(createPExpireTime(key)); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts new file mode 100644 index 0000000000..7d25ddc8fe --- /dev/null +++ b/node/tests/RedisClusterClient.test.ts @@ -0,0 +1,985 @@ +/** + * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 + */ + +import { afterAll, afterEach, beforeAll, describe, it } from "@jest/globals"; +import { gte } from "semver"; +import { v4 as uuidv4 } from "uuid"; +import { + BitwiseOperation, + ClusterTransaction, + FunctionListResponse, + GlideClusterClient, + InfoOptions, + ListDirection, + ProtocolVersion, + Routes, + ScoreFilter, +} from ".."; +import { RedisCluster } from "../../utils/TestUtils.js"; +import { FlushMode } from "../build-ts/src/Commands"; +import { runBaseTests } from "./SharedTests"; +import { + checkClusterResponse, + checkFunctionListResponse, + flushAndCloseClient, + generateLuaLibCode, + getClientConfigurationOption, + getFirstResult, + intoArray, + intoString, + parseCommandLineArgs, + parseEndpoints, + transactionTest, + validateTransactionResponse, +} from "./TestUtilities"; +type Context = { + client: GlideClusterClient; +}; + +const TIMEOUT = 50000; + +describe("GlideClusterClient", () => { + let testsFailed = 0; + let cluster: RedisCluster; + let client: GlideClusterClient; + beforeAll(async () => { + const clusterAddresses = parseCommandLineArgs()["cluster-endpoints"]; + // Connect to cluster or create a new one based on the parsed addresses + cluster = clusterAddresses + ? await RedisCluster.initFromExistingCluster( + parseEndpoints(clusterAddresses), + ) + : // setting replicaCount to 1 to facilitate tests routed to replicas + await RedisCluster.createCluster(true, 3, 1); + }, 20000); + + afterEach(async () => { + await flushAndCloseClient(true, cluster.getAddresses(), client); + }); + + afterAll(async () => { + if (testsFailed === 0) { + await cluster.close(); + } + }); + + runBaseTests({ + init: async (protocol, clientName?) => { + const options = getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ); + options.protocol = protocol; + options.clientName = clientName; + testsFailed += 1; + client = await GlideClusterClient.createClient(options); + return { + context: { + client, + }, + client, + cluster, + }; + }, + close: (context: Context, testSucceeded: boolean) => { + if (testSucceeded) { + testsFailed -= 1; + } + }, + timeout: TIMEOUT, + }); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `info with server and replication_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const info_server = getFirstResult( + await client.info([InfoOptions.Server]), + ); + expect(intoString(info_server)).toEqual( + expect.stringContaining("# Server"), + ); + + const infoReplicationValues = Object.values( + await client.info([InfoOptions.Replication]), + ); + + const replicationInfo = intoArray(infoReplicationValues); + + for (const item of replicationInfo) { + expect(item).toContain("role:master"); + expect(item).toContain("# Replication"); + } + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `info with server and randomNode route_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const result = await client.info( + [InfoOptions.Server], + "randomNode", + ); + expect(intoString(result)).toEqual( + expect.stringContaining("# Server"), + ); + expect(intoString(result)).toEqual( + expect.not.stringContaining("# Errorstats"), + ); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `route by address reaches correct node_%p`, + async (protocol) => { + // returns the line that contains the word "myself", up to that point. This is done because the values after it might change with time. + const cleanResult = (value: string) => { + return ( + value + .split("\n") + .find((line: string) => line.includes("myself")) + ?.split("myself")[0] ?? "" + ); + }; + + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const result = cleanResult( + intoString( + await client.customCommand( + ["cluster", "nodes"], + "randomNode", + ), + ), + ); + + // check that routing without explicit port works + const host = result.split(" ")[1].split("@")[0] ?? ""; + + if (!host) { + throw new Error("No host could be parsed"); + } + + const secondResult = cleanResult( + intoString( + await client.customCommand(["cluster", "nodes"], { + type: "routeByAddress", + host, + }), + ), + ); + + expect(result).toEqual(secondResult); + + const [host2, port] = host.split(":"); + + // check that routing with explicit port works + const thirdResult = cleanResult( + intoString( + await client.customCommand(["cluster", "nodes"], { + type: "routeByAddress", + host: host2, + port: Number(port), + }), + ), + ); + + expect(result).toEqual(thirdResult); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `fail routing by address if no port is provided_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + expect(() => + client.info(undefined, { + type: "routeByAddress", + host: "foo", + }), + ).toThrowError(); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `config get and config set transactions test_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const transaction = new ClusterTransaction(); + transaction.configSet({ timeout: "1000" }); + transaction.configGet(["timeout"]); + const result = await client.exec(transaction); + expect(intoString(result)).toEqual( + intoString(["OK", { timeout: "1000" }]), + ); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `can send transactions_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const transaction = new ClusterTransaction(); + const expectedRes = await transactionTest( + transaction, + cluster.getVersion(), + ); + const result = await client.exec(transaction); + validateTransactionResponse(result, expectedRes); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `can return null on WATCH transaction failures_%p`, + async (protocol) => { + const client1 = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const client2 = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const transaction = new ClusterTransaction(); + transaction.get("key"); + const result1 = await client1.customCommand(["WATCH", "key"]); + expect(result1).toEqual("OK"); + + const result2 = await client2.set("key", "foo"); + expect(result2).toEqual("OK"); + + const result3 = await client1.exec(transaction); + expect(result3).toBeNull(); + + client1.close(); + client2.close(); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `echo with all nodes routing_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + const message = uuidv4(); + const echoDict = await client.echo(message, "allNodes"); + + expect(typeof echoDict).toBe("object"); + expect(intoArray(echoDict)).toEqual( + expect.arrayContaining(intoArray([message])), + ); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `check that multi key command returns a cross slot error`, + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const promises: Promise[] = [ + client.blpop(["abc", "zxy", "lkn"], 0.1), + client.rename("abc", "zxy"), + client.msetnx({ abc: "xyz", def: "abc", hij: "def" }), + client.brpop(["abc", "zxy", "lkn"], 0.1), + client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), + client.smove("abc", "zxy", "value"), + client.renamenx("abc", "zxy"), + client.sinter(["abc", "zxy", "lkn"]), + client.sinterstore("abc", ["zxy", "lkn"]), + client.zinterstore("abc", ["zxy", "lkn"]), + client.sunionstore("abc", ["zxy", "lkn"]), + client.sunion(["abc", "zxy", "lkn"]), + client.pfcount(["abc", "zxy", "lkn"]), + client.pfmerge("abc", ["def", "ghi"]), + client.sdiff(["abc", "zxy", "lkn"]), + client.sdiffstore("abc", ["zxy", "lkn"]), + ]; + + if (gte(cluster.getVersion(), "6.2.0")) { + promises.push( + client.blmove( + "abc", + "def", + ListDirection.LEFT, + ListDirection.LEFT, + 0.2, + ), + client.zdiff(["abc", "zxy", "lkn"]), + client.zdiffWithScores(["abc", "zxy", "lkn"]), + client.zdiffstore("abc", ["zxy", "lkn"]), + client.copy("abc", "zxy", true), + ); + } + + if (gte(cluster.getVersion(), "7.0.0")) { + promises.push( + client.sintercard(["abc", "zxy", "lkn"]), + client.zintercard(["abc", "zxy", "lkn"]), + client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), + client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1), + client.lcs("abc", "xyz"), + client.lcsLen("abc", "xyz"), + client.lcsIdx("abc", "xyz"), + ); + } + + for (const promise of promises) { + await expect(promise).rejects.toThrowError(/crossslot/i); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `check that multi key command routed to multiple nodes`, + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + await client.exists(["abc", "zxy", "lkn"]); + await client.unlink(["abc", "zxy", "lkn"]); + await client.del(["abc", "zxy", "lkn"]); + await client.mget(["abc", "zxy", "lkn"]); + await client.mset({ abc: "1", zxy: "2", lkn: "3" }); + await client.touch(["abc", "zxy", "lkn"]); + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object freq transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + const transaction = new ClusterTransaction(); + transaction.configSet({ + [maxmemoryPolicyKey]: "allkeys-lfu", + }); + transaction.set(key, "foo"); + transaction.objectFreq(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(3); + expect(response[0]).toEqual("OK"); + expect(response[1]).toEqual("OK"); + expect(response[2]).toBeGreaterThanOrEqual(0); + } + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object idletime transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const maxmemoryPolicyKey = "maxmemory-policy"; + const config = await client.configGet([maxmemoryPolicyKey]); + const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); + + try { + const transaction = new ClusterTransaction(); + transaction.configSet({ + // OBJECT IDLETIME requires a non-LFU maxmemory-policy + [maxmemoryPolicyKey]: "allkeys-random", + }); + transaction.set(key, "foo"); + transaction.objectIdletime(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(3); + // transaction.configSet({[maxmemoryPolicyKey]: "allkeys-random"}); + expect(response[0]).toEqual("OK"); + // transaction.set(key, "foo"); + expect(response[1]).toEqual("OK"); + // transaction.objectIdletime(key); + expect(response[2]).toBeGreaterThanOrEqual(0); + } + } finally { + expect( + await client.configSet({ + [maxmemoryPolicyKey]: maxmemoryPolicy, + }), + ).toEqual("OK"); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "object refcount transaction test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + const key = uuidv4(); + const transaction = new ClusterTransaction(); + transaction.set(key, "foo"); + transaction.objectRefcount(key); + + const response = await client.exec(transaction); + expect(response).not.toBeNull(); + + if (response != null) { + expect(response.length).toEqual(2); + expect(response[0]).toEqual("OK"); // transaction.set(key, "foo"); + expect(response[1]).toBeGreaterThanOrEqual(1); // transaction.objectRefcount(key); + } + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lolwut test_%p`, + async (protocol) => { + client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + // test with multi-node route + const result1 = await client.lolwut({}, "allNodes"); + expect(intoString(result1)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result2 = await client.lolwut( + { version: 2, parameters: [10, 20] }, + "allNodes", + ); + expect(intoString(result2)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + // test with single-node route + const result3 = await client.lolwut({}, "randomNode"); + expect(intoString(result3)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + const result4 = await client.lolwut( + { version: 2, parameters: [10, 20] }, + "randomNode", + ); + expect(intoString(result4)).toEqual( + expect.stringContaining("Redis ver. "), + ); + + // transaction tests + const transaction = new ClusterTransaction(); + transaction.lolwut(); + transaction.lolwut({ version: 5 }); + transaction.lolwut({ parameters: [1, 2] }); + transaction.lolwut({ version: 6, parameters: [42] }); + const results = await client.exec(transaction); + + if (results) { + for (const element of results) { + expect(intoString(element)).toEqual( + expect.stringContaining("Redis ver. "), + ); + } + } else { + throw new Error("Invalid LOLWUT transaction test results."); + } + + client.close(); + }, + TIMEOUT, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "copy test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + if (cluster.checkIfServerVersionLessThan("6.2.0")) return; + + const source = `{key}-${uuidv4()}`; + const destination = `{key}-${uuidv4()}`; + const value1 = uuidv4(); + const value2 = uuidv4(); + + // neither key exists + expect(await client.copy(source, destination, true)).toEqual(false); + expect(await client.copy(source, destination)).toEqual(false); + + // source exists, destination does not + expect(await client.set(source, value1)).toEqual("OK"); + expect(await client.copy(source, destination, false)).toEqual(true); + expect(await client.get(destination)).toEqual(value1); + + // new value for source key + expect(await client.set(source, value2)).toEqual("OK"); + + // both exists, no REPLACE + expect(await client.copy(source, destination)).toEqual(false); + expect(await client.copy(source, destination, false)).toEqual( + false, + ); + expect(await client.get(destination)).toEqual(value1); + + // both exists, with REPLACE + expect(await client.copy(source, destination, true)).toEqual(true); + expect(await client.get(destination)).toEqual(value2); + + //transaction tests + const transaction = new ClusterTransaction(); + transaction.set(source, value1); + transaction.copy(source, destination, true); + transaction.get(destination); + const results = await client.exec(transaction); + + expect(results).toEqual(["OK", true, value1]); + + client.close(); + }, + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "flushdb flushall dbsize test_%p", + async (protocol) => { + const client = await GlideClusterClient.createClient( + getClientConfigurationOption(cluster.getAddresses(), protocol), + ); + + expect(await client.dbsize()).toBeGreaterThanOrEqual(0); + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toBeGreaterThan(0); + + expect(await client.flushall()).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toEqual(1); + expect(await client.flushdb(FlushMode.ASYNC)).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); + expect(await client.dbsize()).toEqual(1); + expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); + expect(await client.dbsize()).toEqual(0); + + client.close(); + }, + ); + + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function load and function list", + async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + + let functionList = await client.functionList( + { libNamePattern: libName }, + route, + ); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + // load the library + expect(await client.functionLoad(code)).toEqual( + libName, + ); + + functionList = await client.functionList( + { libNamePattern: libName }, + route, + ); + let expectedDescription = new Map< + string, + string | null + >([[funcName, null]]); + let expectedFlags = new Map([ + [funcName, ["no-writes"]], + ]); + + checkClusterResponse( + functionList, + singleNodeRoute, + (value) => + checkFunctionListResponse( + value as FunctionListResponse, + libName, + expectedDescription, + expectedFlags, + ), + ); + + // call functions from that library to confirm that it works + let fcall = await client.fcallWithRoute( + funcName, + ["one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual("one"), + ); + fcall = await client.fcallReadonlyWithRoute( + funcName, + ["one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual("one"), + ); + + // re-load library without replace + await expect( + client.functionLoad(code), + ).rejects.toThrow( + `Library '${libName}' already exists`, + ); + + // re-load library with replace + expect( + await client.functionLoad(code, true), + ).toEqual(libName); + + // overwrite lib with new code + const func2Name = + "myfunc2c" + uuidv4().replaceAll("-", ""); + const newCode = generateLuaLibCode( + libName, + new Map([ + [funcName, "return args[1]"], + [func2Name, "return #args"], + ]), + true, + ); + expect( + await client.functionLoad(newCode, true), + ).toEqual(libName); + + functionList = await client.functionList( + { libNamePattern: libName, withCode: true }, + route, + ); + expectedDescription = new Map< + string, + string | null + >([ + [funcName, null], + [func2Name, null], + ]); + expectedFlags = new Map([ + [funcName, ["no-writes"]], + [func2Name, ["no-writes"]], + ]); + + checkClusterResponse( + functionList, + singleNodeRoute, + (value) => + checkFunctionListResponse( + value as FunctionListResponse, + libName, + expectedDescription, + expectedFlags, + newCode, + ), + ); + + fcall = await client.fcallWithRoute( + func2Name, + ["one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual(2), + ); + + fcall = await client.fcallReadonlyWithRoute( + func2Name, + ["one", "two"], + route, + ); + checkClusterResponse( + fcall as object, + singleNodeRoute, + (value) => expect(value).toEqual(2), + ); + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); + + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function flush", + async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + + const functionList1 = await client.functionList( + {}, + route, + ); + checkClusterResponse( + functionList1 as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // load the library + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + + // flush functions + expect( + await client.functionFlush( + FlushMode.SYNC, + route, + ), + ).toEqual("OK"); + expect( + await client.functionFlush( + FlushMode.ASYNC, + route, + ), + ).toEqual("OK"); + + const functionList2 = + await client.functionList(); + checkClusterResponse( + functionList2 as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // Attempt to re-load library without overwriting to ensure FLUSH was effective + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); + + describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + "Protocol is RESP2 = %s", + (protocol) => { + describe.each([true, false])( + "Single node route = %s", + (singleNodeRoute) => { + it( + "function delete", + async () => { + if (cluster.checkIfServerVersionLessThan("7.0.0")) + return; + + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + + try { + const libName = + "mylib1C" + uuidv4().replaceAll("-", ""); + const funcName = + "myfunc1c" + uuidv4().replaceAll("-", ""); + const code = generateLuaLibCode( + libName, + new Map([[funcName, "return args[1]"]]), + true, + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + let functionList = await client.functionList( + {}, + route, + ); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + // load the library + expect( + await client.functionLoad( + code, + undefined, + route, + ), + ).toEqual(libName); + + // Delete the function + expect( + await client.functionDelete(libName, route), + ).toEqual("OK"); + + functionList = await client.functionList( + { libNamePattern: libName, withCode: true }, + route, + ); + checkClusterResponse( + functionList as object, + singleNodeRoute, + (value) => expect(value).toEqual([]), + ); + + // Delete a non-existing library + await expect( + client.functionDelete(libName, route), + ).rejects.toThrow(`Library not found`); + } finally { + expect(await client.functionFlush()).toEqual( + "OK", + ); + client.close(); + } + }, + TIMEOUT, + ); + }, + ); + }, + ); +}); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index d2f7dacb9c..334d6acfa6 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -2777,7 +2777,7 @@ export function runBaseTests(config: { expect(await client.set(key, "foo")).toEqual("OK"); // no timeout set yet - if (cluster.checkIfServerVersionLessThan("7.0.0")) { + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { expect(await client.expiretime(key)).toEqual(-1); expect(await client.pexpiretime(key)).toEqual(-1); } @@ -2815,7 +2815,7 @@ export function runBaseTests(config: { ).toEqual(false); expect(await client.ttl(key)).toEqual(-2); - if (cluster.checkIfServerVersionLessThan("7.0.0")) { + if (!cluster.checkIfServerVersionLessThan("7.0.0")) { expect(await client.expiretime(key)).toEqual(-2); expect(await client.pexpiretime(key)).toEqual(-2); } From cb28adcab8692da0bcb44a4f44e967dea2815a67 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 2 Aug 2024 12:26:55 -0700 Subject: [PATCH 12/13] change the since version Signed-off-by: Chloe Yip --- node/src/BaseClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index bb5c9ce8ef..ebbaddb346 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -2436,7 +2436,7 @@ export class BaseClient { * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * - * since - Valkey version 7.0.0. + * since Valkey version 7.0.0. * * @example * ```typescript @@ -2522,7 +2522,7 @@ export class BaseClient { * @param key - The `key` to determine the expiration value of. * @returns The expiration Unix timestamp in seconds, `-2` if `key` does not exist or `-1` if `key` exists but has no associated expire. * - * since - Redis version 7.0.0. + * since Valkey version 7.0.0. * * @example * ```typescript From eb0bea4e770328f42915f328d5f96701c66cfd51 Mon Sep 17 00:00:00 2001 From: Chloe Yip Date: Fri, 2 Aug 2024 12:28:48 -0700 Subject: [PATCH 13/13] remove redis cluster test file Signed-off-by: Chloe Yip --- node/tests/RedisClusterClient.test.ts | 985 -------------------------- 1 file changed, 985 deletions(-) delete mode 100644 node/tests/RedisClusterClient.test.ts diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts deleted file mode 100644 index 7d25ddc8fe..0000000000 --- a/node/tests/RedisClusterClient.test.ts +++ /dev/null @@ -1,985 +0,0 @@ -/** - * Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0 - */ - -import { afterAll, afterEach, beforeAll, describe, it } from "@jest/globals"; -import { gte } from "semver"; -import { v4 as uuidv4 } from "uuid"; -import { - BitwiseOperation, - ClusterTransaction, - FunctionListResponse, - GlideClusterClient, - InfoOptions, - ListDirection, - ProtocolVersion, - Routes, - ScoreFilter, -} from ".."; -import { RedisCluster } from "../../utils/TestUtils.js"; -import { FlushMode } from "../build-ts/src/Commands"; -import { runBaseTests } from "./SharedTests"; -import { - checkClusterResponse, - checkFunctionListResponse, - flushAndCloseClient, - generateLuaLibCode, - getClientConfigurationOption, - getFirstResult, - intoArray, - intoString, - parseCommandLineArgs, - parseEndpoints, - transactionTest, - validateTransactionResponse, -} from "./TestUtilities"; -type Context = { - client: GlideClusterClient; -}; - -const TIMEOUT = 50000; - -describe("GlideClusterClient", () => { - let testsFailed = 0; - let cluster: RedisCluster; - let client: GlideClusterClient; - beforeAll(async () => { - const clusterAddresses = parseCommandLineArgs()["cluster-endpoints"]; - // Connect to cluster or create a new one based on the parsed addresses - cluster = clusterAddresses - ? await RedisCluster.initFromExistingCluster( - parseEndpoints(clusterAddresses), - ) - : // setting replicaCount to 1 to facilitate tests routed to replicas - await RedisCluster.createCluster(true, 3, 1); - }, 20000); - - afterEach(async () => { - await flushAndCloseClient(true, cluster.getAddresses(), client); - }); - - afterAll(async () => { - if (testsFailed === 0) { - await cluster.close(); - } - }); - - runBaseTests({ - init: async (protocol, clientName?) => { - const options = getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ); - options.protocol = protocol; - options.clientName = clientName; - testsFailed += 1; - client = await GlideClusterClient.createClient(options); - return { - context: { - client, - }, - client, - cluster, - }; - }, - close: (context: Context, testSucceeded: boolean) => { - if (testSucceeded) { - testsFailed -= 1; - } - }, - timeout: TIMEOUT, - }); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `info with server and replication_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const info_server = getFirstResult( - await client.info([InfoOptions.Server]), - ); - expect(intoString(info_server)).toEqual( - expect.stringContaining("# Server"), - ); - - const infoReplicationValues = Object.values( - await client.info([InfoOptions.Replication]), - ); - - const replicationInfo = intoArray(infoReplicationValues); - - for (const item of replicationInfo) { - expect(item).toContain("role:master"); - expect(item).toContain("# Replication"); - } - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `info with server and randomNode route_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const result = await client.info( - [InfoOptions.Server], - "randomNode", - ); - expect(intoString(result)).toEqual( - expect.stringContaining("# Server"), - ); - expect(intoString(result)).toEqual( - expect.not.stringContaining("# Errorstats"), - ); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `route by address reaches correct node_%p`, - async (protocol) => { - // returns the line that contains the word "myself", up to that point. This is done because the values after it might change with time. - const cleanResult = (value: string) => { - return ( - value - .split("\n") - .find((line: string) => line.includes("myself")) - ?.split("myself")[0] ?? "" - ); - }; - - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const result = cleanResult( - intoString( - await client.customCommand( - ["cluster", "nodes"], - "randomNode", - ), - ), - ); - - // check that routing without explicit port works - const host = result.split(" ")[1].split("@")[0] ?? ""; - - if (!host) { - throw new Error("No host could be parsed"); - } - - const secondResult = cleanResult( - intoString( - await client.customCommand(["cluster", "nodes"], { - type: "routeByAddress", - host, - }), - ), - ); - - expect(result).toEqual(secondResult); - - const [host2, port] = host.split(":"); - - // check that routing with explicit port works - const thirdResult = cleanResult( - intoString( - await client.customCommand(["cluster", "nodes"], { - type: "routeByAddress", - host: host2, - port: Number(port), - }), - ), - ); - - expect(result).toEqual(thirdResult); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `fail routing by address if no port is provided_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - expect(() => - client.info(undefined, { - type: "routeByAddress", - host: "foo", - }), - ).toThrowError(); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `config get and config set transactions test_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const transaction = new ClusterTransaction(); - transaction.configSet({ timeout: "1000" }); - transaction.configGet(["timeout"]); - const result = await client.exec(transaction); - expect(intoString(result)).toEqual( - intoString(["OK", { timeout: "1000" }]), - ); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `can send transactions_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const transaction = new ClusterTransaction(); - const expectedRes = await transactionTest( - transaction, - cluster.getVersion(), - ); - const result = await client.exec(transaction); - validateTransactionResponse(result, expectedRes); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `can return null on WATCH transaction failures_%p`, - async (protocol) => { - const client1 = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const client2 = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const transaction = new ClusterTransaction(); - transaction.get("key"); - const result1 = await client1.customCommand(["WATCH", "key"]); - expect(result1).toEqual("OK"); - - const result2 = await client2.set("key", "foo"); - expect(result2).toEqual("OK"); - - const result3 = await client1.exec(transaction); - expect(result3).toBeNull(); - - client1.close(); - client2.close(); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `echo with all nodes routing_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - const message = uuidv4(); - const echoDict = await client.echo(message, "allNodes"); - - expect(typeof echoDict).toBe("object"); - expect(intoArray(echoDict)).toEqual( - expect.arrayContaining(intoArray([message])), - ); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `check that multi key command returns a cross slot error`, - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - const promises: Promise[] = [ - client.blpop(["abc", "zxy", "lkn"], 0.1), - client.rename("abc", "zxy"), - client.msetnx({ abc: "xyz", def: "abc", hij: "def" }), - client.brpop(["abc", "zxy", "lkn"], 0.1), - client.bitop(BitwiseOperation.AND, "abc", ["zxy", "lkn"]), - client.smove("abc", "zxy", "value"), - client.renamenx("abc", "zxy"), - client.sinter(["abc", "zxy", "lkn"]), - client.sinterstore("abc", ["zxy", "lkn"]), - client.zinterstore("abc", ["zxy", "lkn"]), - client.sunionstore("abc", ["zxy", "lkn"]), - client.sunion(["abc", "zxy", "lkn"]), - client.pfcount(["abc", "zxy", "lkn"]), - client.pfmerge("abc", ["def", "ghi"]), - client.sdiff(["abc", "zxy", "lkn"]), - client.sdiffstore("abc", ["zxy", "lkn"]), - ]; - - if (gte(cluster.getVersion(), "6.2.0")) { - promises.push( - client.blmove( - "abc", - "def", - ListDirection.LEFT, - ListDirection.LEFT, - 0.2, - ), - client.zdiff(["abc", "zxy", "lkn"]), - client.zdiffWithScores(["abc", "zxy", "lkn"]), - client.zdiffstore("abc", ["zxy", "lkn"]), - client.copy("abc", "zxy", true), - ); - } - - if (gte(cluster.getVersion(), "7.0.0")) { - promises.push( - client.sintercard(["abc", "zxy", "lkn"]), - client.zintercard(["abc", "zxy", "lkn"]), - client.zmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX), - client.bzmpop(["abc", "zxy", "lkn"], ScoreFilter.MAX, 0.1), - client.lcs("abc", "xyz"), - client.lcsLen("abc", "xyz"), - client.lcsIdx("abc", "xyz"), - ); - } - - for (const promise of promises) { - await expect(promise).rejects.toThrowError(/crossslot/i); - } - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `check that multi key command routed to multiple nodes`, - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - await client.exists(["abc", "zxy", "lkn"]); - await client.unlink(["abc", "zxy", "lkn"]); - await client.del(["abc", "zxy", "lkn"]); - await client.mget(["abc", "zxy", "lkn"]); - await client.mset({ abc: "1", zxy: "2", lkn: "3" }); - await client.touch(["abc", "zxy", "lkn"]); - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "object freq transaction test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - const key = uuidv4(); - const maxmemoryPolicyKey = "maxmemory-policy"; - const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); - - try { - const transaction = new ClusterTransaction(); - transaction.configSet({ - [maxmemoryPolicyKey]: "allkeys-lfu", - }); - transaction.set(key, "foo"); - transaction.objectFreq(key); - - const response = await client.exec(transaction); - expect(response).not.toBeNull(); - - if (response != null) { - expect(response.length).toEqual(3); - expect(response[0]).toEqual("OK"); - expect(response[1]).toEqual("OK"); - expect(response[2]).toBeGreaterThanOrEqual(0); - } - } finally { - expect( - await client.configSet({ - [maxmemoryPolicyKey]: maxmemoryPolicy, - }), - ).toEqual("OK"); - } - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "object idletime transaction test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - const key = uuidv4(); - const maxmemoryPolicyKey = "maxmemory-policy"; - const config = await client.configGet([maxmemoryPolicyKey]); - const maxmemoryPolicy = String(config[maxmemoryPolicyKey]); - - try { - const transaction = new ClusterTransaction(); - transaction.configSet({ - // OBJECT IDLETIME requires a non-LFU maxmemory-policy - [maxmemoryPolicyKey]: "allkeys-random", - }); - transaction.set(key, "foo"); - transaction.objectIdletime(key); - - const response = await client.exec(transaction); - expect(response).not.toBeNull(); - - if (response != null) { - expect(response.length).toEqual(3); - // transaction.configSet({[maxmemoryPolicyKey]: "allkeys-random"}); - expect(response[0]).toEqual("OK"); - // transaction.set(key, "foo"); - expect(response[1]).toEqual("OK"); - // transaction.objectIdletime(key); - expect(response[2]).toBeGreaterThanOrEqual(0); - } - } finally { - expect( - await client.configSet({ - [maxmemoryPolicyKey]: maxmemoryPolicy, - }), - ).toEqual("OK"); - } - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "object refcount transaction test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - const key = uuidv4(); - const transaction = new ClusterTransaction(); - transaction.set(key, "foo"); - transaction.objectRefcount(key); - - const response = await client.exec(transaction); - expect(response).not.toBeNull(); - - if (response != null) { - expect(response.length).toEqual(2); - expect(response[0]).toEqual("OK"); // transaction.set(key, "foo"); - expect(response[1]).toBeGreaterThanOrEqual(1); // transaction.objectRefcount(key); - } - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - `lolwut test_%p`, - async (protocol) => { - client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - // test with multi-node route - const result1 = await client.lolwut({}, "allNodes"); - expect(intoString(result1)).toEqual( - expect.stringContaining("Redis ver. "), - ); - - const result2 = await client.lolwut( - { version: 2, parameters: [10, 20] }, - "allNodes", - ); - expect(intoString(result2)).toEqual( - expect.stringContaining("Redis ver. "), - ); - - // test with single-node route - const result3 = await client.lolwut({}, "randomNode"); - expect(intoString(result3)).toEqual( - expect.stringContaining("Redis ver. "), - ); - - const result4 = await client.lolwut( - { version: 2, parameters: [10, 20] }, - "randomNode", - ); - expect(intoString(result4)).toEqual( - expect.stringContaining("Redis ver. "), - ); - - // transaction tests - const transaction = new ClusterTransaction(); - transaction.lolwut(); - transaction.lolwut({ version: 5 }); - transaction.lolwut({ parameters: [1, 2] }); - transaction.lolwut({ version: 6, parameters: [42] }); - const results = await client.exec(transaction); - - if (results) { - for (const element of results) { - expect(intoString(element)).toEqual( - expect.stringContaining("Redis ver. "), - ); - } - } else { - throw new Error("Invalid LOLWUT transaction test results."); - } - - client.close(); - }, - TIMEOUT, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "copy test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - if (cluster.checkIfServerVersionLessThan("6.2.0")) return; - - const source = `{key}-${uuidv4()}`; - const destination = `{key}-${uuidv4()}`; - const value1 = uuidv4(); - const value2 = uuidv4(); - - // neither key exists - expect(await client.copy(source, destination, true)).toEqual(false); - expect(await client.copy(source, destination)).toEqual(false); - - // source exists, destination does not - expect(await client.set(source, value1)).toEqual("OK"); - expect(await client.copy(source, destination, false)).toEqual(true); - expect(await client.get(destination)).toEqual(value1); - - // new value for source key - expect(await client.set(source, value2)).toEqual("OK"); - - // both exists, no REPLACE - expect(await client.copy(source, destination)).toEqual(false); - expect(await client.copy(source, destination, false)).toEqual( - false, - ); - expect(await client.get(destination)).toEqual(value1); - - // both exists, with REPLACE - expect(await client.copy(source, destination, true)).toEqual(true); - expect(await client.get(destination)).toEqual(value2); - - //transaction tests - const transaction = new ClusterTransaction(); - transaction.set(source, value1); - transaction.copy(source, destination, true); - transaction.get(destination); - const results = await client.exec(transaction); - - expect(results).toEqual(["OK", true, value1]); - - client.close(); - }, - ); - - it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "flushdb flushall dbsize test_%p", - async (protocol) => { - const client = await GlideClusterClient.createClient( - getClientConfigurationOption(cluster.getAddresses(), protocol), - ); - - expect(await client.dbsize()).toBeGreaterThanOrEqual(0); - expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); - expect(await client.dbsize()).toBeGreaterThan(0); - - expect(await client.flushall()).toEqual("OK"); - expect(await client.dbsize()).toEqual(0); - - expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); - expect(await client.dbsize()).toEqual(1); - expect(await client.flushdb(FlushMode.ASYNC)).toEqual("OK"); - expect(await client.dbsize()).toEqual(0); - - expect(await client.set(uuidv4(), uuidv4())).toEqual("OK"); - expect(await client.dbsize()).toEqual(1); - expect(await client.flushdb(FlushMode.SYNC)).toEqual("OK"); - expect(await client.dbsize()).toEqual(0); - - client.close(); - }, - ); - - describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "Protocol is RESP2 = %s", - (protocol) => { - describe.each([true, false])( - "Single node route = %s", - (singleNodeRoute) => { - it( - "function load and function list", - async () => { - if (cluster.checkIfServerVersionLessThan("7.0.0")) - return; - - const client = - await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - - try { - const libName = - "mylib1C" + uuidv4().replaceAll("-", ""); - const funcName = - "myfunc1c" + uuidv4().replaceAll("-", ""); - const code = generateLuaLibCode( - libName, - new Map([[funcName, "return args[1]"]]), - true, - ); - const route: Routes = singleNodeRoute - ? { type: "primarySlotKey", key: "1" } - : "allPrimaries"; - - let functionList = await client.functionList( - { libNamePattern: libName }, - route, - ); - checkClusterResponse( - functionList as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - // load the library - expect(await client.functionLoad(code)).toEqual( - libName, - ); - - functionList = await client.functionList( - { libNamePattern: libName }, - route, - ); - let expectedDescription = new Map< - string, - string | null - >([[funcName, null]]); - let expectedFlags = new Map([ - [funcName, ["no-writes"]], - ]); - - checkClusterResponse( - functionList, - singleNodeRoute, - (value) => - checkFunctionListResponse( - value as FunctionListResponse, - libName, - expectedDescription, - expectedFlags, - ), - ); - - // call functions from that library to confirm that it works - let fcall = await client.fcallWithRoute( - funcName, - ["one", "two"], - route, - ); - checkClusterResponse( - fcall as object, - singleNodeRoute, - (value) => expect(value).toEqual("one"), - ); - fcall = await client.fcallReadonlyWithRoute( - funcName, - ["one", "two"], - route, - ); - checkClusterResponse( - fcall as object, - singleNodeRoute, - (value) => expect(value).toEqual("one"), - ); - - // re-load library without replace - await expect( - client.functionLoad(code), - ).rejects.toThrow( - `Library '${libName}' already exists`, - ); - - // re-load library with replace - expect( - await client.functionLoad(code, true), - ).toEqual(libName); - - // overwrite lib with new code - const func2Name = - "myfunc2c" + uuidv4().replaceAll("-", ""); - const newCode = generateLuaLibCode( - libName, - new Map([ - [funcName, "return args[1]"], - [func2Name, "return #args"], - ]), - true, - ); - expect( - await client.functionLoad(newCode, true), - ).toEqual(libName); - - functionList = await client.functionList( - { libNamePattern: libName, withCode: true }, - route, - ); - expectedDescription = new Map< - string, - string | null - >([ - [funcName, null], - [func2Name, null], - ]); - expectedFlags = new Map([ - [funcName, ["no-writes"]], - [func2Name, ["no-writes"]], - ]); - - checkClusterResponse( - functionList, - singleNodeRoute, - (value) => - checkFunctionListResponse( - value as FunctionListResponse, - libName, - expectedDescription, - expectedFlags, - newCode, - ), - ); - - fcall = await client.fcallWithRoute( - func2Name, - ["one", "two"], - route, - ); - checkClusterResponse( - fcall as object, - singleNodeRoute, - (value) => expect(value).toEqual(2), - ); - - fcall = await client.fcallReadonlyWithRoute( - func2Name, - ["one", "two"], - route, - ); - checkClusterResponse( - fcall as object, - singleNodeRoute, - (value) => expect(value).toEqual(2), - ); - } finally { - expect(await client.functionFlush()).toEqual( - "OK", - ); - client.close(); - } - }, - TIMEOUT, - ); - }, - ); - }, - ); - - describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "Protocol is RESP2 = %s", - (protocol) => { - describe.each([true, false])( - "Single node route = %s", - (singleNodeRoute) => { - it( - "function flush", - async () => { - if (cluster.checkIfServerVersionLessThan("7.0.0")) - return; - - const client = - await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - - try { - const libName = - "mylib1C" + uuidv4().replaceAll("-", ""); - const funcName = - "myfunc1c" + uuidv4().replaceAll("-", ""); - const code = generateLuaLibCode( - libName, - new Map([[funcName, "return args[1]"]]), - true, - ); - const route: Routes = singleNodeRoute - ? { type: "primarySlotKey", key: "1" } - : "allPrimaries"; - - const functionList1 = await client.functionList( - {}, - route, - ); - checkClusterResponse( - functionList1 as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - - // load the library - expect( - await client.functionLoad( - code, - undefined, - route, - ), - ).toEqual(libName); - - // flush functions - expect( - await client.functionFlush( - FlushMode.SYNC, - route, - ), - ).toEqual("OK"); - expect( - await client.functionFlush( - FlushMode.ASYNC, - route, - ), - ).toEqual("OK"); - - const functionList2 = - await client.functionList(); - checkClusterResponse( - functionList2 as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - - // Attempt to re-load library without overwriting to ensure FLUSH was effective - expect( - await client.functionLoad( - code, - undefined, - route, - ), - ).toEqual(libName); - } finally { - expect(await client.functionFlush()).toEqual( - "OK", - ); - client.close(); - } - }, - TIMEOUT, - ); - }, - ); - }, - ); - - describe.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( - "Protocol is RESP2 = %s", - (protocol) => { - describe.each([true, false])( - "Single node route = %s", - (singleNodeRoute) => { - it( - "function delete", - async () => { - if (cluster.checkIfServerVersionLessThan("7.0.0")) - return; - - const client = - await GlideClusterClient.createClient( - getClientConfigurationOption( - cluster.getAddresses(), - protocol, - ), - ); - - try { - const libName = - "mylib1C" + uuidv4().replaceAll("-", ""); - const funcName = - "myfunc1c" + uuidv4().replaceAll("-", ""); - const code = generateLuaLibCode( - libName, - new Map([[funcName, "return args[1]"]]), - true, - ); - const route: Routes = singleNodeRoute - ? { type: "primarySlotKey", key: "1" } - : "allPrimaries"; - let functionList = await client.functionList( - {}, - route, - ); - checkClusterResponse( - functionList as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - // load the library - expect( - await client.functionLoad( - code, - undefined, - route, - ), - ).toEqual(libName); - - // Delete the function - expect( - await client.functionDelete(libName, route), - ).toEqual("OK"); - - functionList = await client.functionList( - { libNamePattern: libName, withCode: true }, - route, - ); - checkClusterResponse( - functionList as object, - singleNodeRoute, - (value) => expect(value).toEqual([]), - ); - - // Delete a non-existing library - await expect( - client.functionDelete(libName, route), - ).rejects.toThrow(`Library not found`); - } finally { - expect(await client.functionFlush()).toEqual( - "OK", - ); - client.close(); - } - }, - TIMEOUT, - ); - }, - ); - }, - ); -});