From d104a9c038ea321e5e18049bffefc54850d4294f Mon Sep 17 00:00:00 2001 From: Weyoss Date: Sat, 11 May 2024 13:30:28 +0200 Subject: [PATCH] refactor(queue-rate-limit): move QueueRateLimit.set() logic to LUA --- .../scripts/lua/set-queue-rate-limit.lua | 14 ++++++ src/common/redis-client/scripts/scripts.ts | 7 +++ src/lib/queue-rate-limit/errors/index.ts | 1 + .../queue-rate-limit-queue-not-found.error.ts | 12 +++++ src/lib/queue-rate-limit/queue-rate-limit.ts | 17 +++++-- tests/common/mock-module.ts | 20 ++++++++ tests/setup.ts | 11 +++- .../tests/queue-rate-limit/test00028.test.ts | 14 +++++- .../tests/queue-rate-limit/test00032.test.ts | 50 +++++++++++++++++++ 9 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 src/common/redis-client/scripts/lua/set-queue-rate-limit.lua create mode 100644 src/lib/queue-rate-limit/errors/queue-rate-limit-queue-not-found.error.ts create mode 100644 tests/common/mock-module.ts create mode 100644 tests/tests/queue-rate-limit/test00032.test.ts diff --git a/src/common/redis-client/scripts/lua/set-queue-rate-limit.lua b/src/common/redis-client/scripts/lua/set-queue-rate-limit.lua new file mode 100644 index 00000000..cdb1d455 --- /dev/null +++ b/src/common/redis-client/scripts/lua/set-queue-rate-limit.lua @@ -0,0 +1,14 @@ +local keyQueueProperties = KEYS[1] + +--- + +local EQueuePropertyRateLimit = ARGV[1] +local rateLimit = ARGV[2] + +local result = redis.call("EXISTS", keyQueueProperties) +if result == 0 then + return 'QUEUE_NOT_FOUND'; +end + +redis.call("HSET", keyQueueProperties, EQueuePropertyRateLimit, rateLimit) +return 'OK' \ No newline at end of file diff --git a/src/common/redis-client/scripts/scripts.ts b/src/common/redis-client/scripts/scripts.ts index df2eebb0..e0ac3cfa 100755 --- a/src/common/redis-client/scripts/scripts.ts +++ b/src/common/redis-client/scripts/scripts.ts @@ -25,6 +25,7 @@ export enum ELuaScriptName { FETCH_MESSAGE_FOR_PROCESSING = 'FETCH_MESSAGE_FOR_PROCESSING', DELETE_CONSUMER_GROUP = 'DELETE_CONSUMER_GROUP', CLEANUP_OFFLINE_CONSUMER = 'CLEANUP_OFFLINE_CONSUMER', + SET_QUEUE_RATE_LIMIT = 'SET_QUEUE_RATE_LIMIT', } RedisClientAbstract.addScript( @@ -103,3 +104,9 @@ RedisClientAbstract.addScript( .readFileSync(resolve(getDirname(), './lua/cleanup-offline-consumer.lua')) .toString(), ); +RedisClientAbstract.addScript( + ELuaScriptName.SET_QUEUE_RATE_LIMIT, + fs + .readFileSync(resolve(getDirname(), './lua/set-queue-rate-limit.lua')) + .toString(), +); diff --git a/src/lib/queue-rate-limit/errors/index.ts b/src/lib/queue-rate-limit/errors/index.ts index a116d7e4..a67f9bf0 100644 --- a/src/lib/queue-rate-limit/errors/index.ts +++ b/src/lib/queue-rate-limit/errors/index.ts @@ -10,3 +10,4 @@ export { QueueRateLimitError } from './queue-rate-limit.error.js'; export { QueueRateLimitInvalidIntervalError } from './queue-rate-limit-invalid-interval.error.js'; export { QueueRateLimitInvalidLimitError } from './queue-rate-limit-invalid-limit.error.js'; +export { QueueRateLimitQueueNotFoundError } from './queue-rate-limit-queue-not-found.error.js'; diff --git a/src/lib/queue-rate-limit/errors/queue-rate-limit-queue-not-found.error.ts b/src/lib/queue-rate-limit/errors/queue-rate-limit-queue-not-found.error.ts new file mode 100644 index 00000000..4629b72d --- /dev/null +++ b/src/lib/queue-rate-limit/errors/queue-rate-limit-queue-not-found.error.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) + * Weyoss + * https://github.com/weyoss + * + * This source code is licensed under the MIT license found in the LICENSE file + * in the root directory of this source tree. + */ + +import { QueueRateLimitError } from './queue-rate-limit.error.js'; + +export class QueueRateLimitQueueNotFoundError extends QueueRateLimitError {} diff --git a/src/lib/queue-rate-limit/queue-rate-limit.ts b/src/lib/queue-rate-limit/queue-rate-limit.ts index 557686f8..f5862f0d 100644 --- a/src/lib/queue-rate-limit/queue-rate-limit.ts +++ b/src/lib/queue-rate-limit/queue-rate-limit.ts @@ -14,6 +14,7 @@ import { logger, } from 'redis-smq-common'; import { RedisClientInstance } from '../../common/redis-client/redis-client-instance.js'; +import { ELuaScriptName } from '../../common/redis-client/scripts/scripts.js'; import { redisKeys } from '../../common/redis-keys/redis-keys.js'; import { Configuration } from '../../config/index.js'; import { _parseQueueParamsAndValidate } from '../queue/_/_parse-queue-params-and-validate.js'; @@ -27,6 +28,7 @@ import { _hasRateLimitExceeded } from './_/_has-rate-limit-exceeded.js'; import { QueueRateLimitInvalidLimitError, QueueRateLimitInvalidIntervalError, + QueueRateLimitQueueNotFoundError, } from './errors/index.js'; export class QueueRateLimit { @@ -91,11 +93,16 @@ export class QueueRateLimit { queueParams, null, ); - client.hset( - keyQueueProperties, - String(EQueueProperty.RATE_LIMIT), - JSON.stringify(validatedRateLimit), - (err) => cb(err), + client.runScript( + ELuaScriptName.SET_QUEUE_RATE_LIMIT, + [keyQueueProperties], + [EQueueProperty.RATE_LIMIT, JSON.stringify(validatedRateLimit)], + (err, reply) => { + if (err) cb(err); + else if (reply !== 'OK') + cb(new QueueRateLimitQueueNotFoundError()); + else cb(); + }, ); } }); diff --git a/tests/common/mock-module.ts b/tests/common/mock-module.ts new file mode 100644 index 00000000..cd3068ad --- /dev/null +++ b/tests/common/mock-module.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) + * Weyoss + * https://github.com/weyoss + * + * This source code is licensed under the MIT license found in the LICENSE file + * in the root directory of this source tree. + */ + +import { jest } from '@jest/globals'; +import { TFunction } from 'redis-smq-common'; + +export function mockModule(moduleName: string, mockFactory: TFunction) { + if (process.env['NODE_OPTIONS']?.includes('--experimental-vm-modules')) { + jest.unstable_mockModule(moduleName, mockFactory); + } else { + // Fixing TypeError: The second argument of `jest.mock` must be an inline function. + jest.mock(moduleName, (...args: unknown[]) => mockFactory(...args)); + } +} diff --git a/tests/setup.ts b/tests/setup.ts index 48fedacc..6a5c18e8 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -7,7 +7,13 @@ * in the root directory of this source tree. */ -import { beforeAll, afterAll, beforeEach, afterEach } from '@jest/globals'; +import { + beforeAll, + afterAll, + beforeEach, + afterEach, + jest, +} from '@jest/globals'; import { shutdown } from './common/shut-down.js'; import { startUp } from './common/start-up.js'; @@ -16,7 +22,10 @@ beforeAll(() => void 0); afterAll(() => void 0); beforeEach(async () => { + jest.resetAllMocks(); + jest.resetModules(); await startUp(); + jest.resetModules(); }); afterEach(async () => { diff --git a/tests/tests/queue-rate-limit/test00028.test.ts b/tests/tests/queue-rate-limit/test00028.test.ts index b74627eb..2dd753d6 100644 --- a/tests/tests/queue-rate-limit/test00028.test.ts +++ b/tests/tests/queue-rate-limit/test00028.test.ts @@ -10,11 +10,23 @@ import { test, expect } from '@jest/globals'; import { QueueRateLimitInvalidIntervalError } from '../../../src/lib/queue-rate-limit/errors/queue-rate-limit-invalid-interval.error.js'; import { QueueRateLimitInvalidLimitError } from '../../../src/lib/queue-rate-limit/errors/queue-rate-limit-invalid-limit.error.js'; -import { defaultQueue } from '../../common/message-producing-consuming.js'; +import { QueueQueueNotFoundError } from '../../../src/lib/queue/errors/queue-queue-not-found.error.js'; +import { + createQueue, + defaultQueue, +} from '../../common/message-producing-consuming.js'; import { getQueueRateLimit } from '../../common/queue-rate-limit.js'; test('SetQueueRateLimit()/GetQueueRateLimit()/ClearQueueRateLimit()', async () => { const queueRateLimit = await getQueueRateLimit(); + await expect( + queueRateLimit.setAsync(defaultQueue, { + limit: 5, + interval: 1000, + }), + ).rejects.toThrow(QueueQueueNotFoundError); + + await createQueue(defaultQueue, false); await queueRateLimit.setAsync(defaultQueue, { limit: 5, interval: 1000, diff --git a/tests/tests/queue-rate-limit/test00032.test.ts b/tests/tests/queue-rate-limit/test00032.test.ts new file mode 100644 index 00000000..b40ab117 --- /dev/null +++ b/tests/tests/queue-rate-limit/test00032.test.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) + * Weyoss + * https://github.com/weyoss + * + * This source code is licensed under the MIT license found in the LICENSE file + * in the root directory of this source tree. + */ + +import { expect, test } from '@jest/globals'; +import bluebird from 'bluebird'; +import { resolve } from 'path'; +import { getDirname, ICallback, IRedisClient } from 'redis-smq-common'; +import { IQueueParams } from '../../../src/lib/queue/types/queue.js'; +import { defaultQueue } from '../../common/message-producing-consuming.js'; +import { mockModule } from '../../common/mock-module.js'; + +const dir = getDirname(); + +test('SetQueueRateLimit(): QueueRateLimitQueueNotFoundError', async () => { + const modulePath = resolve( + dir, + '../../../src/lib/queue/_/_parse-queue-params-and-validate.js', + ); + mockModule(modulePath, () => { + return { + _parseQueueParamsAndValidate( + redisClient: IRedisClient, + queue: string | IQueueParams, + cb: ICallback, + ) { + cb(null, defaultQueue); + }, + }; + }); + const { QueueRateLimit } = await import( + '../../../src/lib/queue-rate-limit/queue-rate-limit.js' + ); + const { QueueRateLimitQueueNotFoundError } = await import( + '../../../src/lib/queue-rate-limit/errors/queue-rate-limit-queue-not-found.error.js' + ); + const queueRateLimit = bluebird.promisifyAll(new QueueRateLimit()); + await expect( + queueRateLimit.setAsync(defaultQueue, { + limit: 5, + interval: 1000, + }), + ).rejects.toThrow(QueueRateLimitQueueNotFoundError); + await queueRateLimit.shutdownAsync(); +});