Skip to content

Commit 793adda

Browse files
authored
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
1 parent 8d846c5 commit 793adda

File tree

6 files changed

+85
-59
lines changed

6 files changed

+85
-59
lines changed

apps/sim/lib/core/execution-limits/types.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function getExecutionTimeout(
6565
type: 'sync' | 'async' = 'sync'
6666
): number {
6767
if (!isBillingEnabled) {
68-
return EXECUTION_TIMEOUTS.enterprise[type]
68+
return EXECUTION_TIMEOUTS.free[type]
6969
}
7070
return EXECUTION_TIMEOUTS[plan || 'free'][type]
7171
}
@@ -74,9 +74,7 @@ export function getMaxExecutionTimeout(): number {
7474
return EXECUTION_TIMEOUTS.enterprise.async
7575
}
7676

77-
export const DEFAULT_EXECUTION_TIMEOUT_MS = isBillingEnabled
78-
? EXECUTION_TIMEOUTS.free.sync
79-
: EXECUTION_TIMEOUTS.enterprise.sync
77+
export const DEFAULT_EXECUTION_TIMEOUT_MS = EXECUTION_TIMEOUTS.free.sync
8078

8179
export function isTimeoutError(error: unknown): boolean {
8280
if (!error) return false

apps/sim/lib/core/rate-limiter/rate-limiter.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ConsumeResult, RateLimitStorageAdapter, TokenStatus } from './stor
55
import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS, RateLimitError } from './types'
66

77
vi.mock('@sim/logger', () => loggerMock)
8+
vi.mock('@/lib/core/config/feature-flags', () => ({ isBillingEnabled: true }))
89

