diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-03-28 11:43:30 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-03-28 11:43:30 -0400 |
| commit | 86e34175d36ce07b1baec28b871ef0ef0692c326 (patch) | |
| tree | 650869c07488051bc888f954a2363defef4032cb /packages/backend/src/server/SkRateLimiterService.ts | |
| parent | limit the number of active connections per client, and limit upgrade requests... (diff) | |
| download | sharkey-86e34175d36ce07b1baec28b871ef0ef0692c326.tar.gz sharkey-86e34175d36ce07b1baec28b871ef0ef0692c326.tar.bz2 sharkey-86e34175d36ce07b1baec28b871ef0ef0692c326.zip | |
SkRateLimiterService revision 3: cache lockouts in memory to avoid redis calls
Diffstat (limited to 'packages/backend/src/server/SkRateLimiterService.ts')
| -rw-r--r-- | packages/backend/src/server/SkRateLimiterService.ts | 57 |
1 files changed, 57 insertions, 0 deletions
diff --git a/packages/backend/src/server/SkRateLimiterService.ts b/packages/backend/src/server/SkRateLimiterService.ts index 30bf092e4f..ccc26c1b61 100644 --- a/packages/backend/src/server/SkRateLimiterService.ts +++ b/packages/backend/src/server/SkRateLimiterService.ts @@ -17,10 +17,17 @@ import type { RoleService } from '@/core/RoleService.js'; // Required because MemoryKVCache doesn't support null keys. const defaultUserKey = ''; +interface Lockout { + at: number; + info: LimitInfo; +} + @Injectable() export class SkRateLimiterService { // 1-minute cache interval private readonly factorCache = new MemoryKVCache<number>(1000 * 60); + // 10-second cache interval + private readonly lockoutCache = new MemoryKVCache<Lockout>(1000 * 10); private readonly disabled: boolean; constructor( @@ -58,6 +65,15 @@ export class SkRateLimiterService { } const actor = typeof(actorOrUser) === 'object' ? actorOrUser.id : actorOrUser; + + // TODO add to docs + // Fast-path to avoid extra redis calls for blocked clients + const lockoutKey = `@${actor}#${limit.key}`; + const lockout = this.getLockout(lockoutKey); + if (lockout) { + return lockout; + } + const userCacheKey = typeof(actorOrUser) === 'object' ? actorOrUser.id : defaultUserKey; const userRoleKey = typeof(actorOrUser) === 'object' ? actorOrUser.id : null; const factor = this.factorCache.get(userCacheKey) ?? await this.factorCache.fetch(userCacheKey, async () => { @@ -73,6 +89,47 @@ export class SkRateLimiterService { throw new Error(`Rate limit factor is zero or negative: ${factor}`); } + const info = await this.applyLimit(limit, actor, factor); + + // Store blocked status to avoid hammering redis + if (info.blocked) { + this.lockoutCache.set(lockoutKey, { + at: this.timeService.now, + info, + }); + } + + return info; + } + + private getLockout(lockoutKey: string): LimitInfo | null { + const lockout = this.lockoutCache.get(lockoutKey); + if (!lockout) { + // Not blocked, proceed with redis check + return null; + } + + const now = this.timeService.now; + const elapsedMs = now - lockout.at; + if (elapsedMs >= lockout.info.resetMs) { + // Block expired, clear and proceed with redis check + this.lockoutCache.delete(lockoutKey); + return null; + } + + // Limit is still active, update calculations + lockout.at = now; + lockout.info.resetMs -= elapsedMs; + lockout.info.resetSec = Math.ceil(lockout.info.resetMs / 1000); + lockout.info.fullResetMs -= elapsedMs; + lockout.info.fullResetSec = Math.ceil(lockout.info.fullResetMs / 1000); + + // Re-cache the new object + this.lockoutCache.set(lockoutKey, lockout); + return lockout.info; + } + + private async applyLimit(limit: Keyed<RateLimit>, actor: string, factor: number) { if (isLegacyRateLimit(limit)) { return await this.limitLegacy(limit, actor, factor); } else { |