Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/api/apiUtils/rateLimit/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 5 additions & 9 deletions lib/api/bucketPutRateLimit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 less than ${nodes}, per-node rate would be less than 1 req/s, effectively blocking traffic.`
));
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
16 changes: 7 additions & 9 deletions tests/functional/aws-node-sdk/test/bucket/putBucketRateLimit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 number of nodes', async () => {
let error;
try {
const invalidConfig = { RequestsPerSecond: minLimit - 1 };
Expand All @@ -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 number of nodes', async () => {
try {
const validConfig = { RequestsPerSecond: minLimit };
await sendRateLimitRequest('PUT', '127.0.0.1:8000',
Expand All @@ -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 number of nodes', async () => {
try {
const validConfig = { RequestsPerSecond: minLimit + 1000 };
await sendRateLimitRequest('PUT', '127.0.0.1:8000',
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/api/bucketPutRateLimit.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,19 @@ describe('bucketPutRateLimit API', () => {
});
});

it('should return InvalidArgument error if RequestsPerSecond is less than number of 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);
Expand Down Expand Up @@ -208,6 +221,23 @@ describe('bucketPutRateLimit API success cases', () => {
});
});

it('should accept RequestsPerSecond equal to number of 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);
Expand Down
Loading