From d82de0c9775da54cae0c1fb276d3e7fa9be3bc99 Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Sat, 20 Sep 2025 01:14:46 +0300 Subject: [PATCH 1/6] add latency histogram command, tests (##1955) --- .../lib/commands/LATENCY_HISTOGRAM.spec.ts | 37 +++++++++++++++++++ .../client/lib/commands/LATENCY_HISTOGRAM.ts | 33 +++++++++++++++++ packages/client/lib/commands/index.ts | 3 ++ 3 files changed, 73 insertions(+) create mode 100644 packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts create mode 100644 packages/client/lib/commands/LATENCY_HISTOGRAM.ts diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts new file mode 100644 index 0000000000..d729f73ff9 --- /dev/null +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts @@ -0,0 +1,37 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import LATENCY_HISTOGRAM from './LATENCY_HISTOGRAM'; +import { parseArgs } from './generic-transformers'; + +describe('LATENCY HISTOGRAM', () => { + describe('transformArguments', () => { + it('filtered by command set', () => { + assert.deepStrictEqual( + parseArgs(LATENCY_HISTOGRAM, 'set'), + ['LATENCY', 'HISTOGRAM', 'set'], + ); + }); + + it('show all', () => { + assert.deepStrictEqual( + parseArgs(LATENCY_HISTOGRAM), + ['LATENCY', 'HISTOGRAM'], + ); + }); + }); + + testUtils.testWithClient('client.latencyHistogram', async client => { + await client.set('histogram-test-key', 42); + const reply = await client.latencyHistogram('set'); + assert.ok(Array.isArray(reply)); + const command = reply[0]; + const histogram = reply[1]; + assert.strictEqual(typeof command, 'string'); + assert.ok(Array.isArray(histogram)); + const [calls, amount, histogram_usec, stats] = histogram; + assert.strictEqual(calls, 'calls'); + assert.strictEqual(typeof amount, 'number'); + assert.strictEqual(histogram_usec, 'histogram_usec'); + assert.ok(Array.isArray(stats)); + }, GLOBAL.SERVERS.OPEN); +}); \ No newline at end of file diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.ts new file mode 100644 index 0000000000..9f23327df0 --- /dev/null +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.ts @@ -0,0 +1,33 @@ +import { CommandParser } from '../client/parser'; +import { + ArrayReply, BlobStringReply, Command, NumberReply, + TuplesReply +} from '../RESP/types'; + +export default { + CACHEABLE: false, + IS_READ_ONLY: true, + /** + * Constructs the LATENCY HISTOGRAM command + * + * @param parser - The command parser + * @param commands - The list of redis commands to get histogram for + * @see https://redis.io/docs/latest/commands/latency-histogram/ + */ + parseCommand(parser: CommandParser, ...commands: string[]) { + const args = ['LATENCY', 'HISTOGRAM']; + if (commands.length !== 0) { + args.push(...commands); + } + parser.push(...args); + }, + transformReply: undefined as unknown as () => ArrayReply< + BlobStringReply | + TuplesReply<[ + BlobStringReply, + NumberReply, + BlobStringReply, + NumberReply[], + ]> + > +} as const satisfies Command; diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 05c07bbf34..4ce4c3683d 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -361,6 +361,7 @@ import VREM from './VREM'; import VSETATTR from './VSETATTR'; import VSIM from './VSIM'; import VSIM_WITHSCORES from './VSIM_WITHSCORES'; +import LATENCY_HISTOGRAM from './LATENCY_HISTOGRAM'; export default { ACL_CAT, @@ -705,6 +706,8 @@ export default { latencyGraph: LATENCY_GRAPH, LATENCY_HISTORY, latencyHistory: LATENCY_HISTORY, + LATENCY_HISTOGRAM, + latencyHistogram: LATENCY_HISTOGRAM, LATENCY_LATEST, latencyLatest: LATENCY_LATEST, LATENCY_RESET, From 446722944abff81ffec88db3ec39bcaeb465c862 Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Sat, 20 Sep 2025 01:39:48 +0300 Subject: [PATCH 2/6] adding additiona test for command --- .../lib/commands/LATENCY_HISTOGRAM.spec.ts | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts index d729f73ff9..6ce00a636e 100644 --- a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts @@ -20,18 +20,30 @@ describe('LATENCY HISTOGRAM', () => { }); }); - testUtils.testWithClient('client.latencyHistogram', async client => { - await client.set('histogram-test-key', 42); - const reply = await client.latencyHistogram('set'); - assert.ok(Array.isArray(reply)); - const command = reply[0]; - const histogram = reply[1]; - assert.strictEqual(typeof command, 'string'); - assert.ok(Array.isArray(histogram)); - const [calls, amount, histogram_usec, stats] = histogram; - assert.strictEqual(calls, 'calls'); - assert.strictEqual(typeof amount, 'number'); - assert.strictEqual(histogram_usec, 'histogram_usec'); - assert.ok(Array.isArray(stats)); - }, GLOBAL.SERVERS.OPEN); + describe('histogram information', () => { + it('unfiltered list', () => { + testUtils.testWithClient('client.latencyHistogram', async client => { + const reply = await client.latencyHistogram(); + assert.ok(Array.isArray(reply)); + }, GLOBAL.SERVERS.OPEN); + }); + + it('filtered by a command list', () => { + testUtils.testWithClient('client.latencyHistogram', async client => { + await client.configSet('latency-monitor-threshold', '100'); + await client.set('histogram-test-key', 42); + const reply = await client.latencyHistogram('set'); + assert.ok(Array.isArray(reply)); + const command = reply[0]; + const histogram = reply[1]; + assert.strictEqual(typeof command, 'string'); + assert.ok(Array.isArray(histogram)); + const [calls, amount, histogram_usec, stats] = histogram; + assert.strictEqual(calls, 'calls'); + assert.strictEqual(typeof amount, 'number'); + assert.strictEqual(histogram_usec, 'histogram_usec'); + assert.ok(Array.isArray(stats)); + }, GLOBAL.SERVERS.OPEN); + }); + }); }); \ No newline at end of file From 8ea57e2b6d14cf9973905857dc881a15cea7e5ca Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Fri, 10 Oct 2025 16:56:17 +0300 Subject: [PATCH 3/6] histogram tests update --- .../lib/commands/LATENCY_HISTOGRAM.spec.ts | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts index 6ce00a636e..ac07a4a7a0 100644 --- a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts @@ -12,7 +12,7 @@ describe('LATENCY HISTOGRAM', () => { ); }); - it('show all', () => { + it('unfiltered', () => { assert.deepStrictEqual( parseArgs(LATENCY_HISTOGRAM), ['LATENCY', 'HISTOGRAM'], @@ -20,30 +20,25 @@ describe('LATENCY HISTOGRAM', () => { }); }); - describe('histogram information', () => { - it('unfiltered list', () => { - testUtils.testWithClient('client.latencyHistogram', async client => { - const reply = await client.latencyHistogram(); - assert.ok(Array.isArray(reply)); - }, GLOBAL.SERVERS.OPEN); - }); + testUtils.testWithClient('unfiltered list', async client => { + const reply = await client.latencyHistogram(); + assert.ok(Array.isArray(reply)); + assert.strictEqual(reply.length, 0); + }, GLOBAL.SERVERS.OPEN); - it('filtered by a command list', () => { - testUtils.testWithClient('client.latencyHistogram', async client => { - await client.configSet('latency-monitor-threshold', '100'); - await client.set('histogram-test-key', 42); - const reply = await client.latencyHistogram('set'); - assert.ok(Array.isArray(reply)); - const command = reply[0]; - const histogram = reply[1]; - assert.strictEqual(typeof command, 'string'); - assert.ok(Array.isArray(histogram)); - const [calls, amount, histogram_usec, stats] = histogram; - assert.strictEqual(calls, 'calls'); - assert.strictEqual(typeof amount, 'number'); - assert.strictEqual(histogram_usec, 'histogram_usec'); - assert.ok(Array.isArray(stats)); - }, GLOBAL.SERVERS.OPEN); - }); - }); + testUtils.testWithClient('filtered by a command list', async client => { + await client.configSet('latency-monitor-threshold', '100'); + await client.set('histogram-test-key', 42); + const reply = await client.latencyHistogram('set'); + assert.ok(Array.isArray(reply)); + + assert.strictEqual(reply[0], 'set'); + + const histogram = reply[1]; + assert.ok(Array.isArray(histogram)); + assert.strictEqual(histogram[0], 'calls'); + assert.strictEqual(typeof histogram[1], 'number'); + assert.strictEqual(histogram[2], 'histogram_usec'); + assert.ok(Array.isArray(histogram[3])); + }, GLOBAL.SERVERS.OPEN); }); \ No newline at end of file From 9777c210be9c21f2c0eb2cf38108133e39c13ebf Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Fri, 10 Oct 2025 20:45:28 +0300 Subject: [PATCH 4/6] histogram test cases --- .../lib/commands/LATENCY_HISTOGRAM.spec.ts | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts index ac07a4a7a0..9d2ba44216 100644 --- a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts @@ -21,24 +21,28 @@ describe('LATENCY HISTOGRAM', () => { }); testUtils.testWithClient('unfiltered list', async client => { - const reply = await client.latencyHistogram(); - assert.ok(Array.isArray(reply)); - assert.strictEqual(reply.length, 0); + await client.configResetStat(); + await client.lPush('push-key', 'hello '); + await client.set('set-key', 'world!'); + const histogram = await client.latencyHistogram(); + const commands = ['config|resetstat', 'latency|histogram', 'set', 'lpush']; + for (const command of commands) { + assert.ok(histogram.includes(command)); + } + assert.equal(histogram.length, 8); }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('filtered by a command list', async client => { await client.configSet('latency-monitor-threshold', '100'); - await client.set('histogram-test-key', 42); + await client.set('set-key', 'hello'); const reply = await client.latencyHistogram('set'); assert.ok(Array.isArray(reply)); - - assert.strictEqual(reply[0], 'set'); - + assert.equal(reply[0], 'set'); const histogram = reply[1]; assert.ok(Array.isArray(histogram)); - assert.strictEqual(histogram[0], 'calls'); - assert.strictEqual(typeof histogram[1], 'number'); - assert.strictEqual(histogram[2], 'histogram_usec'); + assert.equal(histogram[0], 'calls'); + assert.equal(typeof histogram[1], 'number'); + assert.equal(histogram[2], 'histogram_usec'); assert.ok(Array.isArray(histogram[3])); }, GLOBAL.SERVERS.OPEN); }); \ No newline at end of file From fdc7b7d3644b0e84e154368a9ccfb81fb56fddef Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Fri, 10 Oct 2025 21:39:22 +0300 Subject: [PATCH 5/6] histogram updating contract --- .../lib/commands/LATENCY_HISTOGRAM.spec.ts | 24 ++++++-------- .../client/lib/commands/LATENCY_HISTOGRAM.ts | 33 +++++++++++-------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts index 9d2ba44216..0116a61a5c 100644 --- a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts @@ -22,27 +22,23 @@ describe('LATENCY HISTOGRAM', () => { testUtils.testWithClient('unfiltered list', async client => { await client.configResetStat(); - await client.lPush('push-key', 'hello '); - await client.set('set-key', 'world!'); + await Promise.all([ + client.lPush('push-key', 'hello '), + client.set('set-key', 'world!'), + ]); const histogram = await client.latencyHistogram(); - const commands = ['config|resetstat', 'latency|histogram', 'set', 'lpush']; + const commands = ['config|resetstat', 'set', 'lpush']; for (const command of commands) { - assert.ok(histogram.includes(command)); + assert.ok(typeof histogram[command]['calls'], 'number'); + assert.ok(Array.isArray(histogram[command]['histogram_usec'])); } - assert.equal(histogram.length, 8); }, GLOBAL.SERVERS.OPEN); testUtils.testWithClient('filtered by a command list', async client => { await client.configSet('latency-monitor-threshold', '100'); await client.set('set-key', 'hello'); - const reply = await client.latencyHistogram('set'); - assert.ok(Array.isArray(reply)); - assert.equal(reply[0], 'set'); - const histogram = reply[1]; - assert.ok(Array.isArray(histogram)); - assert.equal(histogram[0], 'calls'); - assert.equal(typeof histogram[1], 'number'); - assert.equal(histogram[2], 'histogram_usec'); - assert.ok(Array.isArray(histogram[3])); + const histogram = await client.latencyHistogram('set'); + assert.ok(typeof histogram.set['calls'], 'number'); + assert.ok(Array.isArray(histogram.set['histogram_usec'])); }, GLOBAL.SERVERS.OPEN); }); \ No newline at end of file diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.ts index 9f23327df0..e2722c3819 100644 --- a/packages/client/lib/commands/LATENCY_HISTOGRAM.ts +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.ts @@ -1,8 +1,12 @@ import { CommandParser } from '../client/parser'; -import { - ArrayReply, BlobStringReply, Command, NumberReply, - TuplesReply -} from '../RESP/types'; +import { Command } from '../RESP/types'; + +type RawHistogram = [string, number, string, number[]]; + +type Histogram = Record; export default { CACHEABLE: false, @@ -21,13 +25,16 @@ export default { } parser.push(...args); }, - transformReply: undefined as unknown as () => ArrayReply< - BlobStringReply | - TuplesReply<[ - BlobStringReply, - NumberReply, - BlobStringReply, - NumberReply[], - ]> - > + transformReply(reply: (string | RawHistogram)[]): Histogram { + const result: Histogram = {}; + if (reply.length === 0) return result; + for (let i = 1; i < reply.length; i += 2) { + const histogram = reply[i] as RawHistogram; + result[reply[i - 1] as string] = { + calls: histogram[1], + histogram_usec: histogram[3], + }; + } + return result; + } } as const satisfies Command; From 3993f0343be56377efd6038eb96a8c9cc1d69e27 Mon Sep 17 00:00:00 2001 From: Trofymenko Vladyslav Date: Fri, 10 Oct 2025 21:39:22 +0300 Subject: [PATCH 6/6] histogram updating contract --- .../lib/commands/LATENCY_HISTOGRAM.spec.ts | 133 ++++++++++++------ .../client/lib/commands/LATENCY_HISTOGRAM.ts | 40 ++++-- 2 files changed, 116 insertions(+), 57 deletions(-) diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts index 9d2ba44216..34125c2650 100644 --- a/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.spec.ts @@ -1,48 +1,95 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import LATENCY_HISTOGRAM from './LATENCY_HISTOGRAM'; -import { parseArgs } from './generic-transformers'; - -describe('LATENCY HISTOGRAM', () => { - describe('transformArguments', () => { - it('filtered by command set', () => { - assert.deepStrictEqual( - parseArgs(LATENCY_HISTOGRAM, 'set'), - ['LATENCY', 'HISTOGRAM', 'set'], - ); +import { strict as assert } from "assert"; +import testUtils, { GLOBAL } from "../test-utils"; +import LATENCY_HISTOGRAM from "./LATENCY_HISTOGRAM"; +import { parseArgs } from "./generic-transformers"; + +describe("LATENCY HISTOGRAM", () => { + describe("transformArguments", () => { + it("filtered by command set", () => { + assert.deepStrictEqual(parseArgs(LATENCY_HISTOGRAM, "set"), [ + "LATENCY", + "HISTOGRAM", + "set", + ]); }); - it('unfiltered', () => { - assert.deepStrictEqual( - parseArgs(LATENCY_HISTOGRAM), - ['LATENCY', 'HISTOGRAM'], - ); + it("unfiltered", () => { + assert.deepStrictEqual(parseArgs(LATENCY_HISTOGRAM), [ + "LATENCY", + "HISTOGRAM", + ]); }); }); - testUtils.testWithClient('unfiltered list', async client => { - await client.configResetStat(); - await client.lPush('push-key', 'hello '); - await client.set('set-key', 'world!'); - const histogram = await client.latencyHistogram(); - const commands = ['config|resetstat', 'latency|histogram', 'set', 'lpush']; - for (const command of commands) { - assert.ok(histogram.includes(command)); - } - assert.equal(histogram.length, 8); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('filtered by a command list', async client => { - await client.configSet('latency-monitor-threshold', '100'); - await client.set('set-key', 'hello'); - const reply = await client.latencyHistogram('set'); - assert.ok(Array.isArray(reply)); - assert.equal(reply[0], 'set'); - const histogram = reply[1]; - assert.ok(Array.isArray(histogram)); - assert.equal(histogram[0], 'calls'); - assert.equal(typeof histogram[1], 'number'); - assert.equal(histogram[2], 'histogram_usec'); - assert.ok(Array.isArray(histogram[3])); - }, GLOBAL.SERVERS.OPEN); -}); \ No newline at end of file + describe("RESP 2", () => { + testUtils.testWithClient( + "unfiltered list", + async (client) => { + await client.configResetStat(); + await Promise.all([ + client.lPush("push-key", "hello "), + client.set("set-key", "world!"), + ]); + const histogram = await client.latencyHistogram(); + const commands = ["config|resetstat", "set", "lpush"]; + for (const command of commands) { + assert.ok(typeof histogram[command]["calls"], "number"); + } + }, + GLOBAL.SERVERS.OPEN, + ); + + testUtils.testWithClient( + "filtered by a command list", + async (client) => { + await client.configSet("latency-monitor-threshold", "100"); + await client.set("set-key", "hello"); + const histogram = await client.latencyHistogram("set"); + + assert.ok(typeof histogram.set["calls"], "number"); + }, + GLOBAL.SERVERS.OPEN, + ); + }); + + describe("RESP 3", () => { + testUtils.testWithClient( + "unfiltered list", + async (client) => { + await client.configResetStat(); + await Promise.all([ + client.lPush("push-key", "hello "), + client.set("set-key", "world!"), + ]); + const histogram = await client.latencyHistogram(); + const commands = ["config|resetstat", "set", "lpush"]; + for (const command of commands) { + assert.ok(typeof histogram[command]["calls"], "number"); + } + }, + { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + }, + }, + ); + + testUtils.testWithClient( + "filtered by a command list", + async (client) => { + await client.configSet("latency-monitor-threshold", "100"); + await client.set("set-key", "hello"); + const histogram = await client.latencyHistogram("set"); + + assert.ok(typeof histogram.set["calls"], "number"); + }, + { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + RESP: 3, + }, + }, + ); + }); +}); diff --git a/packages/client/lib/commands/LATENCY_HISTOGRAM.ts b/packages/client/lib/commands/LATENCY_HISTOGRAM.ts index 9f23327df0..5004263da2 100644 --- a/packages/client/lib/commands/LATENCY_HISTOGRAM.ts +++ b/packages/client/lib/commands/LATENCY_HISTOGRAM.ts @@ -1,15 +1,20 @@ import { CommandParser } from '../client/parser'; -import { - ArrayReply, BlobStringReply, Command, NumberReply, - TuplesReply -} from '../RESP/types'; +import { Command } from '../RESP/types'; +import { transformTuplesToMap } from './generic-transformers'; + +type RawHistogram = [string, number, string, number[]]; + +type Histogram = Record; +}>; export default { CACHEABLE: false, IS_READ_ONLY: true, /** * Constructs the LATENCY HISTOGRAM command - * + * * @param parser - The command parser * @param commands - The list of redis commands to get histogram for * @see https://redis.io/docs/latest/commands/latency-histogram/ @@ -21,13 +26,20 @@ export default { } parser.push(...args); }, - transformReply: undefined as unknown as () => ArrayReply< - BlobStringReply | - TuplesReply<[ - BlobStringReply, - NumberReply, - BlobStringReply, - NumberReply[], - ]> - > + transformReply: { + 2: (reply: (string | RawHistogram)[]): Histogram => { + const result: Histogram = {}; + if (reply.length === 0) return result; + for (let i = 1; i < reply.length; i += 2) { + const histogram = reply[i] as RawHistogram; + const usec = transformTuplesToMap(histogram[3], n => n); + result[reply[i - 1] as string] = { + calls: histogram[1], + histogram_usec: usec, + }; + } + return result; + }, + 3: undefined as unknown as () => Histogram + } } as const satisfies Command;