diff --git a/Makefile b/Makefile index 604f21f..3d9a14b 100644 --- a/Makefile +++ b/Makefile @@ -1,16 +1,16 @@ export CLUSTER_NO_TLS_VALIDATION=true test-standalone-setup: - docker-compose up -d + docker compose up -d test-standalone-teardown: - docker-compose down + docker compose down test-cluster-setup: - docker-compose -f docker-compose-cluster.yml up -d + docker compose -f docker-compose-cluster.yml up -d test-cluster-teardown: - docker-compose -f docker-compose-cluster.yml down + docker compose -f docker-compose-cluster.yml down test-cluster: npm run test-cluster diff --git a/README.md b/README.md index b24b9bd..49fb2aa 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,8 @@ The result object has: - `conformant` (boolean): true if the requested amount is conformant to the limit. - `remaining` (int): the amount of remaining tokens in the bucket. - `reset` (int / unix timestamp): unix timestamp of the date when the bucket will be full again. -- `limit` (int): the size of the bucket. +- `limit` (int): the size of the bucket. +- `delta_reset_ms` (int): the time remaining until the bucket is full again, expressed in milliseconds from the current time. ## TAKEELEVATED @@ -305,6 +306,7 @@ The result object has: - `remaining` (int): the amount of remaining tokens in the bucket. - `reset` (int / unix timestamp): unix timestamp of the date when the bucket will be full again. - `limit` (int): the size of the bucket. +- `delta_reset_ms` (int): the time remaining until the bucket is full again, expressed in milliseconds from the current time. - `elevated_limits` (object) - `triggered` (boolean): true if ERL was triggered in the current request. - `activated` (boolean): true if ERL is activated. Not necessarily triggered in this call. diff --git a/lib/db.js b/lib/db.js index dde39db..f7aaefc 100644 --- a/lib/db.js +++ b/lib/db.js @@ -223,6 +223,7 @@ class LimitDBRedis extends EventEmitter { conformant: true, remaining: bucketKeyConfig.size, reset: Math.ceil(Date.now() / 1000), + delta_reset_ms: 0, limit: bucketKeyConfig.size, delayed: false, }); @@ -283,6 +284,7 @@ class LimitDBRedis extends EventEmitter { reset: Math.ceil(reset / 1000), limit: bucketKeyConfig.size, delayed: false, + delta_reset_ms: Math.max(reset - currentMS, 0) }; if (bucketKeyConfig.skip_n_calls > 0) { this.callCounts.set(key, { res, count: 0 }); @@ -345,6 +347,7 @@ class LimitDBRedis extends EventEmitter { quota_allocated: elevated_limits.erl_quota, erl_activation_period_seconds: elevated_limits.erl_activation_period_seconds, }, + delta_reset_ms: Math.max(reset - currentMS, 0), }; if (bucketKeyConfig.skip_n_calls > 0) { this.callCounts.set(key, { res, count: 0 }); diff --git a/test/db.tests.js b/test/db.tests.js index 95f04e3..fa2702e 100644 --- a/test/db.tests.js +++ b/test/db.tests.js @@ -272,6 +272,7 @@ module.exports.tests = (clientCreator) => { assert.ok(result.conformant); assert.equal(result.remaining, 9); assert.closeTo(result.reset, now / 1000, 3); + assert.closeTo(result.delta_reset_ms, (result.limit - result.remaining) * 1000/buckets.ip.per_second, 3); assert.equal(result.limit, 10); done(); }); @@ -291,6 +292,7 @@ module.exports.tests = (clientCreator) => { assert.ok(result.conformant); assert.equal(result.remaining, 9); assert.closeTo(result.reset, now / 1000, 3); + assert.closeTo(result.delta_reset_ms, (result.limit - result.remaining) * 1000/buckets.ip.per_second, 3); assert.equal(result.limit, 10); done(); }); @@ -309,6 +311,7 @@ module.exports.tests = (clientCreator) => { assert.notOk(result.conformant); assert.equal(result.remaining, 10); assert.closeTo(result.reset, now / 1000, 3); + assert.closeTo(result.delta_reset_ms, (result.limit - result.remaining) * 1000/buckets.ip.per_second, 3); assert.equal(result.limit, 10); done(); }); @@ -431,6 +434,7 @@ module.exports.tests = (clientCreator) => { assert.ok(lastResult.conformant); assert.equal(lastResult.remaining, 1); assert.closeTo(lastResult.reset, now / 1000, 3); + assert.closeTo(lastResult.delta_reset_ms, (lastResult.limit - lastResult.remaining) * 1000/buckets.ip.per_second, 100); assert.equal(lastResult.limit, 10); done(); }); @@ -446,6 +450,7 @@ module.exports.tests = (clientCreator) => { assert.ok(result.conformant); assert.equal(result.remaining, 0); assert.closeTo(result.reset, now / 1000 + 1800, 1); + assert.closeTo(result.delta_reset_ms, (result.limit - result.remaining) * 3600000/buckets.ip.overrides['10.0.0.1'].per_hour, 1); assert.equal(result.limit, 1); done(); }); @@ -460,6 +465,7 @@ module.exports.tests = (clientCreator) => { assert.equal(response.limit, 100); assert.equal(response.remaining, 100); assert.closeTo(response.reset, now / 1000, 1); + assert.closeTo(response.delta_reset_ms, (response.limit - response.remaining) * 1000/buckets.ip.per_second, 1); done(); }); }); @@ -695,6 +701,46 @@ module.exports.tests = (clientCreator) => { }); }); }); + + describe(`${testParams.name} delta_reset_ms`, () => { + it('should reset the bucket after the specified interval', (done) => { + db.configurateBuckets({ 'test_bucket': { size: 100, per_second: 100 } }); + const params = { ...testParams.params, type: 'test_bucket', key: 'delta_key', count: 100 }; + testParams.take(params, (err, res) => { + if (err) { + done(err); + } + assert.isTrue(res.conformant); + assert.equal(res.remaining, 0); + assert.notEqual(res.delta_reset_ms, 0); + + setTimeout(() => { + params.count = 1; + testParams.take(params, (err, res) => { + if (err) { + done(err); + } + assert.isTrue(res.conformant); + assert.notEqual(res.delta_reset_ms, 0); + done(); + }); + }, res.delta_reset_ms); + }); + }); + + it('should set delta_reset_ms to 0 when bucket is unlimited', (done) => { + db.configurateBuckets({ 'test_bucket': { size: 100, unlimited: true } }); + const params = { ...testParams.params, type: 'test_bucket', key: 'delta_key', count: 100 }; + testParams.take(params, (err, res) => { + if (err) { + done(err); + } + assert.isTrue(res.conformant); + assert.equal(res.delta_reset_ms, 0); + done(); + }); + }); + }); }); }); @@ -720,6 +766,7 @@ module.exports.tests = (clientCreator) => { } const dayFromNow = Date.now() + oneDayInMs; assert.closeTo(response.reset, dayFromNow / 1000, 3); + assert.closeTo(response.delta_reset_ms, (response.limit - response.remaining) * 24*60*60*1000, 3); done(); }); }); @@ -737,6 +784,7 @@ module.exports.tests = (clientCreator) => { const dayFromNow = Date.now() + oneDayInMs; assert.closeTo(response.reset, dayFromNow / 1000, 3); + assert.closeTo(response.delta_reset_ms, (response.limit - response.remaining) * 24*60*60*1000, 3); done(); }); }); @@ -2146,6 +2194,7 @@ module.exports.tests = (clientCreator) => { if (err) return done(err); const dayFromNow = Date.now() + oneDayInMs; assert.closeTo(response.reset, dayFromNow / 1000, 3); + assert.closeTo(response.delta_reset_ms, (response.limit - response.remaining) * 24*60*60*1000, 3); done(); }); }); @@ -2165,6 +2214,7 @@ module.exports.tests = (clientCreator) => { assert.equal(response.remaining, 3); const dayFromNow = Date.now() + oneDayInMs; assert.closeTo(response.reset, dayFromNow / 1000, 3); + assert.closeTo(response.delta_reset_ms, (response.limit - response.remaining) * 24*60*60*1000, 3); done(); }); }); @@ -2297,6 +2347,7 @@ module.exports.tests = (clientCreator) => { assert.notOk(response.delayed); assert.equal(response.remaining, 9); assert.closeTo(response.reset, now / 1000, 3); + assert.closeTo(response.delta_reset_ms, (response.limit - response.remaining) * 1000/buckets.ip.per_second, 3); done(); }); });