Skip to content

Commit 9b02185

Browse files
committed
Merge branch 'bugfix/CLDSRV-828-fix-limit-override-bug' into q/9.2
2 parents 4237aad + 456bd7c commit 9b02185

6 files changed

Lines changed: 101 additions & 0 deletions

File tree

lib/api/apiUtils/rateLimit/cache.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,22 @@ function expireCachedConfigs(now) {
3939
return toRemove.length;
4040
}
4141

42+
/**
43+
* Invalidate cached config for a specific bucket
44+
*
45+
* @param {string} bucketName - Bucket name
46+
* @returns {boolean} True if entry was found and removed
47+
*/
48+
function invalidateCachedConfig(bucketName) {
49+
const cacheKey = `bucket:${bucketName}`;
50+
return configCache.delete(cacheKey);
51+
}
52+
4253
module.exports = {
4354
setCachedConfig,
4455
getCachedConfig,
4556
expireCachedConfigs,
57+
invalidateCachedConfig,
4658

4759
// Do not access directly
4860
// Used only for tests

lib/api/apiUtils/rateLimit/tokenBucket.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,16 @@ function getTokenBucket(bucketName, limitConfig, log) {
247247
bufferSize: bucket.bufferSize,
248248
refillThreshold: bucket.refillThreshold,
249249
});
250+
} else if (bucket.limitConfig.limit !== limitConfig.limit) {
251+
// Update limit config when it changes dynamically
252+
const oldLimit = bucket.limitConfig.limit;
253+
bucket.limitConfig = limitConfig;
254+
255+
log.info('Updated token bucket limit config', {
256+
bucketName,
257+
oldLimit,
258+
newLimit: limitConfig.limit,
259+
});
250260
}
251261

252262
return bucket;
@@ -286,9 +296,20 @@ function cleanupTokenBuckets(maxIdleMs = 60000) {
286296
return toRemove.length;
287297
}
288298

299+
/**
300+
* Remove a specific token bucket (used when rate limit config is deleted)
301+
*
302+
* @param {string} bucketName - Bucket name
303+
* @returns {boolean} True if bucket was found and removed
304+
*/
305+
function removeTokenBucket(bucketName) {
306+
return tokenBuckets.delete(bucketName);
307+
}
308+
289309
module.exports = {
290310
WorkerTokenBucket,
291311
getTokenBucket,
292312
getAllTokenBuckets,
293313
cleanupTokenBuckets,
314+
removeTokenBucket,
294315
};

lib/api/bucketDeleteRateLimit.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ const metadata = require('../metadata/wrapper');
44
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
55
const collectCorsHeaders = require('../utilities/collectCorsHeaders');
66
const { isRateLimitServiceUser } = require('./apiUtils/authorization/serviceUser');
7+
const { invalidateCachedConfig } = require('./apiUtils/rateLimit/cache');
8+
const { removeTokenBucket } = require('./apiUtils/rateLimit/tokenBucket');
79