910
interface MockAdapter {
1011
consumeTokens: Mock

apps/sim/lib/core/rate-limiter/rate-limiter.ts

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { createLogger } from '@sim/logger'
2+
import { createStorageAdapter, type RateLimitStorageAdapter } from './storage'
23
import {
3-
createStorageAdapter,
4-
type RateLimitStorageAdapter,
5-
type TokenBucketConfig,
6-
} from './storage'
7-
import {
4+
getRateLimit,
85
MANUAL_EXECUTION_LIMIT,
96
RATE_LIMIT_WINDOW_MS,
10-
RATE_LIMITS,
117
type RateLimitCounterType,
128
type SubscriptionPlan,
139
type TriggerType,
@@ -57,21 +53,6 @@ export class RateLimiter {
5753
return isAsync ? 'async' : 'sync'
5854
}
5955

60-
private getBucketConfig(
61-
plan: SubscriptionPlan,
62-
counterType: RateLimitCounterType
63-
): TokenBucketConfig {
64-
const config = RATE_LIMITS[plan]
65-
switch (counterType) {
66-
case 'api-endpoint':
67-
return config.apiEndpoint
68-
case 'async':
69-
return config.async
70-
case 'sync':
71-
return config.sync
72-
}
73-
}
74-
7556
private buildStorageKey(rateLimitKey: string, counterType: RateLimitCounterType): string {
7657
return `${rateLimitKey}:${counterType}`
7758
}
@@ -84,15 +65,6 @@ export class RateLimiter {
8465
}
8566
}
8667

87-
private createUnlimitedStatus(config: TokenBucketConfig): RateLimitStatus {
88-
return {
89-
requestsPerMinute: MANUAL_EXECUTION_LIMIT,
90-
maxBurst: MANUAL_EXECUTION_LIMIT,
91-
remaining: MANUAL_EXECUTION_LIMIT,
92-
resetAt: new Date(Date.now() + config.refillIntervalMs),
93-
}
94-
}
95-
9668
async checkRateLimitWithSubscription(
9769
userId: string,
9870
subscription: SubscriptionInfo | null,
@@ -107,7 +79,7 @@ export class RateLimiter {
10779
const plan = (subscription?.plan || 'free') as SubscriptionPlan
10880
const rateLimitKey = this.getRateLimitKey(userId, subscription)
10981
const counterType = this.getCounterType(triggerType, isAsync)
110-
const config = this.getBucketConfig(plan, counterType)
82+
const config = getRateLimit(plan, counterType)
11183
const storageKey = this.buildStorageKey(rateLimitKey, counterType)
11284

11385
const result = await this.storage.consumeTokens(storageKey, 1, config)
@@ -152,10 +124,15 @@ export class RateLimiter {
152124
try {
153125
const plan = (subscription?.plan || 'free') as SubscriptionPlan
154126
const counterType = this.getCounterType(triggerType, isAsync)
155-
const config = this.getBucketConfig(plan, counterType)
127+
const config = getRateLimit(plan, counterType)
156128

157129
if (triggerType === 'manual') {
158-
return this.createUnlimitedStatus(config)
130+
return {
131+
requestsPerMinute: MANUAL_EXECUTION_LIMIT,
132+
maxBurst: MANUAL_EXECUTION_LIMIT,
133+
remaining: MANUAL_EXECUTION_LIMIT,
134+
resetAt: new Date(Date.now() + config.refillIntervalMs),
135+
}
159136
}
160137

161138
const rateLimitKey = this.getRateLimitKey(userId, subscription)
@@ -178,7 +155,7 @@ export class RateLimiter {
178155
})
179156
const plan = (subscription?.plan || 'free') as SubscriptionPlan
180157
const counterType = this.getCounterType(triggerType, isAsync)
181-
const config = this.getBucketConfig(plan, counterType)
158+
const config = getRateLimit(plan, counterType)
182159
return {
183160
requestsPerMinute: config.refillRate,
184161
maxBurst: config.maxTokens,

apps/sim/lib/core/rate-limiter/types.ts

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { env } from '@/lib/core/config/env'
2+
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
23
import type { CoreTriggerType } from '@/stores/logs/filters/types'
34
import type { TokenBucketConfig } from './storage'
45

56
export type TriggerType = CoreTriggerType | 'form' | 'api-endpoint'
67

78
export type RateLimitCounterType = 'sync' | 'async' | 'api-endpoint'
89

10+
type RateLimitConfigKey = 'sync' | 'async' | 'apiEndpoint'
11+
912
export type SubscriptionPlan = 'free' | 'pro' | 'team' | 'enterprise'
1013

1114
export interface RateLimitConfig {
@@ -18,6 +21,17 @@ export const RATE_LIMIT_WINDOW_MS = Number.parseInt(env.RATE_LIMIT_WINDOW_MS) ||
1821

1922
export const MANUAL_EXECUTION_LIMIT = Number.parseInt(env.MANUAL_EXECUTION_LIMIT) || 999999
2023

24+
const DEFAULT_RATE_LIMITS = {
25+
free: { sync: 50, async: 200, apiEndpoint: 30 },
26+
pro: { sync: 150, async: 1000, apiEndpoint: 100 },
27+
team: { sync: 300, async: 2500, apiEndpoint: 200 },
28+
enterprise: { sync: 600, async: 5000, apiEndpoint: 500 },
29+
} as const
30+
31+
function toConfigKey(type: RateLimitCounterType): RateLimitConfigKey {
32+
return type === 'api-endpoint' ? 'apiEndpoint' : type
33+
}
34+
2135
function createBucketConfig(ratePerMinute: number, burstMultiplier = 2): TokenBucketConfig {
2236
return {
2337
maxTokens: ratePerMinute * burstMultiplier,
@@ -26,29 +40,64 @@ function createBucketConfig(ratePerMinute: number, burstMultiplier = 2): TokenBu
2640
}
2741
}
2842

43+
function getRateLimitForPlan(plan: SubscriptionPlan, type: RateLimitConfigKey): TokenBucketConfig {
44+
const envVarMap: Record<SubscriptionPlan, Record<RateLimitConfigKey, string | undefined>> = {
45+
free: {
46+
sync: env.RATE_LIMIT_FREE_SYNC,
47+
async: env.RATE_LIMIT_FREE_ASYNC,
48+
apiEndpoint: undefined,
49+
},
50+
pro: { sync: env.RATE_LIMIT_PRO_SYNC, async: env.RATE_LIMIT_PRO_ASYNC, apiEndpoint: undefined },
51+
team: {
52+
sync: env.RATE_LIMIT_TEAM_SYNC,
53+
async: env.RATE_LIMIT_TEAM_ASYNC,
54+
apiEndpoint: undefined,
55+
},
56+
enterprise: {
57+
sync: env.RATE_LIMIT_ENTERPRISE_SYNC,
58+
async: env.RATE_LIMIT_ENTERPRISE_ASYNC,
59+
apiEndpoint: undefined,
60+
},
61+
}
62+
63+
const rate = Number.parseInt(envVarMap[plan][type] || '') || DEFAULT_RATE_LIMITS[plan][type]
64+
return createBucketConfig(rate)
65+
}
66+
2967
export const RATE_LIMITS: Record<SubscriptionPlan, RateLimitConfig> = {
3068
free: {
31-
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_FREE_SYNC) || 50),
32-
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_FREE_ASYNC) || 200),
33-
apiEndpoint: createBucketConfig(30),
69+
sync: getRateLimitForPlan('free', 'sync'),
70+
async: getRateLimitForPlan('free', 'async'),
71+
apiEndpoint: getRateLimitForPlan('free', 'apiEndpoint'),
3472
},
3573
pro: {
36-
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_PRO_SYNC) || 150),
37-
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_PRO_ASYNC) || 1000),
38-
apiEndpoint: createBucketConfig(100),
74+
sync: getRateLimitForPlan('pro', 'sync'),
75+
async: getRateLimitForPlan('pro', 'async'),
76+
apiEndpoint: getRateLimitForPlan('pro', 'apiEndpoint'),
3977
},
4078
team: {
41-
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_TEAM_SYNC) || 300),
42-
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_TEAM_ASYNC) || 2500),
43-
apiEndpoint: createBucketConfig(200),
79+
sync: getRateLimitForPlan('team', 'sync'),
80+
async: getRateLimitForPlan('team', 'async'),
81+
apiEndpoint: getRateLimitForPlan('team', 'apiEndpoint'),
4482
},
4583
enterprise: {
46-
sync: createBucketConfig(Number.parseInt(env.RATE_LIMIT_ENTERPRISE_SYNC) || 600),
47-
async: createBucketConfig(Number.parseInt(env.RATE_LIMIT_ENTERPRISE_ASYNC) || 5000),
48-
apiEndpoint: createBucketConfig(500),
84+
sync: getRateLimitForPlan('enterprise', 'sync'),
85+
async: getRateLimitForPlan('enterprise', 'async'),
86+
apiEndpoint: getRateLimitForPlan('enterprise', 'apiEndpoint'),
4987
},
5088
}
5189

90+
export function getRateLimit(
91+
plan: SubscriptionPlan | undefined,
92+
type: RateLimitCounterType
93+
): TokenBucketConfig {
94+
const key = toConfigKey(type)
95+
if (!isBillingEnabled) {
96+
return RATE_LIMITS.free[key]
97+
}
98+
return RATE_LIMITS[plan || 'free'][key]
99+
}
100+
52101
export class RateLimitError extends Error {
53102
statusCode: number
54103
constructor(message: string, statusCode = 429) {

bun.lock

Lines changed: 9 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
"glob": "13.0.0",
4343
"husky": "9.1.7",
4444
"lint-staged": "16.0.0",
45-
"turbo": "2.8.0"
45+
"turbo": "2.8.3"
4646
},
4747
"lint-staged": {
4848
"*.{js,jsx,ts,tsx,json,css,scss}": [

0 commit comments

Comments
 (0)