From 0207a36bbda04f81ad172c8ee980c985be49f9ae Mon Sep 17 00:00:00 2001 From: Taylor McKinnon Date: Mon, 12 Oct 2026 09:33:48 -0700 Subject: [PATCH 1/2] impr(CLDSRV-1024): Fix mismatched min rate limit check --- lib/api/apiUtils/rateLimit/config.js | 3 +- lib/api/bucketPutRateLimit.js | 14 ++++----- .../test/bucket/putBucketRateLimit.js | 16 +++++----- tests/unit/api/bucketPutRateLimit.js | 30 +++++++++++++++++++ 4 files changed, 44 insertions(+), 19 deletions(-) diff --git a/lib/api/apiUtils/rateLimit/config.js b/lib/api/apiUtils/rateLimit/config.js index 84c4e49a35..7f96bac6b7 100644 --- a/lib/api/apiUtils/rateLimit/config.js +++ b/lib/api/apiUtils/rateLimit/config.js @@ -18,7 +18,8 @@ const { rateLimitDefaultConfigCacheTTL, rateLimitDefaultBurstCapacity } = requir * * // Optional: Number of CloudServer nodes in distributed deployment * // Default: 1 - * // Used to calculate per-worker rate limit: limit / nodes / workers + * // Used to calculate per-node rate limit: limit / nodes + * // Workers on the same node share the per-node quota * "nodes": 1, * * // Optional: Bucket-level rate limiting configuration diff --git a/lib/api/bucketPutRateLimit.js b/lib/api/bucketPutRateLimit.js index d895678775..8a0e78f256 100644 --- a/lib/api/bucketPutRateLimit.js +++ b/lib/api/bucketPutRateLimit.js @@ -24,19 +24,15 @@ function validateRateLimitConfig(requestConfig, callback) { .customizeDescription('RequestsPerSecond must be a positive integer')); } - // Validate limit against node count AND worker count + // Each node enforces limit/nodes locally; workers on a node share that quota const nodes = config.rateLimiting?.nodes || 1; - const workers = config.clusters || 1; - const minLimit = nodes * workers; - if (limit > 0 && limit < minLimit) { + if (limit > 0 && limit < nodes) { return callback(errorInstances.InvalidArgument .customizeDescription( - `RequestsPerSecond (${limit}) must be >= ` + - `(nodes x workers = ${nodes} x ${workers} = ${minLimit}) or 0 (unlimited). ` + - 'Each worker enforces limit/nodes/workers locally. ' + - `With limit less than ${minLimit}, per-worker rate would be less than 1 req/s, ` + - 'effectively blocking traffic.' + `RequestsPerSecond (${limit}) must be >= nodes (${nodes}) or 0 (unlimited). ` + + 'Each node enforces limit/nodes locally. ' + + `With limit < ${nodes}, per-node rate would be < 1 req/s, effectively blocking traffic.` )); } diff --git a/tests/functional/aws-node-sdk/test/bucket/putBucketRateLimit.js b/tests/functional/aws-node-sdk/test/bucket/putBucketRateLimit.js index 8a2cb423ac..b089310e26 100644 --- a/tests/functional/aws-node-sdk/test/bucket/putBucketRateLimit.js +++ b/tests/functional/aws-node-sdk/test/bucket/putBucketRateLimit.js @@ -112,17 +112,15 @@ skipIfRateLimitDisabled('Test put bucket rate limit', () => { } }); - describe('validation against node and worker count', () => { + describe('validation against node count', () => { - const nodes = config.rateLimiting?.nodes || 1; - const workers = config.clusters || 1; - const minLimit = nodes * workers; - const skipIfSingleNode = nodes === 1 ? it.skip : it; + const minLimit = config.rateLimiting?.nodes || 1; + const skipIfSingleNode = minLimit === 1 ? it.skip : it; // Test requires multiple nodes to pass - // With only 1 node and 1 worker the minLimit is 1. + // With only 1 node the minLimit is 1. // This leaves no invalid values to test as 0 is also a valid setting (unlimited) - skipIfSingleNode('should reject limits less than (nodes x workers)', async () => { + skipIfSingleNode('should reject limits less than nodes', async () => { let error; try { const invalidConfig = { RequestsPerSecond: minLimit - 1 }; @@ -136,7 +134,7 @@ skipIfRateLimitDisabled('Test put bucket rate limit', () => { } }); - it('should accept limits equal to (nodes x workers)', async () => { + it('should accept limits equal to nodes', async () => { try { const validConfig = { RequestsPerSecond: minLimit }; await sendRateLimitRequest('PUT', '127.0.0.1:8000', @@ -150,7 +148,7 @@ skipIfRateLimitDisabled('Test put bucket rate limit', () => { } }); - it('should accept limits greater than (nodes x workers)', async () => { + it('should accept limits greater than nodes', async () => { try { const validConfig = { RequestsPerSecond: minLimit + 1000 }; await sendRateLimitRequest('PUT', '127.0.0.1:8000', diff --git a/tests/unit/api/bucketPutRateLimit.js b/tests/unit/api/bucketPutRateLimit.js index 8baaa82225..7c5ac7840f 100644 --- a/tests/unit/api/bucketPutRateLimit.js +++ b/tests/unit/api/bucketPutRateLimit.js @@ -120,6 +120,19 @@ describe('bucketPutRateLimit API', () => { }); }); + it('should return InvalidArgument error if RequestsPerSecond is less than nodes', done => { + config.rateLimiting.nodes = 3; + bucketPut(regularAuthInfo, bucketPutReq, log, err => { + assert.ifError(err); + const putRequest = getRateLimitPutRequest(bucketName, { RequestsPerSecond: 2 }); + bucketPutRateLimit(rateLimitServiceAuthInfo, putRequest, log, err => { + assert.strictEqual(err.is.InvalidArgument, true); + assert.match(err.description, /must be >= nodes \(3\)/); + done(); + }); + }); + }); + it('should return InvalidArgument error if request body is invalid JSON', done => { bucketPut(regularAuthInfo, bucketPutReq, log, err => { assert.ifError(err); @@ -208,6 +221,23 @@ describe('bucketPutRateLimit API success cases', () => { }); }); + it('should accept RequestsPerSecond equal to nodes regardless of worker count', done => { + config.rateLimiting.nodes = 3; + sandbox.stub(config, 'clusters').value(10); + bucketPut(regularAuthInfo, bucketPutReq, log, err => { + assert.ifError(err); + const putRequest = getRateLimitPutRequest(bucketName, { RequestsPerSecond: 3 }); + bucketPutRateLimit(rateLimitServiceAuthInfo, putRequest, log, err => { + assert.ifError(err); + metadata.getBucket(bucketName, log, (err, bucket) => { + assert.ifError(err); + assert.strictEqual(bucket.getRateLimitConfiguration().getData().RequestsPerSecond.Limit, 3); + done(); + }); + }); + }); + }); + it('should allow zero as a valid RequestsPerSecond value', done => { bucketPut(regularAuthInfo, bucketPutReq, log, err => { assert.ifError(err); From 451c964cdf2f4a97ec7875d29cf054b12e5816cd Mon Sep 17 00:00:00 2001 From: Taylor McKinnon Date: Mon, 12 Oct 2026 09:38:57 -0700 Subject: [PATCH 2/2] Bump project version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e700f5dd6..8402da7932 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zenko/cloudserver", - "version": "9.2.42", + "version": "9.2.43", "description": "Zenko CloudServer, an open-source Node.js implementation of a server handling the Amazon S3 protocol", "main": "index.js", "engines": {