fix(limits): updated rate limiter to match execution timeouts, adjusted timeouts fallback to be free plan (#3136)

* fix(limits): updated rate limiter to match execution timeouts, adjusted timeouts fallback to be free plan

* upgrade turborepo
This commit is contained in:
Waleed
2026-02-04 15:31:53 -08:00
committed by GitHub
parent 8d846c5983
commit 793adda986
6 changed files with 85 additions and 59 deletions

View File

@@ -65,7 +65,7 @@ export function getExecutionTimeout(
type: 'sync' | 'async' = 'sync'
): number {
if (!isBillingEnabled) {
return EXECUTION_TIMEOUTS.enterprise[type]
return EXECUTION_TIMEOUTS.free[type]
}
return EXECUTION_TIMEOUTS[plan || 'free'][type]
}
@@ -74,9 +74,7 @@ export function getMaxExecutionTimeout(): number {
return EXECUTION_TIMEOUTS.enterprise.async
}
export const DEFAULT_EXECUTION_TIMEOUT_MS = isBillingEnabled
? EXECUTION_TIMEOUTS.free.sync
: EXECUTION_TIMEOUTS.enterprise.sync
export const DEFAULT_EXECUTION_TIMEOUT_MS = EXECUTION_TIMEOUTS.free.sync
export function isTimeoutError(error: unknown): boolean {
if (!error) return false

View File

@@ -5,6 +5,7 @@ import type { ConsumeResult, RateLimitStorageAdapter, TokenStatus } from './stor
import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS, RateLimitError } from './types'
vi.mock('@sim/logger', () => loggerMock)
vi.mock('@/lib/core/config/feature-flags', () => ({ isBillingEnabled: true }))
interface MockAdapter {
consumeTokens: Mock

View File

@@ -1,13 +1,9 @@
import { createLogger } from '@sim/logger'
import { createStorageAdapter, type RateLimitStorageAdapter } from './storage'
import {
createStorageAdapter,
type RateLimitStorageAdapter,
type TokenBucketConfig,
} from './storage'
import {
getRateLimit,
MANUAL_EXECUTION_LIMIT,
RATE_LIMIT_WINDOW_MS,
RATE_LIMITS,
type RateLimitCounterType,
type SubscriptionPlan,
type TriggerType,
@@ -57,21 +53,6 @@ export class RateLimiter {
return isAsync ? 'async' : 'sync'
}
private getBucketConfig(
plan: SubscriptionPlan,
counterType: RateLimitCounterType
): TokenBucketConfig {
const config = RATE_LIMITS[plan]
switch (counterType) {
case 'api-endpoint':
return config.apiEndpoint
case 'async':
return config.async
case 'sync':
return config.sync
}
}
private buildStorageKey(rateLimitKey: string, counterType: RateLimitCounterType): string {
return `${rateLimitKey}:${counterType}`
}
@@ -84,15 +65,6 @@ export class RateLimiter {
}
}
private createUnlimitedStatus(config: TokenBucketConfig): RateLimitStatus {
return {
requestsPerMinute: MANUAL_EXECUTION_LIMIT,
maxBurst: MANUAL_EXECUTION_LIMIT,
remaining: MANUAL_EXECUTION_LIMIT,
resetAt: new Date(Date.now() + config.refillIntervalMs),
}
}
async checkRateLimitWithSubscription(
userId: string,
subscription: SubscriptionInfo | null,
@@ -107,7 +79,7 @@ export class RateLimiter {
const plan = (subscription?.plan || 'free') as SubscriptionPlan
const rateLimitKey = this.getRateLimitKey(userId, subscription)
const counterType = this.getCounterType(triggerType, isAsync)
const config = this.getBucketConfig(plan, counterType)
const config = getRateLimit(plan, counterType)
const storageKey = this.buildStorageKey(rateLimitKey, counterType)
const result = await this.storage.consumeTokens(storageKey, 1, config)
@@ -152,10 +124,15 @@ export class RateLimiter {
try {
const plan = (subscription?.plan || 'free') as SubscriptionPlan
const counterType = this.getCounterType(triggerType, isAsync)
const config = this.getBucketConfig(plan, counterType)
const config = getRateLimit(plan, counterType)
if (triggerType === 'manual') {
return this.createUnlimitedStatus(config)
return {
requestsPerMinute: MANUAL_EXECUTION_LIMIT,
maxBurst: MANUAL_EXECUTION_LIMIT,
remaining: MANUAL_EXECUTION_LIMIT,
resetAt: new Date(Date.now() + config.refillIntervalMs),
}
}
const rateLimitKey = this.getRateLimitKey(userId, subscription)
@@ -178,7 +155,7 @@ export class RateLimiter {
})
const plan = (subscription?.plan || 'free') as SubscriptionPlan
const counterType = this.getCounterType(triggerType, isAsync)
const config = this.getBucketConfig(plan, counterType)
const config = getRateLimit(plan, counterType)
return {
requestsPerMinute: config.refillRate,
maxBurst: config.maxTokens,

View File

@@ -1,4 +1,5 @@
import { env } from '@/lib/core/config/env'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import type { CoreTriggerType } from '@/stores/logs/filters/types'
import type { TokenBucketConfig } from './storage'
@@ -6,6 +7,8 @@ export type TriggerType = CoreTriggerType | 'form' | 'api-endpoint'
export type RateLimitCounterType = 'sync' | 'async' | 'api-endpoint'
type RateLimitConfigKey = 'sync' | 'async' | 'apiEndpoint'
export type SubscriptionPlan = 'free' | 'pro' | 'team' | 'enterprise'
export interface RateLimitConfig {
@@ -18,6 +21,17 @@ export const RATE_LIMIT_WINDOW_MS = Number.parseInt(env.RATE_LIMIT_WINDOW_MS) ||
export const MANUAL_EXECUTION_LIMIT = Number.parseInt(env.MANUAL_EXECUTION_LIMIT) || 999999
const DEFAULT_RATE_LIMITS = {
free: { sync: 50, async: 200, apiEndpoint: 30 },
pro: { sync: 150, async: 1000, apiEndpoint: 100 },
team: { sync: 300, async: 2500, apiEndpoint: 200 },
enterprise: { sync: 600, async: 5000, apiEndpoint: 500 },
} as const
function toConfigKey(type: RateLimitCounterType): RateLimitConfigKey {
return type === 'api-endpoint' ? 'apiEndpoint' : type
}
function createBucketConfig(ratePerMinute: number, burstMultiplier = 2): TokenBucketConfig {
return {
maxTokens: ratePerMinute * burstMultiplier,
@@ -26,29 +40,64 @@ function createBucketConfig(ratePerMinute: number, burstMultiplier = 2): TokenBu
}
}
function getRateLimitForPlan(plan: SubscriptionPlan, type: RateLimitConfigKey): TokenBucketConfig {
const envVarMap: Record<SubscriptionPlan, Record<RateLimitConfigKey, string | undefined>> = {
free: {
sync: env.RATE_LIMIT_FREE_SYNC,
async: env.RATE_LIMIT_FREE_ASYNC,
apiEndpoint: undefined,
},
pro: { sync: env.RATE_LIMIT_PRO_SYNC, async: env.RATE_LIMIT_PRO_ASYNC, apiEndpoint: undefined },
team: {
sync: env.RATE_LIMIT_TEAM_SYNC,
async: env.RATE_LIMIT_TEAM_ASYNC,
apiEndpoint: undefined,
},
enterprise: {
sync: env.RATE_LIMIT_ENTERPRISE_SYNC,
async: env.RATE_LIMIT_ENTERPRISE_ASYNC,
apiEndpoint: undefined,
},
}
const rate = Number.parseInt(envVarMap[plan][type] || '') || DEFAULT_RATE_LIMITS[plan][type]
return createBucketConfig(rate)
}
export const RATE_LIMITS: Record<SubscriptionPlan, RateLimitConfig> = {
free: {
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_FREE_SYNC) || 50),
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_FREE_ASYNC) || 200),
apiEndpoint: createBucketConfig(30),
sync: getRateLimitForPlan('free', 'sync'),
async: getRateLimitForPlan('free', 'async'),
apiEndpoint: getRateLimitForPlan('free', 'apiEndpoint'),
},
pro: {
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_PRO_SYNC) || 150),
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_PRO_ASYNC) || 1000),
apiEndpoint: createBucketConfig(100),
sync: getRateLimitForPlan('pro', 'sync'),
async: getRateLimitForPlan('pro', 'async'),
apiEndpoint: getRateLimitForPlan('pro', 'apiEndpoint'),
},
team: {
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_TEAM_SYNC) || 300),
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_TEAM_ASYNC) || 2500),
apiEndpoint: createBucketConfig(200),
sync: getRateLimitForPlan('team', 'sync'),
async: getRateLimitForPlan('team', 'async'),
apiEndpoint: getRateLimitForPlan('team', 'apiEndpoint'),
},
enterprise: {
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_ENTERPRISE_SYNC) || 600),
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_ENTERPRISE_ASYNC) || 5000),
apiEndpoint: createBucketConfig(500),
sync: getRateLimitForPlan('enterprise', 'sync'),
async: getRateLimitForPlan('enterprise', 'async'),
apiEndpoint: getRateLimitForPlan('enterprise', 'apiEndpoint'),
},
}
export function getRateLimit(
plan: SubscriptionPlan | undefined,
type: RateLimitCounterType
): TokenBucketConfig {
const key = toConfigKey(type)
if (!isBillingEnabled) {
return RATE_LIMITS.free[key]
}
return RATE_LIMITS[plan || 'free'][key]
}
export class RateLimitError extends Error {
statusCode: number
constructor(message: string, statusCode = 429) {

View File

@@ -1,5 +1,6 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "simstudio",
@@ -12,7 +13,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.0",
"turbo": "2.8.3",
},
},
"apps/docs": {
@@ -3429,19 +3430,19 @@
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"turbo": ["turbo@2.8.0", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.0", "turbo-darwin-arm64": "2.8.0", "turbo-linux-64": "2.8.0", "turbo-linux-arm64": "2.8.0", "turbo-windows-64": "2.8.0", "turbo-windows-arm64": "2.8.0" }, "bin": { "turbo": "bin/turbo" } }, "sha512-hYbxnLEdvJF+DLALS+Ia+PbfNtn0sDP0hH2u9AFoskSUDmcVHSrtwHpzdX94MrRJKo9D9tYxY3MyP20gnlrWyA=="],
"turbo": ["turbo@2.8.3", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.3", "turbo-darwin-arm64": "2.8.3", "turbo-linux-64": "2.8.3", "turbo-linux-arm64": "2.8.3", "turbo-windows-64": "2.8.3", "turbo-windows-arm64": "2.8.3" }, "bin": { "turbo": "bin/turbo" } }, "sha512-8Osxz5Tu/Dw2kb31EAY+nhq/YZ3wzmQSmYa1nIArqxgCAldxv9TPlrAiaBUDVnKA4aiPn0OFBD1ACcpc5VFOAQ=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N7f4PYqz25yk8c5kituk09bJ89tE4wPPqKXgYccT6nbEQnGnrdvlyCHLyqViNObTgjjrddqjb1hmDkv7VcxE0g=="],
"turbo-darwin-64": ["turbo-darwin-64@2.8.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-4kXRLfcygLOeNcP6JquqRLmGB/ATjjfehiojL2dJkL7GFm3SPSXbq7oNj8UbD8XriYQ5hPaSuz59iF1ijPHkTw=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eVzejaP5fn51gmJAPW68U6mSjFaAZ26rPiE36mMdk+tMC4XBGmJHT/fIgrhcrXMvINCl27RF8VmguRe+MBlSuQ=="],
"turbo-darwin-arm64": ["turbo-darwin-arm64@2.8.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xF7uCeC0UY0Hrv/tqax0BMbFlVP1J/aRyeGQPZT4NjvIPj8gSPDgFhfkfz06DhUwDg5NgMo04uiSkAWE8WB/QQ=="],
"turbo-linux-64": ["turbo-linux-64@2.8.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ILR45zviYae3icf4cmUISdj8X17ybNcMh3Ms4cRdJF5sS50qDDTv8qeWqO/lPeHsu6r43gVWDofbDZYVuXYL7Q=="],
"turbo-linux-64": ["turbo-linux-64@2.8.3", "", { "os": "linux", "cpu": "x64" }, "sha512-vxMDXwaOjweW/4etY7BxrXCSkvtwh0PbwVafyfT1Ww659SedUxd5rM3V2ZCmbwG8NiCfY7d6VtxyHx3Wh1GoZA=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-z9pUa8ENFuHmadPfjEYMRWlXO82t1F/XBDx2XTg+cWWRZHf85FnEB6D4ForJn/GoKEEvwdPhFLzvvhOssom2ug=="],
"turbo-linux-arm64": ["turbo-linux-arm64@2.8.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-mQX7uYBZFkuPLLlKaNe9IjR1JIef4YvY8f21xFocvttXvdPebnq3PK1Zjzl9A1zun2BEuWNUwQIL8lgvN9Pm3Q=="],
"turbo-windows-64": ["turbo-windows-64@2.8.0", "", { "os": "win32", "cpu": "x64" }, "sha512-J6juRSRjmSErEqJCv7nVIq2DgZ2NHXqyeV8NQTFSyIvrThKiWe7FDOO6oYpuR06+C1NW82aoN4qQt4/gYvz25w=="],
"turbo-windows-64": ["turbo-windows-64@2.8.3", "", { "os": "win32", "cpu": "x64" }, "sha512-YLGEfppGxZj3VWcNOVa08h6ISsVKiG85aCAWosOKNUjb6yErWEuydv6/qImRJUI+tDLvDvW7BxopAkujRnWCrw=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qarBZvCu6uka35739TS+y/3CBU3zScrVAfohAkKHG+So+93Wn+5tKArs8HrO2fuTaGou8fMIeTV7V5NgzCVkSQ=="],
"turbo-windows-arm64": ["turbo-windows-arm64@2.8.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-afTUGKBRmOJU1smQSBnFGcbq0iabAPwh1uXu2BVk7BREg30/1gMnJh9DFEQTah+UD3n3ru8V55J83RQNFfqoyw=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],

View File

@@ -42,7 +42,7 @@
"glob": "13.0.0",
"husky": "9.1.7",
"lint-staged": "16.0.0",
"turbo": "2.8.0"
"turbo": "2.8.3"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,css,scss}": [