810
/**
911
* bucketDeleteRateLimit - Delete the bucket rate limit configuration
@@ -49,6 +51,10 @@ function bucketDeleteRateLimit(authInfo, request, log, callback) {
4951
if (err) {
5052
return callback(err, corsHeaders);
5153
}
54+
// Invalidate cache and remove token bucket
55+
invalidateCachedConfig(bucketName);
56+
removeTokenBucket(bucketName);
57+
log.debug('invalidated rate limit cache and token bucket for bucket', { bucketName });
5258
// TODO: implement Utapi metric support
5359
return callback(null, corsHeaders);
5460
});

lib/api/bucketPutRateLimit.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const { config } = require('../Config');
66
const metadata = require('../metadata/wrapper');
77
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
88
const { isRateLimitServiceUser } = require('./apiUtils/authorization/serviceUser');
9+
const { invalidateCachedConfig } = require('./apiUtils/rateLimit/cache');
910

1011
function parseRequestBody(requestBody, callback) {
1112
try {
@@ -92,6 +93,9 @@ function bucketPutRateLimit(authInfo, request, log, callback) {
9293
{ error: err, method: 'bucketPutRateLimit' });
9394
return callback(err, corsHeaders);
9495
}
96+
// Invalidate cache so new limit takes effect immediately
97+
invalidateCachedConfig(bucketName);
98+
log.debug('invalidated rate limit cache for bucket', { bucketName });
9599
// TODO: implement Utapi metric support
96100
return callback(null, corsHeaders);
97101
});

tests/unit/api/apiUtils/rateLimit/cache.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
getCachedConfig,
88
setCachedConfig,
99
expireCachedConfigs,
10+
invalidateCachedConfig,
1011
} = require('../../../../../lib/api/apiUtils/rateLimit/cache');
1112

1213
describe('test limit config cache storage', () => {
@@ -66,4 +67,21 @@ describe('test limit config cache storage', () => {
6667
config: 10,
6768
});
6869
});
70+
71+
it('should invalidate cached config for a specific bucket', () => {
72+
setCachedConfig('bucket:my-bucket', { limit: 100 }, constants.rateLimitDefaultConfigCacheTTL);
73+
setCachedConfig('bucket:other-bucket', { limit: 200 }, constants.rateLimitDefaultConfigCacheTTL);
74+
75+
const result = invalidateCachedConfig('my-bucket');
76+
77+
assert.strictEqual(result, true);
78+
assert.strictEqual(getCachedConfig('bucket:my-bucket'), undefined);
79+
assert.deepStrictEqual(getCachedConfig('bucket:other-bucket'), { limit: 200 });
80+
});
81+
82+
it('should return false when invalidating non-existent bucket', () => {
83+
const result = invalidateCachedConfig('non-existent-bucket');
84+
85+
assert.strictEqual(result, false);
86+
});
6987
});

tests/unit/api/apiUtils/rateLimit/tokenBucket.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,46 @@ describe('Token bucket management functions', () => {
377377
assert.strictEqual(bucket1.bucketName, 'bucket-1');
378378
assert.strictEqual(bucket2.bucketName, 'bucket-2');
379379
});
380+
381+
it('should update limitConfig when limit changes', () => {
382+
const bucket1 = tokenBucket.getTokenBucket('test-bucket', { limit: 100, source: 'bucket' }, mockLog);
383+
assert.strictEqual(bucket1.limitConfig.limit, 100);
384+
385+
// Simulate rate limit change
386+
const bucket2 = tokenBucket.getTokenBucket('test-bucket', { limit: 200, source: 'bucket' }, mockLog);
387+
388+
assert.strictEqual(bucket1, bucket2); // Same bucket instance
389+
assert.strictEqual(bucket2.limitConfig.limit, 200); // Limit updated
390+
assert(mockLog.info.calledOnce);
391+
assert(mockLog.info.firstCall.args[0].includes('Updated token bucket limit config'));
392+
});
393+
394+
it('should not log update when limit is unchanged', () => {
395+
tokenBucket.getTokenBucket('test-bucket', { limit: 100 }, mockLog);
396+
mockLog.info.resetHistory();
397+
398+
tokenBucket.getTokenBucket('test-bucket', { limit: 100 }, mockLog);
399+
400+
assert.strictEqual(mockLog.info.called, false);
401+
});
402+
});
403+
404+
describe('removeTokenBucket', () => {
405+
it('should remove existing bucket and return true', () => {
406+
tokenBucket.getTokenBucket('test-bucket', { limit: 100 }, mockLog);
407+
assert.strictEqual(tokenBucket.getAllTokenBuckets().size, 1);
408+
409+
const result = tokenBucket.removeTokenBucket('test-bucket');
410+
411+
assert.strictEqual(result, true);
412+
assert.strictEqual(tokenBucket.getAllTokenBuckets().size, 0);
413+
});
414+
415+
it('should return false when bucket does not exist', () => {
416+
const result = tokenBucket.removeTokenBucket('non-existent-bucket');
417+
418+
assert.strictEqual(result, false);
419+
});
380420
});
381421

382422
describe('getAllTokenBuckets', () => {

0 commit comments

Comments
 (0)