From 97e510eb2679f460084aa4acdcf943c6e82f4a48 Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 19 Oct 2023 11:21:48 +0200 Subject: [PATCH 01/47] Initial commit for dynamo db store implementation --- lib/RateLimiterDynamo.js | 41 ++++++++++++++++++++++++++++++++++++++++ lib/constants.js | 1 + 2 files changed, 42 insertions(+) create mode 100644 lib/RateLimiterDynamo.js diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js new file mode 100644 index 0000000..41410e0 --- /dev/null +++ b/lib/RateLimiterDynamo.js @@ -0,0 +1,41 @@ +const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract"); + +class RateLimiterDynamo extends RateLimiterStoreAbstract { + + constructor(opts, cb = null) { + super(opts); + this.client = opts.storeClient; + + this.dbName = opts.dbName; + this.tableName = opts.tableName; + this.indexKeyPrefix = opts.indexKeyPrefix; + } + + get dbName() { + return this._dbName; + } + + set dbName(value) { + this._dbName = typeof value === 'undefined' ? 'node-rate-limiter-flexible' : value; + } + + get tableName() { + return this._tableName; + } + + set tableName(value) { + this._tableName = typeof value === 'undefined' ? this.keyPrefix : value; + } + + get tableCreated() { + return this._tableCreated + } + + set tableCreated(value) { + this._tableCreated = typeof value === 'undefined' ? false : !!value; + } + + +} + +module.exports = RateLimiterDynamo; \ No newline at end of file diff --git a/lib/constants.js b/lib/constants.js index d897c8a..2da360f 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -6,6 +6,7 @@ const LIMITER_TYPES = { REDIS: 'redis', MYSQL: 'mysql', POSTGRES: 'postgres', + DYNAMO: 'dynamo' }; const ERR_UNKNOWN_LIMITER_TYPE_MESSAGE = 'Unknown limiter type. Use one of LIMITER_TYPES constants.'; From f5991d6daac82eccfb085dabeea6244050888c1b Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 19 Oct 2023 13:04:03 +0200 Subject: [PATCH 02/47] New method for dynamodb send comand --- lib/RateLimiterDynamo.js | 91 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 82 insertions(+), 9 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 41410e0..8b8ed64 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -6,17 +6,29 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { super(opts); this.client = opts.storeClient; - this.dbName = opts.dbName; this.tableName = opts.tableName; - this.indexKeyPrefix = opts.indexKeyPrefix; - } - get dbName() { - return this._dbName; - } + this.tableCreated = false; + + this._sendCommand(this._getCreateTableCommand) + .then(() => { + this.tableCreated = true; + + // Callback invocation + if (typeof cb === 'function') { + cb(); + } + }) + .catch( err => { + //callback invocation + if (typeof cb === 'function') { + cb(err); + } else { + throw err; + } + }) + - set dbName(value) { - this._dbName = typeof value === 'undefined' ? 'node-rate-limiter-flexible' : value; } get tableName() { @@ -24,7 +36,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } set tableName(value) { - this._tableName = typeof value === 'undefined' ? this.keyPrefix : value; + this._tableName = typeof value === 'undefined' ? 'node-rate-limiter-flexible' : value; } get tableCreated() { @@ -35,6 +47,67 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { this._tableCreated = typeof value === 'undefined' ? false : !!value; } + /** + * + * @returns {Promise} + * @private + */ + _sendCommand(command) { + return new Promise((resolve, reject) => { + this._client.send(command) + .then( data => { + resolve(data); + } ) + .catch( (err) => { + reject(err); + }) + }); + } + + /** + * + * @returns {Object} + * @private + */ + _getCreateTableCommand() { + + return { + TableName: this.tableName, + KeySchema: [ + { + AttributeName: 'key', + KeyType: 'HASH' + }, + { + AttributeName: 'points', + KeyType: 'RANGE' + }, + { + AttributeName: 'expire', + KeyType: 'RANGE' + } + ], + AttributeDefinitions: [ + { + AttributeName: 'key', + AttributeType: 'S' + }, + { + AttributeName: 'points', + AttributeType: 'N' + }, + { + AttributeName: 'expire', + AttributeType: 'N' + }, + ], + ProvisionedThroughput: { + ReadCapacityUnits: 5, + WriteCapacityUnits: 5 + } + }; + } + } From f8e60fdb1fdf7a65ed713fea6f4780068634aa84 Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 19 Oct 2023 13:31:25 +0200 Subject: [PATCH 03/47] Added test file for dynamodb and dev dependency --- package.json | 1 + test/RateLimiterDynamo.test.js | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 test/RateLimiterDynamo.test.js diff --git a/package.json b/package.json index b874825..527bfdb 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "homepage": "https://github.com/animir/node-rate-limiter-flexible#readme", "types": "./lib/index.d.ts", "devDependencies": { + "@aws-sdk/client-dynamodb": "^3.431.0", "chai": "^4.1.2", "coveralls": "^3.0.1", "eslint": "^4.19.1", diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js new file mode 100644 index 0000000..30cf37d --- /dev/null +++ b/test/RateLimiterDynamo.test.js @@ -0,0 +1,9 @@ +const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const { describe, it, beforeEach } = require('mocha'); + +describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { + this.timeout(2000); + + let dynamoClient = new DynamoDBClient({region: 'eu-central-1'}); + +}) From a4f65fec636c717376ed408d442d66375584536b Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 19 Oct 2023 16:41:51 +0200 Subject: [PATCH 04/47] Continue test dynamo --- test/RateLimiterDynamo.test.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 30cf37d..3c942a5 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -1,9 +1,27 @@ const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const { expect } = require('chai'); const { describe, it, beforeEach } = require('mocha'); +const RateLimiterDynamo = require('../lib/RateLimiterDynamo'); describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { this.timeout(2000); - let dynamoClient = new DynamoDBClient({region: 'eu-central-1'}); + const dynamoClient = new DynamoDBClient({region: 'eu-central-1'}); + + it('instantiate DynamoDb client', (done) => { + expect(dynamoClient).to.not.equal(null); + done(); + }); + + it('rate limiter dynamo init', (done) => { + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient + }, + () => { + done(); + } + ); + //console.log(rateLimiter); + }); }) From 79f37ec9c1ce782da2f3eece7c51f405562aef8d Mon Sep 17 00:00:00 2001 From: daniele Date: Fri, 20 Oct 2023 12:06:40 +0200 Subject: [PATCH 05/47] Fixed create table --- lib/RateLimiterDynamo.js | 80 ++++++++++++---------------------- test/RateLimiterDynamo.test.js | 21 ++++++--- 2 files changed, 42 insertions(+), 59 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 8b8ed64..a320861 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -5,15 +5,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { constructor(opts, cb = null) { super(opts); this.client = opts.storeClient; - this.tableName = opts.tableName; - this.tableCreated = false; - - this._sendCommand(this._getCreateTableCommand) - .then(() => { + + this._createTable() + .then((data) => { this.tableCreated = true; - // Callback invocation if (typeof cb === 'function') { cb(); @@ -26,9 +23,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } else { throw err; } - }) - - + }); } get tableName() { @@ -52,60 +47,39 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @returns {Promise} * @private */ - _sendCommand(command) { - return new Promise((resolve, reject) => { - this._client.send(command) - .then( data => { - resolve(data); - } ) - .catch( (err) => { - reject(err); - }) - }); - } - - /** - * - * @returns {Object} - * @private - */ - _getCreateTableCommand() { + _createTable() { - return { + const params = { TableName: this.tableName, - KeySchema: [ - { - AttributeName: 'key', - KeyType: 'HASH' - }, - { - AttributeName: 'points', - KeyType: 'RANGE' - }, + AttributeDefinitions: [ { - AttributeName: 'expire', - KeyType: 'RANGE' + AttributeName: 'key', + AttributeType: 'S' } ], - AttributeDefinitions: [ - { - AttributeName: 'key', - AttributeType: 'S' - }, - { - AttributeName: 'points', - AttributeType: 'N' - }, + KeySchema: [ { - AttributeName: 'expire', - AttributeType: 'N' - }, + AttributeName: 'key', + KeyType: 'HASH' + } ], ProvisionedThroughput: { - ReadCapacityUnits: 5, - WriteCapacityUnits: 5 + ReadCapacityUnits: 1, + WriteCapacityUnits: 1 } }; + + return new Promise((resolve, reject) => { + + this._client.createTable(params) + .then( (data) => { + resolve(data) + }) + .catch( (err) => { + reject(err); + }); + + }); } diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 3c942a5..4a2bc59 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -1,12 +1,20 @@ -const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const AWS = require('@aws-sdk/client-dynamodb'); +const {DynamoDBClient} = require('@aws-sdk/client-dynamodb'); const { expect } = require('chai'); const { describe, it, beforeEach } = require('mocha'); const RateLimiterDynamo = require('../lib/RateLimiterDynamo'); describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { - this.timeout(2000); + this.timeout(5000); - const dynamoClient = new DynamoDBClient({region: 'eu-central-1'}); + const dynamoClient = new AWS.DynamoDB({region: 'eu-central-1'}); + /* + const client2 = new DynamoDBClient({region: 'eu-central-1'}); + console.log(dynamoClient) + console.log("V3") + console.log(client2) + */ + it('instantiate DynamoDb client', (done) => { expect(dynamoClient).to.not.equal(null); @@ -17,11 +25,12 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient }, - () => { + (data) => { + console.log(data); done(); } ); - //console.log(rateLimiter); + }); - + }) From 68a27f916032c993de5f559bd3348e07e1fa1edb Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 23 Oct 2023 16:56:32 +0200 Subject: [PATCH 06/47] Implemented _getRateLimiterRes and _get --- lib/RateLimiterDynamo.js | 69 ++++++++++++++++++++++------------ test/RateLimiterDynamo.test.js | 18 ++++++++- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index a320861..3f59b05 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -1,3 +1,4 @@ +const RateLimiterRes = require("./RateLimiterRes"); const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract"); class RateLimiterDynamo extends RateLimiterStoreAbstract { @@ -6,24 +7,31 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { super(opts); this.client = opts.storeClient; this.tableName = opts.tableName; - this.tableCreated = false; + this.tableCreated = opts.tableCreated; - this._createTable() - .then((data) => { - this.tableCreated = true; - // Callback invocation - if (typeof cb === 'function') { + if (!this.tableCreated) { + this._createTable() + .then((data) => { + this.tableCreated = true; + // Callback invocation + if (typeof cb === 'function') { + cb(); + } + }) + .catch( err => { + //callback invocation + if (typeof cb === 'function') { + cb(err); + } else { + throw err; + } + }); + + } else { + if(typeof cb === 'function') { cb(); } - }) - .catch( err => { - //callback invocation - if (typeof cb === 'function') { - cb(err); - } else { - throw err; - } - }); + } } get tableName() { @@ -69,20 +77,33 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } }; - return new Promise((resolve, reject) => { + return this._client.createTable(params); + } - this._client.createTable(params) - .then( (data) => { - resolve(data) - }) - .catch( (err) => { - reject(err); - }); + _get(rlKey) { + const params = { + TableName: this.tableName, + Key: { + key: {S: rlKey} + } + }; - }); + return this._client.getItem(params); } + _getRateLimiterRes(rlKey, changedPoints, result) { + const res = new RateLimiterRes(); + + const points = Number(result?.Item?.points?.N); + const expire = Number(result?.Item?.expire?.N); + res.isFirstInDuration = changedPoints === points; + res.consumedPoints = res.isFirstInDuration ? changedPoints : points; + res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); + res.msBeforeNext = expire ? Math.max(expire - Date.now(), 0) : -1; + + return res; + } } module.exports = RateLimiterDynamo; \ No newline at end of file diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 4a2bc59..526220e 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -26,11 +26,27 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() storeClient: dynamoClient }, (data) => { - console.log(data); done(); } ); }); + + it('get item from DynamoDB', (done) => { + const testKey = 'test'; + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient + }, + () => { + rateLimiter.get('test') + .then((response) => { + done(); + }) + .catch((err) => { + done(err); + }); + } + ); + }); }) From d106ee35c239e5756ad33f2ceee4d56672a31c20 Mon Sep 17 00:00:00 2001 From: daniele Date: Tue, 24 Oct 2023 10:19:31 +0200 Subject: [PATCH 07/47] Added _upsert method implementation --- lib/RateLimiterDynamo.js | 66 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 3f59b05..fc5b7b2 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -77,20 +77,82 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } }; + // Return a promise, no need to wrap return this._client.createTable(params); } _get(rlKey) { + + if (!this.tableCreated) { + return Promise.reject(Error('Table is not created yet')); + } + const params = { TableName: this.tableName, Key: { key: {S: rlKey} } }; - + + // Return a promise, no need to wrap return this._client.getItem(params); } + _delete(rlKey) { + + if (!this.tableCreated) { + return Promise.reject(Error('Table is not created yet')); + } + + const params = { + TableName: this.tableName, + Key: { + key: {S: rlKey} + } + } + + // Return a promise, no need to wrap + return this._client.deleteItem(params); + } + + /** + * Implemented with DynamoDB Atomic Counters. + * From the documentation: "UpdateItem calls are naturally serialized within DynamoDB, so there are no race condition concerns with making multiple simultaneous calls." + * See: https://aws.amazon.com/it/blogs/database/implement-resource-counters-with-amazon-dynamodb/ + * @param {*} rlKey + * @param {*} points + * @param {*} msDuration + * @param {*} forceExpire + * @param {*} options + * @returns + */ + _upsert(rlKey, points, msDuration, forceExpire = false, options = {}) { + + if (!this.tableCreated) { + return Promise.reject(Error('Table is not created yet')); + } + + const params = { + TableName: this.tableName, + Key: { + key: {S: rlKey} + }, + UpdateExpression: + forceExpire ? + 'SET points = :points, expire = :expire' : + 'SET points = points + :points, expire = :expire', + ExpressionAttributeValues: { + ':points': {N: Number(points)}, + ':expire': {N: Number(msDuration)} + }, + ReturnValues: 'ALL_NEW' + }; + + // Return a promise, no need to wrap + return this._client.updateItem(params); + + } + _getRateLimiterRes(rlKey, changedPoints, result) { const res = new RateLimiterRes(); @@ -104,6 +166,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { return res; } + + } module.exports = RateLimiterDynamo; \ No newline at end of file From 875f56fa34677ffa7ca5bcb96719f4b39d5ed8fd Mon Sep 17 00:00:00 2001 From: daniele Date: Tue, 24 Oct 2023 10:21:35 +0200 Subject: [PATCH 08/47] Added docstring --- lib/RateLimiterDynamo.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index fc5b7b2..d17af7e 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -50,10 +50,11 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { this._tableCreated = typeof value === 'undefined' ? false : !!value; } + /** - * - * @returns {Promise} - * @private + * Creates a table in the database. + * + * @return {Promise} A promise that resolves with the result of creating the table. */ _createTable() { @@ -81,6 +82,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { return this._client.createTable(params); } + /** + * Retrieves an item from the table. + * + * @param {string} rlKey - The key of the item to retrieve. + * @return {Promise} A promise that resolves with the retrieved item. + */ _get(rlKey) { if (!this.tableCreated) { @@ -98,6 +105,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { return this._client.getItem(params); } + /** + * Deletes an item from the table. + * + * @param {string} rlKey - the key of the item to be deleted + * @return {Promise} a promise that resolves when the item is deleted + */ _delete(rlKey) { if (!this.tableCreated) { @@ -153,6 +166,14 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } + /** + * Generate a RateLimiterRes object based on the provided parameters. + * + * @param {string} rlKey - The key for the rate limiter. + * @param {number} changedPoints - The number of points that have changed. + * @param {Object} result - The result object containing the points and expire properties. + * @returns {RateLimiterRes} - The generated RateLimiterRes object. + */ _getRateLimiterRes(rlKey, changedPoints, result) { const res = new RateLimiterRes(); From 88a964b886ddc213b9c49454d343987a09e370d5 Mon Sep 17 00:00:00 2001 From: daniele Date: Tue, 24 Oct 2023 11:09:45 +0200 Subject: [PATCH 09/47] If the table already exists do not throw exception --- lib/RateLimiterDynamo.js | 28 +++++++++++++++++++++++++++- test/RateLimiterDynamo.test.js | 16 ++++------------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index d17af7e..223884e 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -1,8 +1,21 @@ const RateLimiterRes = require("./RateLimiterRes"); const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract"); +/** + * Implementation of RateLimiterStoreAbstract using DynamoDB. + * @class RateLimiterDynamo + * @extends RateLimiterStoreAbstract + */ class RateLimiterDynamo extends RateLimiterStoreAbstract { + /** + * Constructs a new instance of the class. + * The storeClient MUST be an instance of AWS.DynamoDB NOT of AWS.DynamoDBClient. + * + * @param {Object} opts - The options for the constructor. + * @param {function} cb - The callback function (optional). + * @return {void} + */ constructor(opts, cb = null) { super(opts); this.client = opts.storeClient; @@ -79,7 +92,20 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }; // Return a promise, no need to wrap - return this._client.createTable(params); + return new Promise((resolve, reject) => { + this._client.createTable(params) + .then((data) => { + resolve(date); + }) + .catch((err) => { + // If the table already exists in the database resolve + if (err.__type.includes('ResourceInUseException')) { + resolve(); + } else { + reject(err); + } + }) + }); } /** diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 526220e..03a0890 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -1,5 +1,4 @@ -const AWS = require('@aws-sdk/client-dynamodb'); -const {DynamoDBClient} = require('@aws-sdk/client-dynamodb'); +const {DynamoDB} = require('@aws-sdk/client-dynamodb') const { expect } = require('chai'); const { describe, it, beforeEach } = require('mocha'); const RateLimiterDynamo = require('../lib/RateLimiterDynamo'); @@ -7,15 +6,8 @@ const RateLimiterDynamo = require('../lib/RateLimiterDynamo'); describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { this.timeout(5000); - const dynamoClient = new AWS.DynamoDB({region: 'eu-central-1'}); - /* - const client2 = new DynamoDBClient({region: 'eu-central-1'}); - console.log(dynamoClient) - console.log("V3") - console.log(client2) - */ - - + const dynamoClient = new DynamoDB({region: 'eu-central-1'}); + it('instantiate DynamoDb client', (done) => { expect(dynamoClient).to.not.equal(null); done(); @@ -33,7 +25,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() }); it('get item from DynamoDB', (done) => { - const testKey = 'test'; + const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient }, From 615ac3e8dbb5ea505a965ba5c597d8865dc2a015 Mon Sep 17 00:00:00 2001 From: daniele Date: Tue, 24 Oct 2023 11:56:52 +0200 Subject: [PATCH 10/47] Fix: error in _upsert and new test --- lib/RateLimiterDynamo.js | 23 +++++++++++---- test/RateLimiterDynamo.test.js | 54 ++++++++++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 223884e..6ea776d 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -128,7 +128,20 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }; // Return a promise, no need to wrap - return this._client.getItem(params); + return new Promise((resolve, reject) => { + this._client.getItem(params) + .then((data) => { + // The item exists if data.Item is not null + if (data.Item) { + resolve(data.Item); + } else { + resolve(null); + } + }) + .catch((err) => { + reject(err); + }); + }); } /** @@ -181,8 +194,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { 'SET points = :points, expire = :expire' : 'SET points = points + :points, expire = :expire', ExpressionAttributeValues: { - ':points': {N: Number(points)}, - ':expire': {N: Number(msDuration)} + ':points': {N: points.toString()}, + ':expire': {N: msDuration.toString()} }, ReturnValues: 'ALL_NEW' }; @@ -203,8 +216,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { _getRateLimiterRes(rlKey, changedPoints, result) { const res = new RateLimiterRes(); - const points = Number(result?.Item?.points?.N); - const expire = Number(result?.Item?.expire?.N); + const points = Number(result?.points?.N); + const expire = Number(result?.expire?.N); res.isFirstInDuration = changedPoints === points; res.consumedPoints = res.isFirstInDuration ? changedPoints : points; diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 03a0890..5f76f25 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -13,25 +13,41 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() done(); }); - it('rate limiter dynamo init', (done) => { + it('get item from DynamoDB', (done) => { + + const testKey = 'test'; const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient }, - (data) => { - done(); + () => { + rateLimiter.set(testKey, 999, 10000) + .then((data) => { + rateLimiter.get(testKey) + .then((response) => { + expect(response).to.not.equal(null); + done(); + }) + .catch((err) => { + done(err); + }); + }) + .catch((err) => { + done(err); + }) } ); - }); - it('get item from DynamoDB', (done) => { + it('get NOT existing item from DynamoDB', (done) => { + const testKey = 'not_existing'; const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient }, () => { - rateLimiter.get('test') + rateLimiter.get(testKey) .then((response) => { + expect(response).to.equal(null); done(); }) .catch((err) => { @@ -40,5 +56,31 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() } ); }); + + it('delete item from DynamoDB', (done) => { + + const testKey = 'test'; + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient + }, + () => { + rateLimiter.set(testKey, 999, 10000) + .then((data) => { + rateLimiter.delete(testKey) + .then((response) => { + console.log(response) + expect(response).to.not.equal(null); + done(); + }) + .catch((err) => { + done(err); + }); + }) + .catch((err) => { + done(err); + }) + } + ); + }); }) From 959ac78f9e5248684b74dd4a45da681ba56f77d7 Mon Sep 17 00:00:00 2001 From: daniele Date: Tue, 24 Oct 2023 15:21:17 +0200 Subject: [PATCH 11/47] Fixed some issue. Problem with _upsert --- lib/RateLimiterDynamo.js | 53 +++++++++++++++++++++-------- test/RateLimiterDynamo.test.js | 62 +++++++++++++++++++++++++++++++--- 2 files changed, 97 insertions(+), 18 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 6ea776d..555f925 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -63,7 +63,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { this._tableCreated = typeof value === 'undefined' ? false : !!value; } - /** * Creates a table in the database. * @@ -91,7 +90,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } }; - // Return a promise, no need to wrap return new Promise((resolve, reject) => { this._client.createTable(params) .then((data) => { @@ -127,7 +125,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } }; - // Return a promise, no need to wrap return new Promise((resolve, reject) => { this._client.getItem(params) .then((data) => { @@ -163,8 +160,19 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } } - // Return a promise, no need to wrap - return this._client.deleteItem(params); + return new Promise((resolve, reject) => { + this._client.deleteItem(params) + .then((data) => { + if(data.$metadata.httpStatusCode === 200) { + resolve(true); + } else { + resolve(false); + } + }) + .catch((err) => { + reject(err); + }); + }); } /** @@ -183,25 +191,42 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { if (!this.tableCreated) { return Promise.reject(Error('Table is not created yet')); } + + const dateNow = Date.now(); + const newExpire = msDuration > 0 ? dateNow + msDuration : null; + + const updateValue = { + points: points, + expire: newExpire + }; + + //fix this + if (!forceExpire) { + updateValue.points = + } const params = { TableName: this.tableName, Key: { key: {S: rlKey} }, - UpdateExpression: - forceExpire ? - 'SET points = :points, expire = :expire' : - 'SET points = points + :points, expire = :expire', + UpdateExpression: 'SET points = :points, expire = :expire', ExpressionAttributeValues: { - ':points': {N: points.toString()}, - ':expire': {N: msDuration.toString()} + ':points': {N: updateValue.points.toString()}, + ':expire': {N: updateValue.newExpire.toString()} }, ReturnValues: 'ALL_NEW' }; - // Return a promise, no need to wrap - return this._client.updateItem(params); + return new Promise((resolve, reject) => { + this._client.updateItem(params) + .then((data) => { + resolve(data.Attributes); + }) + .catch((err) => { + reject(err); + }); + }); } @@ -214,11 +239,11 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @returns {RateLimiterRes} - The generated RateLimiterRes object. */ _getRateLimiterRes(rlKey, changedPoints, result) { - const res = new RateLimiterRes(); const points = Number(result?.points?.N); const expire = Number(result?.expire?.N); + const res = new RateLimiterRes(); res.isFirstInDuration = changedPoints === points; res.consumedPoints = res.isFirstInDuration ? changedPoints : points; res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 5f76f25..44187f7 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -7,7 +7,8 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() this.timeout(5000); const dynamoClient = new DynamoDB({region: 'eu-central-1'}); - + + it('instantiate DynamoDb client', (done) => { expect(dynamoClient).to.not.equal(null); done(); @@ -59,7 +60,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() it('delete item from DynamoDB', (done) => { - const testKey = 'test'; + const testKey = 'delete_test'; const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient }, @@ -68,8 +69,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() .then((data) => { rateLimiter.delete(testKey) .then((response) => { - console.log(response) - expect(response).to.not.equal(null); + expect(response).to.equal(true); done(); }) .catch((err) => { @@ -82,5 +82,59 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() } ); }); + + it('delete NOT existing item from DynamoDB', (done) => { + + const testKey = 'delete_test_2'; + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient + }, + () => { + rateLimiter.set(testKey, 999, 10000) + .then((data) => { + rateLimiter.delete(testKey) + .then((response) => { + expect(response).to.equal(true); + done(); + }) + .catch((err) => { + done(err); + }); + }) + .catch((err) => { + done(err); + }) + } + ); + }); + + it('consume 1 point', (done) => { + const testKey = 'consume1'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 2, + duration: 5 + }, + () => { + rateLimiter.set(testKey, 2, 5000) + .then((data) => { + rateLimiter.consume(testKey) + .then((result) => { + console.log(result); + expect(result.consumedPoints).to.equal(1); + done(); + }) + .catch((err) => { + done(err); + }); + }) + .catch((err) => { + done(err); + }); + + }); + + }); }) From e9598203e063ecbbde0d447c4f2427af1c7ca26e Mon Sep 17 00:00:00 2001 From: daniele Date: Wed, 25 Oct 2023 12:24:41 +0200 Subject: [PATCH 12/47] Added new internal function _baseUpsert --- lib/RateLimiterDynamo.js | 111 +++++++++++++++++++++++++-------- test/RateLimiterDynamo.test.js | 3 +- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 555f925..fb780cf 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -1,6 +1,14 @@ const RateLimiterRes = require("./RateLimiterRes"); const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract"); +class DynamoItem { + constructor(rlKey, points, expire) { + this.key = rlKey; + this.points = points; + this.expire = expire; + } +} + /** * Implementation of RateLimiterStoreAbstract using DynamoDB. * @class RateLimiterDynamo @@ -106,11 +114,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }); } + /** - * Retrieves an item from the table. + * Retrieves an item from the table based on the provided key. * - * @param {string} rlKey - The key of the item to retrieve. - * @return {Promise} A promise that resolves with the retrieved item. + * @param {string} rlKey - The key to search for in the table. + * @return {Promise} A promise that resolves to the retrieved DynamoItem if it exists, or null if it does not. */ _get(rlKey) { @@ -130,7 +139,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { .then((data) => { // The item exists if data.Item is not null if (data.Item) { - resolve(data.Item); + const item = new DynamoItem( + data.Item.key.S, + Number(data.Item.points.N), + Number(data.Item.expire.N) + ); + resolve(item); } else { resolve(null); } @@ -191,29 +205,74 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { if (!this.tableCreated) { return Promise.reject(Error('Table is not created yet')); } + + return new Promise((resolve, reject) => { + + const dateNow = Date.now(); + const newExpire = msDuration > 0 ? dateNow + msDuration : null; + const newItem = new DynamoItem(rlKey, points,newExpire); + + if (forceExpire) { + this._baseUpsert(newItem) + .then(data => { + resolve(data); + }) + .catch(err => { + reject(err); + }); + } + + // DynamoDB SDK do not support conditional assignment. Need to get item first + this._get(rlKey) + .then((item) => { + + if (item.expire <= dateNow) { + newItem.points = points; + newItem.expire = newExpire; + } else { + newItem.points = item.points + points; + newItem.expire = item.expire; + } + + this._baseUpsert(newItem) + .then(data => { + resolve(data); + }) + .catch(err => { + reject(err); + }); + + }) + .catch((err) => { + reject(err); + }); + + }); - const dateNow = Date.now(); - const newExpire = msDuration > 0 ? dateNow + msDuration : null; + } - const updateValue = { - points: points, - expire: newExpire - }; + + /** + * Perform a simple upsert operation on DynamoDB using the provided item. + * + * @param {DynamoItem} item - description of parameter + * @return {DynamoItem} description of return value + */ + _baseUpsert(item) { - //fix this - if (!forceExpire) { - updateValue.points = + if (!this.tableCreated) { + return Promise.reject(Error('Table is not created yet')); } const params = { TableName: this.tableName, Key: { - key: {S: rlKey} + key: {S: item.key} }, UpdateExpression: 'SET points = :points, expire = :expire', ExpressionAttributeValues: { - ':points': {N: updateValue.points.toString()}, - ':expire': {N: updateValue.newExpire.toString()} + ':points': {N: item.points.toString()}, + ':expire': {N: item.expire.toString()} }, ReturnValues: 'ALL_NEW' }; @@ -221,13 +280,18 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { return new Promise((resolve, reject) => { this._client.updateItem(params) .then((data) => { - resolve(data.Attributes); + const item = new DynamoItem( + data.Attributes.key.S, + Number(data.Attributes.points.N), + Number(data.Attributes.expire.N) + ); + resolve(item); }) .catch((err) => { reject(err); }); }); - + } /** @@ -235,19 +299,16 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * * @param {string} rlKey - The key for the rate limiter. * @param {number} changedPoints - The number of points that have changed. - * @param {Object} result - The result object containing the points and expire properties. + * @param {DynamoItem} result - The result object of _get() method. * @returns {RateLimiterRes} - The generated RateLimiterRes object. */ _getRateLimiterRes(rlKey, changedPoints, result) { - const points = Number(result?.points?.N); - const expire = Number(result?.expire?.N); - const res = new RateLimiterRes(); - res.isFirstInDuration = changedPoints === points; - res.consumedPoints = res.isFirstInDuration ? changedPoints : points; + res.isFirstInDuration = changedPoints === result.points; + res.consumedPoints = res.isFirstInDuration ? changedPoints : result.points; res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); - res.msBeforeNext = expire ? Math.max(expire - Date.now(), 0) : -1; + res.msBeforeNext = result.expire ? Math.max(result.expire - Date.now(), 0) : -1; return res; } diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 44187f7..50f5b6f 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -117,12 +117,13 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() duration: 5 }, () => { - rateLimiter.set(testKey, 2, 5000) + rateLimiter.set(testKey, 1, 5000) .then((data) => { rateLimiter.consume(testKey) .then((result) => { console.log(result); expect(result.consumedPoints).to.equal(1); + rateLimiter.delete(testKey); done(); }) .catch((err) => { From 2b8ed35ae972c8a623c8e519d271d74239123f72 Mon Sep 17 00:00:00 2001 From: daniele Date: Wed, 25 Oct 2023 13:07:19 +0200 Subject: [PATCH 13/47] Fixed _upsert in some cases --- lib/RateLimiterDynamo.js | 47 +++++++++++++++++----------------- test/RateLimiterDynamo.test.js | 10 +------- 2 files changed, 25 insertions(+), 32 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index fb780cf..2bd3df8 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -201,7 +201,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @returns */ _upsert(rlKey, points, msDuration, forceExpire = false, options = {}) { - if (!this.tableCreated) { return Promise.reject(Error('Table is not created yet')); } @@ -220,32 +219,34 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { .catch(err => { reject(err); }); - } - - // DynamoDB SDK do not support conditional assignment. Need to get item first - this._get(rlKey) - .then((item) => { - - if (item.expire <= dateNow) { - newItem.points = points; - newItem.expire = newExpire; - } else { - newItem.points = item.points + points; - newItem.expire = item.expire; - } + } else { + // DynamoDB SDK do not support conditional assignment. Need to get item first + this._get(rlKey) + .then((item) => { + + if(item != null) { + if (item.expire <= dateNow) { + newItem.points = points; + newItem.expire = newExpire; + } else { + newItem.points = item.points + points; + newItem.expire = item.expire; + } + } - this._baseUpsert(newItem) - .then(data => { - resolve(data); + this._baseUpsert(newItem) + .then(data => { + resolve(data); + }) + .catch(err => { + reject(err); + }); + }) - .catch(err => { + .catch((err) => { reject(err); }); - - }) - .catch((err) => { - reject(err); - }); + } }); diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 50f5b6f..de97351 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -8,7 +8,6 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() const dynamoClient = new DynamoDB({region: 'eu-central-1'}); - it('instantiate DynamoDb client', (done) => { expect(dynamoClient).to.not.equal(null); done(); @@ -117,11 +116,8 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() duration: 5 }, () => { - rateLimiter.set(testKey, 1, 5000) - .then((data) => { - rateLimiter.consume(testKey) + rateLimiter.consume(testKey) .then((result) => { - console.log(result); expect(result.consumedPoints).to.equal(1); rateLimiter.delete(testKey); done(); @@ -129,10 +125,6 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() .catch((err) => { done(err); }); - }) - .catch((err) => { - done(err); - }); }); From 004571b391109bccfa4481869f1eda23ffd5a4df Mon Sep 17 00:00:00 2001 From: daniele Date: Wed, 25 Oct 2023 13:17:42 +0200 Subject: [PATCH 14/47] Minor change --- lib/RateLimiterDynamo.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 2bd3df8..fb9b1c2 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -212,6 +212,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { const newItem = new DynamoItem(rlKey, points,newExpire); if (forceExpire) { + this._baseUpsert(newItem) .then(data => { resolve(data); @@ -219,7 +220,9 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { .catch(err => { reject(err); }); + } else { + // DynamoDB SDK do not support conditional assignment. Need to get item first this._get(rlKey) .then((item) => { From 3b4bad1dba55f43e224e5a5952a22eb113d90c68 Mon Sep 17 00:00:00 2001 From: daniele Date: Wed, 25 Oct 2023 22:21:17 +0200 Subject: [PATCH 15/47] Made _upsert atomically safe --- lib/RateLimiterDynamo.js | 125 +++++++++++++++++++++++---------- test/RateLimiterDynamo.test.js | 8 +-- 2 files changed, 91 insertions(+), 42 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index fb9b1c2..8718e67 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -209,11 +209,24 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { const dateNow = Date.now(); const newExpire = msDuration > 0 ? dateNow + msDuration : null; - const newItem = new DynamoItem(rlKey, points,newExpire); if (forceExpire) { - - this._baseUpsert(newItem) + + const params = { + TableName: this.tableName, + Key: { + key: {S: rlKey} + }, + UpdateExpression: 'SET points = :points, expire = :expire', + ExpressionAttributeValues: { + ':points': {N: points.toString()}, + ':expire': {N: newExpire.toString()} + }, + ReturnValues: 'ALL_NEW' + }; + + // Perform a single upsert + this._baseUpsert(params) .then(data => { resolve(data); }) @@ -222,33 +235,82 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }); } else { - - // DynamoDB SDK do not support conditional assignment. Need to get item first - this._get(rlKey) - .then((item) => { - - if(item != null) { - if (item.expire <= dateNow) { - newItem.points = points; - newItem.expire = newExpire; - } else { - newItem.points = item.points + points; - newItem.expire = item.expire; - } - } - this._baseUpsert(newItem) + // First update, success if the item do not exists + const update1 = { + TableName: this.tableName, + Key: { + key: {S: rlKey} + }, + UpdateExpression: 'SET points = :new_points, expire = :new_expire', + ConditionExpression: "attribute_not_exists(points)", + ExpressionAttributeValues: { + ':new_points': {N: points.toString()}, + ':new_expire': {N: newExpire.toString()}, + }, + ReturnValues: 'ALL_NEW' + }; + + // Second update, ok if the item exists and expire <= now + const update2 = { + TableName: this.tableName, + Key: { + key: {S: rlKey} + }, + UpdateExpression: 'SET points = :new_points, expire = :new_expire', + ConditionExpression: "expire <= :where_expire", + ExpressionAttributeValues: { + ':new_points': {N: points.toString()}, + ':new_expire': {N: newExpire.toString()}, + ':where_expire': {N: dateNow.toString()} + }, + ReturnValues: 'ALL_NEW' + }; + + // Third update, ok if the item exists and expire > now + const update3 = { + TableName: this.tableName, + Key: { + key: {S: rlKey} + }, + UpdateExpression: 'SET points = points + :new_points', + ConditionExpression: "expire > :where_expire", + ExpressionAttributeValues: { + ':new_points': {N: points.toString()}, + ':where_expire': {N: dateNow.toString()} + }, + ReturnValues: 'ALL_NEW' + }; + + // Perform a single upsert, then a second upsert if the first one fails + this._baseUpsert(update1) + .then(data => { + // First update completed, no need to perform second update + console.log("UPDATE1 success"); + resolve(data); + }) + .catch(err => { + // First update failed, perform second update + console.log("UPDATE1 failed"); + this._baseUpsert(update2) .then(data => { + console.log("UPDATE2 success"); resolve(data); }) .catch(err => { - reject(err); + console.log("UPDATE2 failed"); + this._baseUpsert(update3) + .then(data => { + console.log("update 3 success") + resolve(data); + }) + .catch(err => { + console.log("update 3 err") + reject(err); + }); }); - - }) - .catch((err) => { - reject(err); }); + } }); @@ -262,25 +324,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @param {DynamoItem} item - description of parameter * @return {DynamoItem} description of return value */ - _baseUpsert(item) { + _baseUpsert(params) { if (!this.tableCreated) { return Promise.reject(Error('Table is not created yet')); } - - const params = { - TableName: this.tableName, - Key: { - key: {S: item.key} - }, - UpdateExpression: 'SET points = :points, expire = :expire', - ExpressionAttributeValues: { - ':points': {N: item.points.toString()}, - ':expire': {N: item.expire.toString()} - }, - ReturnValues: 'ALL_NEW' - }; - + return new Promise((resolve, reject) => { this._client.updateItem(params) .then((data) => { diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index de97351..6653fc3 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -7,7 +7,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() this.timeout(5000); const dynamoClient = new DynamoDB({region: 'eu-central-1'}); - + /* it('instantiate DynamoDb client', (done) => { expect(dynamoClient).to.not.equal(null); done(); @@ -106,20 +106,20 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() } ); }); - + */ it('consume 1 point', (done) => { const testKey = 'consume1'; const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient, points: 2, - duration: 5 + duration: 10 }, () => { rateLimiter.consume(testKey) .then((result) => { expect(result.consumedPoints).to.equal(1); - rateLimiter.delete(testKey); + //rateLimiter.delete(testKey); done(); }) .catch((err) => { From 190a52dbb5747e7fe6505769e8c63428f04aad93 Mon Sep 17 00:00:00 2001 From: daniele Date: Sun, 29 Oct 2023 19:56:52 +0100 Subject: [PATCH 16/47] better _base_upsert --- lib/RateLimiterDynamo.js | 109 +++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 63 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 8718e67..175de27 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -211,22 +211,15 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { const newExpire = msDuration > 0 ? dateNow + msDuration : null; if (forceExpire) { - - const params = { - TableName: this.tableName, - Key: { - key: {S: rlKey} - }, - UpdateExpression: 'SET points = :points, expire = :expire', - ExpressionAttributeValues: { + + // Perform a single upsert + this._baseUpsert( + rlKey, + 'SET points = :points, expire = :expire', + { ':points': {N: points.toString()}, ':expire': {N: newExpire.toString()} - }, - ReturnValues: 'ALL_NEW' - }; - - // Perform a single upsert - this._baseUpsert(params) + }) .then(data => { resolve(data); }) @@ -236,54 +229,16 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } else { - // First update, success if the item do not exists - const update1 = { - TableName: this.tableName, - Key: { - key: {S: rlKey} - }, - UpdateExpression: 'SET points = :new_points, expire = :new_expire', - ConditionExpression: "attribute_not_exists(points)", - ExpressionAttributeValues: { - ':new_points': {N: points.toString()}, - ':new_expire': {N: newExpire.toString()}, - }, - ReturnValues: 'ALL_NEW' - }; - - // Second update, ok if the item exists and expire <= now - const update2 = { - TableName: this.tableName, - Key: { - key: {S: rlKey} - }, - UpdateExpression: 'SET points = :new_points, expire = :new_expire', - ConditionExpression: "expire <= :where_expire", - ExpressionAttributeValues: { + // Perform a single upsert, then a second upsert if the first one fails then a third one + this._baseUpsert( + rlKey, + 'SET points = :new_points, expire = :new_expire', + { ':new_points': {N: points.toString()}, ':new_expire': {N: newExpire.toString()}, - ':where_expire': {N: dateNow.toString()} }, - ReturnValues: 'ALL_NEW' - }; - - // Third update, ok if the item exists and expire > now - const update3 = { - TableName: this.tableName, - Key: { - key: {S: rlKey} - }, - UpdateExpression: 'SET points = points + :new_points', - ConditionExpression: "expire > :where_expire", - ExpressionAttributeValues: { - ':new_points': {N: points.toString()}, - ':where_expire': {N: dateNow.toString()} - }, - ReturnValues: 'ALL_NEW' - }; - - // Perform a single upsert, then a second upsert if the first one fails - this._baseUpsert(update1) + 'attribute_not_exists(points)' + ) .then(data => { // First update completed, no need to perform second update console.log("UPDATE1 success"); @@ -292,14 +247,31 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { .catch(err => { // First update failed, perform second update console.log("UPDATE1 failed"); - this._baseUpsert(update2) + this._baseUpsert( + rlKey, + 'SET points = :new_points, expire = :new_expire', + { + ':new_points': {N: points.toString()}, + ':new_expire': {N: newExpire.toString()}, + ':where_expire': {N: dateNow.toString()} + }, + "expire <= :where_expire" + ) .then(data => { console.log("UPDATE2 success"); resolve(data); }) .catch(err => { console.log("UPDATE2 failed"); - this._baseUpsert(update3) + this._baseUpsert( + rlKey, + 'SET points = points + :new_points', + { + ':new_points': {N: points.toString()}, + ':where_expire': {N: dateNow.toString()} + }, + "expire > :where_expire" + ) .then(data => { console.log("update 3 success") resolve(data); @@ -316,7 +288,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }); } - /** * Perform a simple upsert operation on DynamoDB using the provided item. @@ -324,12 +295,24 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @param {DynamoItem} item - description of parameter * @return {DynamoItem} description of return value */ - _baseUpsert(params) { + _baseUpsert(rlKey, expression, values, condition) { + // _baseUpsert(params) { if (!this.tableCreated) { return Promise.reject(Error('Table is not created yet')); } + const params = { + TableName: this.tableName, + Key: { + key: {S: rlKey} + }, + UpdateExpression: expression, + ExpressionAttributeValues: values, + ConditionExpression: condition, + ReturnValues: 'ALL_NEW' + }; + return new Promise((resolve, reject) => { this._client.updateItem(params) .then((data) => { From c595b5d874db821c307f7134622a84d64e689e97 Mon Sep 17 00:00:00 2001 From: daniele Date: Sun, 29 Oct 2023 20:00:12 +0100 Subject: [PATCH 17/47] Minor changes --- lib/RateLimiterDynamo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 175de27..513522d 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -209,7 +209,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { const dateNow = Date.now(); const newExpire = msDuration > 0 ? dateNow + msDuration : null; - + console.log('hello') if (forceExpire) { // Perform a single upsert From 86d5984f3e204afc878666b961a77eb166d28852 Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 30 Oct 2023 12:18:44 +0100 Subject: [PATCH 18/47] Convert all promise in async function. Better code --- lib/RateLimiterDynamo.js | 223 ++++++++++++++------------------------- 1 file changed, 82 insertions(+), 141 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 513522d..b1ef94a 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -76,7 +76,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * * @return {Promise} A promise that resolves with the result of creating the table. */ - _createTable() { + async _createTable() { const params = { TableName: this.tableName, @@ -98,20 +98,16 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } }; - return new Promise((resolve, reject) => { - this._client.createTable(params) - .then((data) => { - resolve(date); - }) - .catch((err) => { - // If the table already exists in the database resolve - if (err.__type.includes('ResourceInUseException')) { - resolve(); - } else { - reject(err); - } - }) - }); + try { + const data = await this.client.createTable(params); + return data; + } catch(err) { + if (err.__type.includes('ResourceInUseException')) { + return null; + } else { + throw err; + } + } } @@ -121,10 +117,10 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @param {string} rlKey - The key to search for in the table. * @return {Promise} A promise that resolves to the retrieved DynamoItem if it exists, or null if it does not. */ - _get(rlKey) { + async _get(rlKey) { if (!this.tableCreated) { - return Promise.reject(Error('Table is not created yet')); + throw new Error('Table is not created yet'); } const params = { @@ -134,25 +130,16 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } }; - return new Promise((resolve, reject) => { - this._client.getItem(params) - .then((data) => { - // The item exists if data.Item is not null - if (data.Item) { - const item = new DynamoItem( - data.Item.key.S, - Number(data.Item.points.N), - Number(data.Item.expire.N) - ); - resolve(item); - } else { - resolve(null); - } - }) - .catch((err) => { - reject(err); - }); - }); + const data = await this.client.getItem(params); + if(data.Item) { + return new DynamoItem( + data.Item.key.S, + Number(data.Item.points.N), + Number(data.Item.expire.N) + ); + } else { + return null; + } } /** @@ -161,10 +148,10 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @param {string} rlKey - the key of the item to be deleted * @return {Promise} a promise that resolves when the item is deleted */ - _delete(rlKey) { + async _delete(rlKey) { if (!this.tableCreated) { - return Promise.reject(Error('Table is not created yet')); + throw new Error('Table is not created yet'); } const params = { @@ -174,23 +161,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } } - return new Promise((resolve, reject) => { - this._client.deleteItem(params) - .then((data) => { - if(data.$metadata.httpStatusCode === 200) { - resolve(true); - } else { - resolve(false); - } - }) - .catch((err) => { - reject(err); - }); - }); + const data = this._client.deleteItem(params); + return data.$metadata.httpStatusCode === 200; } /** - * Implemented with DynamoDB Atomic Counters. + * Implemented with DynamoDB Atomic Counters. 3 calls are made to DynamoDB but each call is atomic. * From the documentation: "UpdateItem calls are naturally serialized within DynamoDB, so there are no race condition concerns with making multiple simultaneous calls." * See: https://aws.amazon.com/it/blogs/database/implement-resource-counters-with-amazon-dynamodb/ * @param {*} rlKey @@ -200,93 +176,67 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @param {*} options * @returns */ - _upsert(rlKey, points, msDuration, forceExpire = false, options = {}) { + async _upsert(rlKey, points, msDuration, forceExpire = false, options = {}) { + if (!this.tableCreated) { - return Promise.reject(Error('Table is not created yet')); + throw new Error('Table is not created yet'); } - return new Promise((resolve, reject) => { + const dateNow = Date.now(); + const newExpire = msDuration > 0 ? dateNow + msDuration : null; - const dateNow = Date.now(); - const newExpire = msDuration > 0 ? dateNow + msDuration : null; - console.log('hello') - if (forceExpire) { - - // Perform a single upsert - this._baseUpsert( - rlKey, - 'SET points = :points, expire = :expire', - { - ':points': {N: points.toString()}, - ':expire': {N: newExpire.toString()} - }) - .then(data => { - resolve(data); - }) - .catch(err => { - reject(err); - }); + // Force expire, overwrite points. Create a new entry if not exists + if (forceExpire) { + return this._baseUpsert( + rlKey, + 'SET points = :points, expire = :expire', + { + ':points': {N: points.toString()}, + ':expire': {N: newExpire.toString()} + } + ); + } - } else { + // First try update, success if entry does not exists + try { + return this._baseUpsert( + rlKey, + 'SET points = :new_points, expire = :new_expire', + { + ':new_points': {N: points.toString()}, + ':new_expire': {N: newExpire.toString()}, + }, + 'attribute_not_exists(points)' + ); - // Perform a single upsert, then a second upsert if the first one fails then a third one - this._baseUpsert( + } catch (err) { + + // Second try update, success if entry exists and is not expired + try { + return this._baseUpsert( rlKey, 'SET points = :new_points, expire = :new_expire', { ':new_points': {N: points.toString()}, ':new_expire': {N: newExpire.toString()}, + ':where_expire': {N: dateNow.toString()} }, - 'attribute_not_exists(points)' + "expire <= :where_expire" ) - .then(data => { - // First update completed, no need to perform second update - console.log("UPDATE1 success"); - resolve(data); - }) - .catch(err => { - // First update failed, perform second update - console.log("UPDATE1 failed"); - this._baseUpsert( - rlKey, - 'SET points = :new_points, expire = :new_expire', - { - ':new_points': {N: points.toString()}, - ':new_expire': {N: newExpire.toString()}, - ':where_expire': {N: dateNow.toString()} - }, - "expire <= :where_expire" - ) - .then(data => { - console.log("UPDATE2 success"); - resolve(data); - }) - .catch(err => { - console.log("UPDATE2 failed"); - this._baseUpsert( - rlKey, - 'SET points = points + :new_points', - { - ':new_points': {N: points.toString()}, - ':where_expire': {N: dateNow.toString()} - }, - "expire > :where_expire" - ) - .then(data => { - console.log("update 3 success") - resolve(data); - }) - .catch(err => { - console.log("update 3 err") - reject(err); - }); - }); - }); + } catch (err) { + // Third try update, success if entry exists and is expired + return this._baseUpsert( + rlKey, + 'SET points = points + :new_points', + { + ':new_points': {N: points.toString()}, + ':where_expire': {N: dateNow.toString()} + }, + "expire > :where_expire" + ); } - - }); - + } } /** @@ -295,11 +245,11 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { * @param {DynamoItem} item - description of parameter * @return {DynamoItem} description of return value */ - _baseUpsert(rlKey, expression, values, condition) { + async _baseUpsert(rlKey, expression, values, condition) { // _baseUpsert(params) { if (!this.tableCreated) { - return Promise.reject(Error('Table is not created yet')); + throw new Error('Table is not created yet'); } const params = { @@ -313,21 +263,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { ReturnValues: 'ALL_NEW' }; - return new Promise((resolve, reject) => { - this._client.updateItem(params) - .then((data) => { - const item = new DynamoItem( - data.Attributes.key.S, - Number(data.Attributes.points.N), - Number(data.Attributes.expire.N) - ); - resolve(item); - }) - .catch((err) => { - reject(err); - }); - }); - + const data = await this.client.updateItem(params); + return new DynamoItem( + data.Attributes.key.S, + Number(data.Attributes.points.N), + Number(data.Attributes.expire.N) + ); } /** From 2ebdf31664a2230d2857895a13eb5b3b11858a6d Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 30 Oct 2023 12:43:12 +0100 Subject: [PATCH 19/47] Fixed issue with await resolve in _upsert() --- lib/RateLimiterDynamo.js | 115 ++++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 56 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index b1ef94a..a5b74f2 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -71,8 +71,9 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { this._tableCreated = typeof value === 'undefined' ? false : !!value; } + /** - * Creates a table in the database. + * Creates a table in the database. Return null if the table already exists. * * @return {Promise} A promise that resolves with the result of creating the table. */ @@ -110,12 +111,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } } - /** * Retrieves an item from the table based on the provided key. * - * @param {string} rlKey - The key to search for in the table. - * @return {Promise} A promise that resolves to the retrieved DynamoItem if it exists, or null if it does not. + * @param {string} rlKey - The key used to retrieve the item. + * @throws {Error} Throws an error if the table is not created yet. + * @return {DynamoItem|null} - The retrieved item, or null if it doesn't exist. */ async _get(rlKey) { @@ -142,11 +143,13 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } } + /** - * Deletes an item from the table. + * Deletes an item from the table based on the given rlKey. * - * @param {string} rlKey - the key of the item to be deleted - * @return {Promise} a promise that resolves when the item is deleted + * @param {string} rlKey - The rlKey of the item to delete. + * @throws {Error} Throws an error if the table is not created yet. + * @return {boolean} Returns true if the item was successfully deleted, otherwise false. */ async _delete(rlKey) { @@ -187,88 +190,89 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { // Force expire, overwrite points. Create a new entry if not exists if (forceExpire) { - return this._baseUpsert( - rlKey, - 'SET points = :points, expire = :expire', - { + return await this._baseUpsert({ + TableName: this.tableName, + Key: { key: {S: rlKey} }, + UpdateExpression: 'SET points = :points, expire = :expire', + ExpressionAttributeValues: { ':points': {N: points.toString()}, ':expire': {N: newExpire.toString()} - } - ); + }, + ReturnValues: 'ALL_NEW' + }); } // First try update, success if entry does not exists try { - return this._baseUpsert( - rlKey, - 'SET points = :new_points, expire = :new_expire', - { + return await this._baseUpsert({ + TableName: this.tableName, + Key: { key: {S: rlKey} }, + UpdateExpression: 'SET points = :new_points, expire = :new_expire', + ExpressionAttributeValues: { ':new_points': {N: points.toString()}, ':new_expire': {N: newExpire.toString()}, }, - 'attribute_not_exists(points)' - ); + ConditionExpression: 'attribute_not_exists(points)', + ReturnValues: 'ALL_NEW' + }); } catch (err) { - // Second try update, success if entry exists and is not expired try { - return this._baseUpsert( - rlKey, - 'SET points = :new_points, expire = :new_expire', - { + return await this._baseUpsert({ + TableName: this.tableName, + Key: { key: {S: rlKey} }, + UpdateExpression: 'SET points = :new_points, expire = :new_expire', + ExpressionAttributeValues: { ':new_points': {N: points.toString()}, ':new_expire': {N: newExpire.toString()}, ':where_expire': {N: dateNow.toString()} }, - "expire <= :where_expire" - ) + ConditionExpression: 'expire <= :where_expire', + ReturnValues: 'ALL_NEW' + }); } catch (err) { // Third try update, success if entry exists and is expired - return this._baseUpsert( - rlKey, - 'SET points = points + :new_points', - { + return await this._baseUpsert({ + TableName: this.tableName, + Key: { key: {S: rlKey} }, + UpdateExpression: 'SET points = points + :new_points', + ExpressionAttributeValues: { ':new_points': {N: points.toString()}, - ':where_expire': {N: dateNow.toString()} + ':where_expire': {N: dateNow.toString()} }, - "expire > :where_expire" - ); + ConditionExpression: 'expire > :where_expire', + ReturnValues: 'ALL_NEW' + }); } } } /** - * Perform a simple upsert operation on DynamoDB using the provided item. + * Asynchronously upserts data into the table. params is a DynamoDB params object. * - * @param {DynamoItem} item - description of parameter - * @return {DynamoItem} description of return value + * @param {Object} params - The parameters for the upsert operation. + * @throws {Error} Throws an error if the table is not created yet. + * @return {DynamoItem} Returns a DynamoItem object with the updated data. */ - async _baseUpsert(rlKey, expression, values, condition) { - // _baseUpsert(params) { + async _baseUpsert(params) { if (!this.tableCreated) { throw new Error('Table is not created yet'); } - const params = { - TableName: this.tableName, - Key: { - key: {S: rlKey} - }, - UpdateExpression: expression, - ExpressionAttributeValues: values, - ConditionExpression: condition, - ReturnValues: 'ALL_NEW' - }; - - const data = await this.client.updateItem(params); - return new DynamoItem( - data.Attributes.key.S, - Number(data.Attributes.points.N), - Number(data.Attributes.expire.N) - ); + try { + const data = await this.client.updateItem(params); + return new DynamoItem( + data.Attributes.key.S, + Number(data.Attributes.points.N), + Number(data.Attributes.expire.N) + ); + } catch (err) { + //console.log('_baseUpsert', params, err); + throw err; + } } /** @@ -290,7 +294,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { return res; } - } module.exports = RateLimiterDynamo; \ No newline at end of file From 0f41ae659f1bbf5a625b2f2706a6b7f21976eedd Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 30 Oct 2023 13:17:20 +0100 Subject: [PATCH 20/47] Added tests --- lib/RateLimiterDynamo.js | 13 ++- test/RateLimiterDynamo.test.js | 151 +++++++++++++++++++++++++++++++-- 2 files changed, 155 insertions(+), 9 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index a5b74f2..a606c4f 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -71,7 +71,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { this._tableCreated = typeof value === 'undefined' ? false : !!value; } - /** * Creates a table in the database. Return null if the table already exists. * @@ -164,20 +163,21 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } } - const data = this._client.deleteItem(params); + const data = await this._client.deleteItem(params); return data.$metadata.httpStatusCode === 200; } /** * Implemented with DynamoDB Atomic Counters. 3 calls are made to DynamoDB but each call is atomic. - * From the documentation: "UpdateItem calls are naturally serialized within DynamoDB, so there are no race condition concerns with making multiple simultaneous calls." + * From the documentation: "UpdateItem calls are naturally serialized within DynamoDB, + * so there are no race condition concerns with making multiple simultaneous calls." * See: https://aws.amazon.com/it/blogs/database/implement-resource-counters-with-amazon-dynamodb/ * @param {*} rlKey * @param {*} points * @param {*} msDuration * @param {*} forceExpire * @param {*} options - * @returns + * @returns */ async _upsert(rlKey, points, msDuration, forceExpire = false, options = {}) { @@ -204,6 +204,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { // First try update, success if entry does not exists try { + return await this._baseUpsert({ TableName: this.tableName, Key: { key: {S: rlKey} }, @@ -217,8 +218,10 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }); } catch (err) { + // Second try update, success if entry exists and is not expired try { + return await this._baseUpsert({ TableName: this.tableName, Key: { key: {S: rlKey} }, @@ -233,6 +236,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }); } catch (err) { + // Third try update, success if entry exists and is expired return await this._baseUpsert({ TableName: this.tableName, @@ -245,6 +249,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { ConditionExpression: 'expire > :where_expire', ReturnValues: 'ALL_NEW' }); + } } } diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 6653fc3..0ec8fdc 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -4,10 +4,10 @@ const { describe, it, beforeEach } = require('mocha'); const RateLimiterDynamo = require('../lib/RateLimiterDynamo'); describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { - this.timeout(5000); + this.timeout(10000); const dynamoClient = new DynamoDB({region: 'eu-central-1'}); - /* + it('instantiate DynamoDb client', (done) => { expect(dynamoClient).to.not.equal(null); done(); @@ -106,7 +106,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() } ); }); - */ + it('consume 1 point', (done) => { const testKey = 'consume1'; @@ -119,7 +119,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() rateLimiter.consume(testKey) .then((result) => { expect(result.consumedPoints).to.equal(1); - //rateLimiter.delete(testKey); + rateLimiter.delete(testKey); done(); }) .catch((err) => { @@ -129,5 +129,146 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() }); }); + + it('rejected when consume more than maximum points', (done) => { + const testKey = 'consumerej'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 1, + duration: 5 + }, + () => { + rateLimiter.consume(testKey, 2) + .then((result) => { + expect(result.consumedPoints).to.equal(2); + done(Error('must not resolve')); + }) + .catch((err) => { + expect(err.consumedPoints).to.equal(2); + done(); + }); + + }); + }); + + it('blocks key for block duration when consumed more than points', (done) => { + const testKey = 'block'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 1, + duration: 1, + blockDuration: 2 + }, + () => { + rateLimiter.consume(testKey, 2) + .then((result) => { + expect(result.consumedPoints).to.equal(2); + done(Error('must not resolve')); + }) + .catch((err) => { + expect(err.msBeforeNext > 1000).to.equal(true); + done(); + }); + + }); + + }); + + it('return correct data with _getRateLimiterRes', () => { + const testKey = 'test'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 5, + }, + () => { + + const res = rateLimiter._getRateLimiterRes( + 'test', + 1, + { key: 'test', points: 3, expire: Date.now() + 1000} + ); + + expect(res.msBeforeNext <= 1000 && + res.consumedPoints === 3 && + res.isFirstInDuration === false && + res.remainingPoints === 2 + ).to.equal(true); + + }); + }); + + it('get points', (done) => { + const testKey = 'get'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 5, + }, + () => { + + rateLimiter.set(testKey, 999, 10000) + .then((data) => { + rateLimiter.get(testKey) + .then((response) => { + expect(response.consumedPoints).to.equal(999); + rateLimiter.delete(testKey); + done(); + }) + .catch((err) => { + done(err); + }); + }) + .catch((err) => { + done(err); + }); + + }); + + }); + + it('get points return NULL if key is not set', (done) => { + const testKey = 'getnull'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 5, + }, + () => { + + rateLimiter.get(testKey) + .then((response) => { + expect(response).to.equal(null); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + }); + + it('delete returns false, if there is no key', (done) => { + const testKey = 'getnull3'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 5, + }, + () => { + + rateLimiter.delete(testKey) + .then((response) => { + expect(response).to.equal(false); + done(); + }) + .catch((err) => { + done(err); + }); + }); + + }); -}) +}); From 403530253323a7579e856861ae6dcd32413dd821 Mon Sep 17 00:00:00 2001 From: daniele Date: Tue, 31 Oct 2023 09:50:29 +0100 Subject: [PATCH 21/47] Fixed delete item implementation if item not exists in the table --- lib/RateLimiterDynamo.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index a606c4f..4cd1df7 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -160,11 +160,25 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { TableName: this.tableName, Key: { key: {S: rlKey} + }, + ConditionExpression: 'attribute_exists(#k)', + ExpressionAttributeNames: { + '#k': 'key' + } + } + + try { + const data = await this._client.deleteItem(params); + return data.$metadata.httpStatusCode === 200; + } catch(err) { + // ConditionalCheckFailed, item does not exist in table + if (err.__type.includes('ConditionalCheckFailedException')) { + return false; + } else { + throw err; } } - const data = await this._client.deleteItem(params); - return data.$metadata.httpStatusCode === 200; } /** From 529b59a75ec4b87e16df815d0c22b82c6044ffa4 Mon Sep 17 00:00:00 2001 From: daniele Date: Tue, 31 Oct 2023 09:54:39 +0100 Subject: [PATCH 22/47] Added more tests --- test/RateLimiterDynamo.test.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 0ec8fdc..34db60f 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -270,5 +270,38 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() }); }); + + // todo implement stub, an fake response + it('delete rejects on error', (done) => { + const testKey = 'deketeerr'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 5, + }, + () => { + + rateLimiter.delete(testKey) + .catch((err) => { + done(err); + }); + }); + + }); + + + it('does not expire key if duration set to 0', (done) => { + const testKey = 'neverexpire'; + + const rateLimiter = new RateLimiterDynamo({ + storeClient: dynamoClient, + points: 5, + }, + () => { + + + }); + + }); }); From 85384c64816222cf897c53e7199fce1c884a5ad3 Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 2 Nov 2023 13:04:57 +0100 Subject: [PATCH 23/47] FIxed issue with never expire situation --- lib/RateLimiterDynamo.js | 3 ++- test/RateLimiterDynamo.test.js | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 4cd1df7..13c95bd 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -200,7 +200,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } const dateNow = Date.now(); - const newExpire = msDuration > 0 ? dateNow + msDuration : null; + // -1 means never expire, DynamoDb do not support null values in number fields + const newExpire = msDuration > 0 ? dateNow + msDuration : -1; // Force expire, overwrite points. Create a new entry if not exists if (forceExpire) { diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 34db60f..0904c9c 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -272,6 +272,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() }); // todo implement stub, an fake response + /* it('delete rejects on error', (done) => { const testKey = 'deketeerr'; @@ -288,6 +289,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() }); }); + */ it('does not expire key if duration set to 0', (done) => { @@ -295,10 +297,29 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient, - points: 5, + points: 2, + duration: 0 }, () => { + rateLimiter.set(testKey, 2, 0) + .then(() => { + rateLimiter.consume(testKey, 1) + .then(() => { + rateLimiter.consume(testKey, 1) + .then(() => { + rateLimiter.get(testKey) + .then((res) => { + expect(res.consumedPoints).to.equal(2); + expect(res.msBeforeNext).to.equal(-1); + done(); + }); + }) + }) + }) + .catch((err) => { + done(err); + }); }); From cc569a7828979b3b6e68752d0ba1c3c4225b14da Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 2 Nov 2023 14:09:35 +0100 Subject: [PATCH 24/47] Fixed an issue in the _getRateLimiterRes implementation --- lib/RateLimiterDynamo.js | 2 +- test/RateLimiterDynamo.test.js | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 13c95bd..78e7e05 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -309,7 +309,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { res.isFirstInDuration = changedPoints === result.points; res.consumedPoints = res.isFirstInDuration ? changedPoints : result.points; res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); - res.msBeforeNext = result.expire ? Math.max(result.expire - Date.now(), 0) : -1; + res.msBeforeNext = result.expire != -1 ? Math.max(result.expire - Date.now(), 0) : -1; return res; } diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 0904c9c..120ccfb 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -305,17 +305,20 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() rateLimiter.set(testKey, 2, 0) .then(() => { rateLimiter.consume(testKey, 1) - .then(() => { - rateLimiter.consume(testKey, 1) - .then(() => { - rateLimiter.get(testKey) - .then((res) => { - expect(res.consumedPoints).to.equal(2); - expect(res.msBeforeNext).to.equal(-1); - done(); - }); - }) - }) + .then(() => { + rateLimiter.get(testKey) + .then((res) => { + expect(res.consumedPoints).to.equal(1); + expect(res.msBeforeNext).to.equal(-1); + done(); + }) + .catch((err) => { + done(err); + }) + }) + .catch((err) => { + done(err); + }) }) .catch((err) => { done(err); From 599908c01ac68511e9a0776429d44e8b58d31d11 Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 2 Nov 2023 14:23:04 +0100 Subject: [PATCH 25/47] Added comments --- test/RateLimiterDynamo.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 120ccfb..43c6bb2 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -3,6 +3,10 @@ const { expect } = require('chai'); const { describe, it, beforeEach } = require('mocha'); const RateLimiterDynamo = require('../lib/RateLimiterDynamo'); +/* + In order to perform this tests, you need to set up you aws account credentials: + see here for more info: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html +*/ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { this.timeout(10000); @@ -174,7 +178,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() }); - }); + }); it('return correct data with _getRateLimiterRes', () => { const testKey = 'test'; From 9968950e70abf72d2726afef3265d786fde43e4c Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 2 Nov 2023 15:07:33 +0100 Subject: [PATCH 26/47] Fixed test delete rejects on error --- lib/RateLimiterDynamo.js | 1 - test/RateLimiterDynamo.test.js | 22 +++++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 78e7e05..814de52 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -142,7 +142,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } } - /** * Deletes an item from the table based on the given rlKey. * diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 43c6bb2..da0d0c3 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -2,13 +2,14 @@ const {DynamoDB} = require('@aws-sdk/client-dynamodb') const { expect } = require('chai'); const { describe, it, beforeEach } = require('mocha'); const RateLimiterDynamo = require('../lib/RateLimiterDynamo'); +const sinon = require('sinon'); /* In order to perform this tests, you need to set up you aws account credentials: see here for more info: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html */ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { - this.timeout(10000); + this.timeout(5000); const dynamoClient = new DynamoDB({region: 'eu-central-1'}); @@ -275,25 +276,28 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() }); - // todo implement stub, an fake response - /* it('delete rejects on error', (done) => { - const testKey = 'deketeerr'; + const testKey = 'deleteerr'; const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient, points: 5, }, () => { - + + sinon.stub(dynamoClient, 'deleteItem').callsFake(() => { + throw new Error('stub error'); + }); + rateLimiter.delete(testKey) - .catch((err) => { - done(err); - }); + .catch(() => { + done(); + }); + + dynamoClient.deleteItem.restore(); }); }); - */ it('does not expire key if duration set to 0', (done) => { From 03d956358b959c7080891912222b660e80110f93 Mon Sep 17 00:00:00 2001 From: daniele Date: Sat, 4 Nov 2023 19:11:27 +0100 Subject: [PATCH 27/47] Converted expire field from milliseconds to seconds because of dynamoDB ttl --- lib/RateLimiterDynamo.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 814de52..63a1b0f 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -199,8 +199,10 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } const dateNow = Date.now(); - // -1 means never expire, DynamoDb do not support null values in number fields - const newExpire = msDuration > 0 ? dateNow + msDuration : -1; + /* -1 means never expire, DynamoDb do not support null values in number fields. + DynamoDb TTL use unix timestamp in seconds. + */ + const newExpireSec = msDuration > 0 ? (dateNow + msDuration) / 1000 : -1; // Force expire, overwrite points. Create a new entry if not exists if (forceExpire) { @@ -210,7 +212,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { UpdateExpression: 'SET points = :points, expire = :expire', ExpressionAttributeValues: { ':points': {N: points.toString()}, - ':expire': {N: newExpire.toString()} + ':expire': {N: newExpireSec.toString()} }, ReturnValues: 'ALL_NEW' }); @@ -225,7 +227,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { UpdateExpression: 'SET points = :new_points, expire = :new_expire', ExpressionAttributeValues: { ':new_points': {N: points.toString()}, - ':new_expire': {N: newExpire.toString()}, + ':new_expire': {N: newExpireSec.toString()}, }, ConditionExpression: 'attribute_not_exists(points)', ReturnValues: 'ALL_NEW' @@ -242,7 +244,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { UpdateExpression: 'SET points = :new_points, expire = :new_expire', ExpressionAttributeValues: { ':new_points': {N: points.toString()}, - ':new_expire': {N: newExpire.toString()}, + ':new_expire': {N: newExpireSec.toString()}, ':where_expire': {N: dateNow.toString()} }, ConditionExpression: 'expire <= :where_expire', @@ -308,7 +310,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { res.isFirstInDuration = changedPoints === result.points; res.consumedPoints = res.isFirstInDuration ? changedPoints : result.points; res.remainingPoints = Math.max(this.points - res.consumedPoints, 0); - res.msBeforeNext = result.expire != -1 ? Math.max(result.expire - Date.now(), 0) : -1; + // Expire time saved in unix time seconds not ms + res.msBeforeNext = result.expire != -1 ? Math.max(result.expire * 1000 - Date.now(), 0) : -1; return res; } From 60c9a4b8ed7ffa4f1adef8407f8554d5c7b45aeb Mon Sep 17 00:00:00 2001 From: daniele Date: Sat, 4 Nov 2023 19:56:06 +0100 Subject: [PATCH 28/47] Need to handle TTL if already set on dynamodb --- lib/RateLimiterDynamo.js | 63 +++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 63a1b0f..d00ab7f 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -34,10 +34,17 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { this._createTable() .then((data) => { this.tableCreated = true; - // Callback invocation - if (typeof cb === 'function') { - cb(); - } + + this._setTTL() + .then() + .catch() + .finally(() => { + // Callback invocation + if (typeof cb === 'function') { + cb(); + } + }); + }) .catch( err => { //callback invocation @@ -49,9 +56,16 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }); } else { - if(typeof cb === 'function') { - cb(); - } + + this._setTTL() + .then() + .catch() + .finally(() => { + // Callback invocation + if (typeof cb === 'function') { + cb(); + } + }); } } @@ -296,6 +310,41 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } } + /** + * Sets the Time-to-Live (TTL) for the table. TTL use the expire field in the table. + * See: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/howitworks-ttl.html + * + * @return {Promise} A promise that resolves when the TTL is successfully set. + * @throws {Error} Throws an error if the table is not created yet. + * @returns {Promise} + */ + async _setTTL() { + + if (!this.tableCreated) { + throw new Error('Table is not created yet'); + } + + try { + const params = { + TableName: this.tableName, + TimeToLiveSpecification: { + AttributeName: 'expire', + Enabled: true + } + } + const res = await this.client.updateTimeToLive(params); + console.log('ttl response', res) + return res; + } catch (err) { + if (err.__type === '#ValidationException') { + return true; + } else { + throw err; + } + } + + } + /** * Generate a RateLimiterRes object based on the provided parameters. * From bdfdee118db8c1a39ec2eee14b8383a357175b70 Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 6 Nov 2023 10:43:38 +0100 Subject: [PATCH 29/47] Added function for check if the ttl is enabled --- lib/RateLimiterDynamo.js | 58 +++++++++++++++++++++++++++++----------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index d00ab7f..4959800 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -36,8 +36,6 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { this.tableCreated = true; this._setTTL() - .then() - .catch() .finally(() => { // Callback invocation if (typeof cb === 'function') { @@ -58,14 +56,12 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } else { this._setTTL() - .then() - .catch() - .finally(() => { - // Callback invocation - if (typeof cb === 'function') { - cb(); - } - }); + .finally(() => { + // Callback invocation + if (typeof cb === 'function') { + cb(); + } + }); } } @@ -325,6 +321,13 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } try { + + // Check if the TTL is already set + const isTTLSet = await this._isTTLSet(); + if (isTTLSet) { + return; + } + const params = { TableName: this.tableName, TimeToLiveSpecification: { @@ -332,19 +335,42 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { Enabled: true } } + const res = await this.client.updateTimeToLive(params); - console.log('ttl response', res) return res; + } catch (err) { - if (err.__type === '#ValidationException') { - return true; - } else { - throw err; - } + throw err; } } + /** + * Checks if the Time To Live (TTL) feature is set for the DynamoDB table. + * + * @return {boolean} Returns true if the TTL feature is enabled for the table, otherwise false. + * @throws {Error} Throws an error if the table is not created yet or if there is an error while checking the TTL status. + */ + async _isTTLSet() { + + if (!this.tableCreated) { + throw new Error('Table is not created yet'); + } + + try { + + const res = await this.client.describeTimeToLive({TableName: this.tableName}); + return ( + res.$metadata.httpStatusCode == 200 + && res.TimeToLiveDescription.TimeToLiveStatus === 'ENABLED' + && res.TimeToLiveDescription.AttributeName === 'expire' + ); + + } catch (err) { + throw err; + } + } + /** * Generate a RateLimiterRes object based on the provided parameters. * From 565aea309d069f7dc0b1e08182c94b89a11a42be Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 13 Nov 2023 10:08:51 +0100 Subject: [PATCH 30/47] Added workflow dispatch --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 54a5891..0cb5fc9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,6 +8,7 @@ on: branches: [master] pull_request: branches: [master] + workflow_dispatch: concurrency: group: test-${{ github.ref }} From 41fcaba80909c1977ac18bfd3ae75b38c64f2227 Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 13 Nov 2023 10:22:18 +0100 Subject: [PATCH 31/47] Added dynamodb in test.yml --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0cb5fc9..5427d15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -41,5 +41,10 @@ jobs: uses: supercharge/redis-github-action@1.7.0 with: redis-version: ${{ matrix.redis-version }} + - name: Start DynamoDB local + uses: rrainn/dynamodb-action@v3.0.0 + with: + port: 8000 + cors: '*' - run: npm install - run: npm run test From fd54b7c94a47a2962601ca891d5c080d94b5f9a6 Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 13 Nov 2023 10:33:58 +0100 Subject: [PATCH 32/47] Dropped support for nodejs versione under 20 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5427d15..69b08e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: strategy: matrix: - node-version: [14.x, 16.x, 18.x, 20.x] + node-version: [20.x] redis-version: [6, 7] steps: From 3c3f2cc10d0e655fa0b381d49bd2f96b3459a8e2 Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 13 Nov 2023 10:46:28 +0100 Subject: [PATCH 33/47] Updated istanbul dependency --- .github/workflows/test.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69b08e8..5427d15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: strategy: matrix: - node-version: [20.x] + node-version: [14.x, 16.x, 18.x, 20.x] redis-version: [6, 7] steps: diff --git a/package.json b/package.json index 527bfdb..01276f2 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "eslint-plugin-node": "^6.0.1", "eslint-plugin-security": "^1.4.0", "ioredis": "^5.3.2", - "istanbul": "^0.4.5", + "istanbul": "^1.1.0-alpha.1", "memcached-mock": "^0.1.0", "mocha": "^10.2.0", "redis": "^4.6.8", From 365b7f6607c7e4fce5110b7f23c839fc68ee30fc Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 13 Nov 2023 11:36:48 +0100 Subject: [PATCH 34/47] Added local endpoint to dynamodb client in tests --- test/RateLimiterDynamo.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index da0d0c3..cd54b42 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -11,7 +11,7 @@ const sinon = require('sinon'); describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { this.timeout(5000); - const dynamoClient = new DynamoDB({region: 'eu-central-1'}); + const dynamoClient = new DynamoDB({region: 'eu-central-1', endpoint: 'http://localhost:8000'}); it('instantiate DynamoDb client', (done) => { expect(dynamoClient).to.not.equal(null); From 1ae448852a46cd9bcd5588cf654596a24c726820 Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 13 Nov 2023 12:01:10 +0100 Subject: [PATCH 35/47] Added endpoint to local dynamodb --- .github/workflows/test.yml | 7 ++++--- test/RateLimiterDynamo.test.js | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5427d15..faf174d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,20 +31,21 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4.0.0 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3.8.1 with: node-version: ${{ matrix.node-version }} cache: npm cache-dependency-path: ./package.json + - name: Start Redis uses: supercharge/redis-github-action@1.7.0 with: redis-version: ${{ matrix.redis-version }} + - name: Start DynamoDB local uses: rrainn/dynamodb-action@v3.0.0 - with: - port: 8000 - cors: '*' + - run: npm install - run: npm run test diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index cd54b42..d163b3e 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -1,21 +1,27 @@ const {DynamoDB} = require('@aws-sdk/client-dynamodb') const { expect } = require('chai'); -const { describe, it, beforeEach } = require('mocha'); +const { describe, it } = require('mocha'); const RateLimiterDynamo = require('../lib/RateLimiterDynamo'); const sinon = require('sinon'); /* - In order to perform this tests, you need to set up you aws account credentials: - see here for more info: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html + In order to perform this tests, you need to run a local instance of dynamodb: + docker run -p 8000:8000 amazon/dynamodb-local */ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { this.timeout(5000); - const dynamoClient = new DynamoDB({region: 'eu-central-1', endpoint: 'http://localhost:8000'}); + const dynamoClient = new DynamoDB({endpoint: 'http://localhost:8000'}); - it('instantiate DynamoDb client', (done) => { + it('DynamoDb client connection', (done) => { expect(dynamoClient).to.not.equal(null); - done(); + dynamoClient.listTables() + .then((data) => { + done(); + }) + .catch((err) => { + done(err); + }); }); it('get item from DynamoDB', (done) => { From 52e7ba154422fa5f7b0f5803bc211cbb005e7d7d Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 13 Nov 2023 12:07:14 +0100 Subject: [PATCH 36/47] Added fake credential to local dynamo client --- test/RateLimiterDynamo.test.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index d163b3e..32808e4 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -11,7 +11,14 @@ const sinon = require('sinon'); describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() { this.timeout(5000); - const dynamoClient = new DynamoDB({endpoint: 'http://localhost:8000'}); + const dynamoClient = new DynamoDB({ + region: 'eu-central-1', + credentials: { + accessKeyId: 'fake', + secretAccessKey: 'fake' + }, + endpoint: 'http://localhost:8000' + }); it('DynamoDb client connection', (done) => { expect(dynamoClient).to.not.equal(null); From 88b3727f15333377219543123de8da253fb59d33 Mon Sep 17 00:00:00 2001 From: daniele Date: Mon, 13 Nov 2023 12:36:47 +0100 Subject: [PATCH 37/47] Fixed test return correct data with _getRateLimiterRes --- test/RateLimiterDynamo.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index 32808e4..c72e5e4 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -206,7 +206,7 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() const res = rateLimiter._getRateLimiterRes( 'test', 1, - { key: 'test', points: 3, expire: Date.now() + 1000} + { key: 'test', points: 3, expire: (Date.now() + 1000) / 1000} ); expect(res.msBeforeNext <= 1000 && From 4892f142f70736138b6c571f250f930ab335a5b6 Mon Sep 17 00:00:00 2001 From: daniele Date: Wed, 15 Nov 2023 10:50:17 +0100 Subject: [PATCH 38/47] Fixed test delete not existing item from dynamodb --- test/RateLimiterDynamo.test.js | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/test/RateLimiterDynamo.test.js b/test/RateLimiterDynamo.test.js index c72e5e4..3549eec 100644 --- a/test/RateLimiterDynamo.test.js +++ b/test/RateLimiterDynamo.test.js @@ -100,27 +100,21 @@ describe('RateLimiterDynamo with fixed window', function RateLimiterDynamoTest() ); }); - it('delete NOT existing item from DynamoDB', (done) => { + it('delete NOT existing item from DynamoDB return false', (done) => { const testKey = 'delete_test_2'; const rateLimiter = new RateLimiterDynamo({ storeClient: dynamoClient }, () => { - rateLimiter.set(testKey, 999, 10000) - .then((data) => { - rateLimiter.delete(testKey) - .then((response) => { - expect(response).to.equal(true); - done(); - }) - .catch((err) => { - done(err); - }); + rateLimiter.delete(testKey) + .then((response) => { + expect(response).to.equal(false); + done(); }) .catch((err) => { done(err); - }) + }); } ); }); From d480ed642513e8e1ebea3f528cefc18ecc64e553 Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 16 Nov 2023 19:52:04 +0100 Subject: [PATCH 39/47] Added custom option for dynamodb table, read capacity unit and write capacity unit --- lib/RateLimiterDynamo.js | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 4959800..6b503ff 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -2,6 +2,12 @@ const RateLimiterRes = require("./RateLimiterRes"); const RateLimiterStoreAbstract = require("./RateLimiterStoreAbstract"); class DynamoItem { + /** + * Create a DynamoItem. + * @param {string} rlKey - The key for the rate limiter. + * @param {number} points - The number of points. + * @param {number} expire - The expiration time in seconds. + */ constructor(rlKey, points, expire) { this.key = rlKey; this.points = points; @@ -9,6 +15,24 @@ class DynamoItem { } } +class DynamoTableOpts { + + // Free tier DynamoDB provisioned mode params + static DEFAULT_READ_CAPACITY_UNITS = 25; + static DEFAULT_WRITE_CAPACITY_UNITS = 25; + + /** + * Initializes a new instance of the class. + * + * @param {number} RCU - the read capacity units + * @param {number} WCU - the write capacity units + */ + constructor(RCU, WCU) { + this.readCapacityUnits = RCU; + this.writeCapacityUnits = WCU; + } +} + /** * Implementation of RateLimiterStoreAbstract using DynamoDB. * @class RateLimiterDynamo @@ -26,12 +50,13 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { */ constructor(opts, cb = null) { super(opts); + this.client = opts.storeClient; this.tableName = opts.tableName; this.tableCreated = opts.tableCreated; if (!this.tableCreated) { - this._createTable() + this._createTable(opts.dynamoTableOpts) .then((data) => { this.tableCreated = true; @@ -83,10 +108,11 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { /** * Creates a table in the database. Return null if the table already exists. - * + * + * @param {DynamoTableOpts} dynamoTableOpts * @return {Promise} A promise that resolves with the result of creating the table. */ - async _createTable() { + async _createTable(dynamoTableOpts) { const params = { TableName: this.tableName, @@ -103,8 +129,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } ], ProvisionedThroughput: { - ReadCapacityUnits: 1, - WriteCapacityUnits: 1 + ReadCapacityUnits: dynamoTableOpts?.readCapacityUnits || DynamoTableOpts.DEFAULT_READ_CAPACITY_UNITS, + WriteCapacityUnits: dynamoTableOpts?.writeCapacityUnits || DynamoTableOpts.DEFAULT_WRITE_CAPACITY_UNITS } }; From 2445d50d8c6d433549ef1f0cd05d7b83e162200a Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 23 Nov 2023 10:09:34 +0100 Subject: [PATCH 40/47] Removed usless class DynamoTableOpts --- lib/RateLimiterDynamo.js | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 6b503ff..c50c70b 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -15,24 +15,6 @@ class DynamoItem { } } -class DynamoTableOpts { - - // Free tier DynamoDB provisioned mode params - static DEFAULT_READ_CAPACITY_UNITS = 25; - static DEFAULT_WRITE_CAPACITY_UNITS = 25; - - /** - * Initializes a new instance of the class. - * - * @param {number} RCU - the read capacity units - * @param {number} WCU - the write capacity units - */ - constructor(RCU, WCU) { - this.readCapacityUnits = RCU; - this.writeCapacityUnits = WCU; - } -} - /** * Implementation of RateLimiterStoreAbstract using DynamoDB. * @class RateLimiterDynamo @@ -40,6 +22,10 @@ class DynamoTableOpts { */ class RateLimiterDynamo extends RateLimiterStoreAbstract { + // Free tier DynamoDB provisioned mode params + static DEFAULT_READ_CAPACITY_UNITS = 25; + static DEFAULT_WRITE_CAPACITY_UNITS = 25; + /** * Constructs a new instance of the class. * The storeClient MUST be an instance of AWS.DynamoDB NOT of AWS.DynamoDBClient. @@ -109,7 +95,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { /** * Creates a table in the database. Return null if the table already exists. * - * @param {DynamoTableOpts} dynamoTableOpts + * @param {{readCapacityUnits: number, writeCapacityUnits: number}} dynamoTableOpts * @return {Promise} A promise that resolves with the result of creating the table. */ async _createTable(dynamoTableOpts) { @@ -129,8 +115,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } ], ProvisionedThroughput: { - ReadCapacityUnits: dynamoTableOpts?.readCapacityUnits || DynamoTableOpts.DEFAULT_READ_CAPACITY_UNITS, - WriteCapacityUnits: dynamoTableOpts?.writeCapacityUnits || DynamoTableOpts.DEFAULT_WRITE_CAPACITY_UNITS + ReadCapacityUnits: dynamoTableOpts?.readCapacityUnits || RateLimiterDynamo.DEFAULT_READ_CAPACITY_UNITS, + WriteCapacityUnits: dynamoTableOpts?.writeCapacityUnits || RateLimiterDynamo.DEFAULT_WRITE_CAPACITY_UNITS } }; From b481fd545821cf6698503e6073a481c78675f458 Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 23 Nov 2023 10:13:20 +0100 Subject: [PATCH 41/47] Removed ?. operator in _createTable. May not be compatible with nodejs 14.x --- lib/RateLimiterDynamo.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index c50c70b..e402a04 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -95,10 +95,10 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { /** * Creates a table in the database. Return null if the table already exists. * - * @param {{readCapacityUnits: number, writeCapacityUnits: number}} dynamoTableOpts + * @param {{readCapacityUnits: number, writeCapacityUnits: number}} tableOpts * @return {Promise} A promise that resolves with the result of creating the table. */ - async _createTable(dynamoTableOpts) { + async _createTable(tableOpts) { const params = { TableName: this.tableName, @@ -115,8 +115,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } ], ProvisionedThroughput: { - ReadCapacityUnits: dynamoTableOpts?.readCapacityUnits || RateLimiterDynamo.DEFAULT_READ_CAPACITY_UNITS, - WriteCapacityUnits: dynamoTableOpts?.writeCapacityUnits || RateLimiterDynamo.DEFAULT_WRITE_CAPACITY_UNITS + ReadCapacityUnits: tableOpts && tableOpts.readCapacityUnits ? tableOpts.readCapacityUnits : RateLimiterDynamo.DEFAULT_READ_CAPACITY_UNITS, + WriteCapacityUnits: tableOpts && tableOpts.writeCapacityUnits ? tableOpts.writeCapacityUnits : RateLimiterDynamo.DEFAULT_WRITE_CAPACITY_UNITS } }; From f4ffd731772729322936355ef2827bd977da1cde Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 23 Nov 2023 12:16:26 +0100 Subject: [PATCH 42/47] Collapsed two dynamo db update in one single update with the OR condition in ConditionExpression --- lib/RateLimiterDynamo.js | 53 ++++++++++++---------------------------- 1 file changed, 16 insertions(+), 37 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index e402a04..d599920 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -240,9 +240,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { }); } - // First try update, success if entry does not exists - try { - + try { + // First try update, success if entry NOT exists or IS expired return await this._baseUpsert({ TableName: this.tableName, Key: { key: {S: rlKey} }, @@ -250,45 +249,25 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { ExpressionAttributeValues: { ':new_points': {N: points.toString()}, ':new_expire': {N: newExpireSec.toString()}, + ':where_expire': {N: dateNow.toString()} }, - ConditionExpression: 'attribute_not_exists(points)', + ConditionExpression: 'expire <= :where_expire OR attribute_not_exists(points)', ReturnValues: 'ALL_NEW' }); } catch (err) { - - // Second try update, success if entry exists and is not expired - try { - - return await this._baseUpsert({ - TableName: this.tableName, - Key: { key: {S: rlKey} }, - UpdateExpression: 'SET points = :new_points, expire = :new_expire', - ExpressionAttributeValues: { - ':new_points': {N: points.toString()}, - ':new_expire': {N: newExpireSec.toString()}, - ':where_expire': {N: dateNow.toString()} - }, - ConditionExpression: 'expire <= :where_expire', - ReturnValues: 'ALL_NEW' - }); - - } catch (err) { - - // Third try update, success if entry exists and is expired - return await this._baseUpsert({ - TableName: this.tableName, - Key: { key: {S: rlKey} }, - UpdateExpression: 'SET points = points + :new_points', - ExpressionAttributeValues: { - ':new_points': {N: points.toString()}, - ':where_expire': {N: dateNow.toString()} - }, - ConditionExpression: 'expire > :where_expire', - ReturnValues: 'ALL_NEW' - }); - - } + // Second try update, success if entry exists and IS NOT expired + return await this._baseUpsert({ + TableName: this.tableName, + Key: { key: {S: rlKey} }, + UpdateExpression: 'SET points = points + :new_points', + ExpressionAttributeValues: { + ':new_points': {N: points.toString()}, + ':where_expire': {N: dateNow.toString()} + }, + ConditionExpression: 'expire > :where_expire', + ReturnValues: 'ALL_NEW' + }); } } From 55c523954c7d9e9a08e5f27280fcd6cc2bf3c49f Mon Sep 17 00:00:00 2001 From: daniele Date: Thu, 23 Nov 2023 12:30:20 +0100 Subject: [PATCH 43/47] Extracted static parameter from RateLimiterDynamo --- lib/RateLimiterDynamo.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index d599920..7363203 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -15,6 +15,10 @@ class DynamoItem { } } +// Free tier DynamoDB provisioned mode params +const DEFAULT_READ_CAPACITY_UNITS = 25; +const DEFAULT_WRITE_CAPACITY_UNITS = 25; + /** * Implementation of RateLimiterStoreAbstract using DynamoDB. * @class RateLimiterDynamo @@ -22,10 +26,6 @@ class DynamoItem { */ class RateLimiterDynamo extends RateLimiterStoreAbstract { - // Free tier DynamoDB provisioned mode params - static DEFAULT_READ_CAPACITY_UNITS = 25; - static DEFAULT_WRITE_CAPACITY_UNITS = 25; - /** * Constructs a new instance of the class. * The storeClient MUST be an instance of AWS.DynamoDB NOT of AWS.DynamoDBClient. @@ -115,8 +115,8 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } ], ProvisionedThroughput: { - ReadCapacityUnits: tableOpts && tableOpts.readCapacityUnits ? tableOpts.readCapacityUnits : RateLimiterDynamo.DEFAULT_READ_CAPACITY_UNITS, - WriteCapacityUnits: tableOpts && tableOpts.writeCapacityUnits ? tableOpts.writeCapacityUnits : RateLimiterDynamo.DEFAULT_WRITE_CAPACITY_UNITS + ReadCapacityUnits: tableOpts && tableOpts.readCapacityUnits ? tableOpts.readCapacityUnits : DEFAULT_READ_CAPACITY_UNITS, + WriteCapacityUnits: tableOpts && tableOpts.writeCapacityUnits ? tableOpts.writeCapacityUnits : DEFAULT_WRITE_CAPACITY_UNITS } }; From b70785051ee110136bd144100bcc0d5c9525b320 Mon Sep 17 00:00:00 2001 From: daniele Date: Fri, 24 Nov 2023 17:41:54 +0100 Subject: [PATCH 44/47] Added typescript definitions --- lib/index.d.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/index.d.ts b/lib/index.d.ts index eb59884..9dc9461 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -388,3 +388,14 @@ export class BurstyRateLimiter { options?: IRateLimiterMongoFunctionOptions ): Promise; } + +interface IRateLimiterDynamoOptions extends IRateLimiterStoreOptions { + dynamoTableOpts?: { + readCapacityUnits: number; + writeCapacityUnits: number; + } +} + +export class RateLimiterDynamo extends RateLimiterStoreAbstract { + constructor(opts: IRateLimiterDynamoOptions, cb?: ICallbackReady); +} From 1454a440f4f2408be87b57a5a92c683ea3d1eafc Mon Sep 17 00:00:00 2001 From: daniele Date: Fri, 24 Nov 2023 18:03:20 +0100 Subject: [PATCH 45/47] Added rate limiter dynamo export in index.js --- index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.js b/index.js index 1930772..3d06545 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ const RateLimiterUnion = require('./lib/RateLimiterUnion'); const RateLimiterQueue = require('./lib/RateLimiterQueue'); const BurstyRateLimiter = require('./lib/BurstyRateLimiter'); const RateLimiterRes = require('./lib/RateLimiterRes'); +const RateLimiterDynamo = require('./lib/RateLimiterDynamo'); module.exports = { RateLimiterRedis, @@ -26,4 +27,5 @@ module.exports = { RateLimiterQueue, BurstyRateLimiter, RateLimiterRes, + RateLimiterDynamo }; From 5189be822ff03d281d4ac155af80a820cb088b6f Mon Sep 17 00:00:00 2001 From: daniele Date: Fri, 24 Nov 2023 18:13:41 +0100 Subject: [PATCH 46/47] Added check in error type checking --- lib/RateLimiterDynamo.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 7363203..8d27917 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -124,7 +124,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { const data = await this.client.createTable(params); return data; } catch(err) { - if (err.__type.includes('ResourceInUseException')) { + if (err.__type && err.__type.includes('ResourceInUseException')) { return null; } else { throw err; @@ -193,7 +193,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { return data.$metadata.httpStatusCode === 200; } catch(err) { // ConditionalCheckFailed, item does not exist in table - if (err.__type.includes('ConditionalCheckFailedException')) { + if (err.__type && err.__type.includes('ConditionalCheckFailedException')) { return false; } else { throw err; From abe8fa9e7dd3f3a020040ef622411e118e14ef33 Mon Sep 17 00:00:00 2001 From: daniele Date: Fri, 24 Nov 2023 19:50:04 +0100 Subject: [PATCH 47/47] Fixed issue with wrong compare between seconds and miliseconds --- lib/RateLimiterDynamo.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/RateLimiterDynamo.js b/lib/RateLimiterDynamo.js index 8d27917..dca5cf8 100644 --- a/lib/RateLimiterDynamo.js +++ b/lib/RateLimiterDynamo.js @@ -221,6 +221,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { } const dateNow = Date.now(); + const dateNowSec = dateNow / 1000; /* -1 means never expire, DynamoDb do not support null values in number fields. DynamoDb TTL use unix timestamp in seconds. */ @@ -249,7 +250,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { ExpressionAttributeValues: { ':new_points': {N: points.toString()}, ':new_expire': {N: newExpireSec.toString()}, - ':where_expire': {N: dateNow.toString()} + ':where_expire': {N: dateNowSec.toString()} }, ConditionExpression: 'expire <= :where_expire OR attribute_not_exists(points)', ReturnValues: 'ALL_NEW' @@ -263,7 +264,7 @@ class RateLimiterDynamo extends RateLimiterStoreAbstract { UpdateExpression: 'SET points = points + :new_points', ExpressionAttributeValues: { ':new_points': {N: points.toString()}, - ':where_expire': {N: dateNow.toString()} + ':where_expire': {N: dateNowSec.toString()} }, ConditionExpression: 'expire > :where_expire', ReturnValues: 'ALL_NEW'