mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-04 19:55:08 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
17
bun.lock
17
bun.lock
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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}": [
|
||||
|
||||
Reference in New Issue
Block a user