Skip to content

Commit f9db9ad

Browse files
committed
Improved the available rate limit logic
1 parent b8e75cf commit f9db9ad

File tree

3 files changed

+39
-98
lines changed

3 files changed

+39
-98
lines changed

apps/webapp/app/presenters/v3/LimitsPresenter.server.ts

Lines changed: 20 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1+
import { Ratelimit } from "@upstash/ratelimit";
12
import { createHash } from "node:crypto";
23
import { env } from "~/env.server";
3-
import { createRedisClient } from "~/redis.server";
44
import { getCurrentPlan } from "~/services/platform.v3.server";
55
import {
66
RateLimiterConfig,
7+
createLimiterFromConfig,
78
type RateLimitTokenBucketConfig,
89
} from "~/services/authorizationRateLimitMiddleware.server";
9-
import type { Duration } from "~/services/rateLimiter.server";
10+
import { createRedisRateLimitClient, type Duration } from "~/services/rateLimiter.server";
1011
import { BasePresenter } from "./basePresenter.server";
1112
import { singleton } from "~/utils/singleton";
1213
import { logger } from "~/services/logger.server";
1314

1415
// Create a singleton Redis client for rate limit queries
15-
const rateLimitRedis = singleton("rateLimitQueryRedis", () =>
16-
createRedisClient("trigger:rateLimitQuery", {
16+
const rateLimitRedisClient = singleton("rateLimitQueryRedisClient", () =>
17+
createRedisRateLimitClient({
1718
port: env.RATE_LIMIT_REDIS_PORT,
1819
host: env.RATE_LIMIT_REDIS_HOST,
1920
username: env.RATE_LIMIT_REDIS_USERNAME,
@@ -391,11 +392,8 @@ function resolveBatchConcurrencyConfig(batchConcurrencyConfig?: unknown): {
391392
}
392393

393394
/**
394-
* Query Redis for the current remaining tokens for a rate limiter.
395-
* The @upstash/ratelimit library stores token bucket state in Redis.
396-
* Key format: ratelimit:{prefix}:{hashedIdentifier}
397-
*
398-
* For token bucket, the value is stored as: "tokens:lastRefillTime"
395+
* Query the current remaining tokens for a rate limiter using the Upstash getRemaining method.
396+
* This uses the same configuration and hashing logic as the rate limit middleware.
399397
*/
400398
async function getRateLimitRemainingTokens(
401399
keyPrefix: string,
@@ -409,47 +407,20 @@ async function getRateLimitRemainingTokens(
409407
hash.update(authorizationValue);
410408
const hashedKey = hash.digest("hex");
411409

412-
const redis = rateLimitRedis;
413-
const redisKey = `ratelimit:${keyPrefix}:${hashedKey}`;
414-
415-
// Get the stored value from Redis
416-
const value = await redis.get(redisKey);
417-
418-
if (!value) {
419-
// No rate limit data yet - return max tokens (bucket is full)
420-
if (config.type === "tokenBucket") {
421-
return config.maxTokens;
422-
} else if (config.type === "fixedWindow" || config.type === "slidingWindow") {
423-
return config.tokens;
424-
}
425-
return null;
426-
}
427-
428-
// For token bucket, the @upstash/ratelimit library stores: "tokens:timestamp"
429-
// Parse the value to get remaining tokens
430-
if (typeof value === "string") {
431-
const parts = value.split(":");
432-
if (parts.length >= 1) {
433-
const tokens = parseInt(parts[0], 10);
434-
if (!isNaN(tokens)) {
435-
// For token bucket, we need to calculate current tokens based on refill
436-
if (config.type === "tokenBucket" && parts.length >= 2) {
437-
const lastRefillTime = parseInt(parts[1], 10);
438-
if (!isNaN(lastRefillTime)) {
439-
const now = Date.now();
440-
const elapsed = now - lastRefillTime;
441-
const intervalMs = durationToMs(config.interval);
442-
const tokensToAdd = Math.floor(elapsed / intervalMs) * config.refillRate;
443-
const currentTokens = Math.min(tokens + tokensToAdd, config.maxTokens);
444-
return Math.max(0, currentTokens);
445-
}
446-
}
447-
return Math.max(0, tokens);
448-
}
449-
}
450-
}
410+
// Create a Ratelimit instance with the same configuration
411+
const limiter = createLimiterFromConfig(config);
412+
const ratelimit = new Ratelimit({
413+
redis: rateLimitRedisClient,
414+
limiter,
415+
ephemeralCache: new Map(),
416+
analytics: false,
417+
prefix: `ratelimit:${keyPrefix}`,
418+
});
451419

452-
return null;
420+
// Use the getRemaining method to get the current remaining tokens
421+
// getRemaining returns a Promise<number>
422+
const remaining = await ratelimit.getRemaining(hashedKey);
423+
return remaining;
453424
} catch (error) {
454425
logger.warn("Failed to get rate limit remaining tokens", {
455426
keyPrefix,
@@ -458,29 +429,3 @@ async function getRateLimitRemainingTokens(
458429
return null;
459430
}
460431
}
461-
462-
/**
463-
* Convert a duration string (e.g., "1s", "10s", "1m") to milliseconds
464-
*/
465-
function durationToMs(duration: Duration): number {
466-
const match = duration.match(/^(\d+)(ms|s|m|h|d)$/);
467-
if (!match) return 1000; // default to 1 second
468-
469-
const value = parseInt(match[1], 10);
470-
const unit = match[2];
471-
472-
switch (unit) {
473-
case "ms":
474-
return value;
475-
case "s":
476-
return value * 1000;
477-
case "m":
478-
return value * 60 * 1000;
479-
case "h":
480-
return value * 60 * 60 * 1000;
481-
case "d":
482-
return value * 24 * 60 * 60 * 1000;
483-
default:
484-
return 1000;
485-
}
486-
}

apps/webapp/app/runEngine/concerns/batchLimits.server.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { Organization } from "@trigger.dev/database";
2-
import { Ratelimit } from "@upstash/ratelimit";
32
import { z } from "zod";
43
import { env } from "~/env.server";
5-
import { RateLimiterConfig } from "~/services/authorizationRateLimitMiddleware.server";
4+
import {
5+
RateLimiterConfig,
6+
createLimiterFromConfig,
7+
} from "~/services/authorizationRateLimitMiddleware.server";
68
import { createRedisRateLimitClient, Duration, RateLimiter } from "~/services/rateLimiter.server";
79
import { singleton } from "~/utils/singleton";
810

@@ -33,16 +35,7 @@ function createBatchLimitsRedisClient() {
3335
function createOrganizationRateLimiter(organization: Organization): RateLimiter {
3436
const limiterConfig = resolveBatchRateLimitConfig(organization.batchRateLimitConfig);
3537

36-
const limiter =
37-
limiterConfig.type === "fixedWindow"
38-
? Ratelimit.fixedWindow(limiterConfig.tokens, limiterConfig.window)
39-
: limiterConfig.type === "tokenBucket"
40-
? Ratelimit.tokenBucket(
41-
limiterConfig.refillRate,
42-
limiterConfig.interval,
43-
limiterConfig.maxTokens
44-
)
45-
: Ratelimit.slidingWindow(limiterConfig.tokens, limiterConfig.window);
38+
const limiter = createLimiterFromConfig(limiterConfig);
4639

4740
return new RateLimiter({
4841
redisClient: batchLimitsRedisClient,

apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { z } from "zod";
77
import { env } from "~/env.server";
88
import { RedisWithClusterOptions } from "~/redis.server";
99
import { logger } from "./logger.server";
10-
import { createRedisRateLimitClient, Duration, RateLimiter } from "./rateLimiter.server";
10+
import { createRedisRateLimitClient, Duration, Limiter, RateLimiter } from "./rateLimiter.server";
1111
import { RedisCacheStore } from "./unkey/redisCacheStore.server";
1212

1313
const DurationSchema = z.custom<Duration>((value) => {
@@ -130,6 +130,18 @@ async function resolveLimitConfig(
130130
return cacheResult.val ?? defaultLimiter;
131131
}
132132

133+
/**
134+
* Creates a Ratelimit limiter from a RateLimiterConfig.
135+
* This function is shared across the codebase to ensure consistent limiter creation.
136+
*/
137+
export function createLimiterFromConfig(config: RateLimiterConfig): Limiter {
138+
return config.type === "fixedWindow"
139+
? Ratelimit.fixedWindow(config.tokens, config.window)
140+
: config.type === "tokenBucket"
141+
? Ratelimit.tokenBucket(config.refillRate, config.interval, config.maxTokens)
142+
: Ratelimit.slidingWindow(config.tokens, config.window);
143+
}
144+
133145
//returns an Express middleware that rate limits using the Bearer token in the Authorization header
134146
export function authorizationRateLimitMiddleware({
135147
redis,
@@ -249,16 +261,7 @@ export function authorizationRateLimitMiddleware({
249261
limiterConfigOverride
250262
);
251263

252-
const limiter =
253-
limiterConfig.type === "fixedWindow"
254-
? Ratelimit.fixedWindow(limiterConfig.tokens, limiterConfig.window)
255-
: limiterConfig.type === "tokenBucket"
256-
? Ratelimit.tokenBucket(
257-
limiterConfig.refillRate,
258-
limiterConfig.interval,
259-
limiterConfig.maxTokens
260-
)
261-
: Ratelimit.slidingWindow(limiterConfig.tokens, limiterConfig.window);
264+
const limiter = createLimiterFromConfig(limiterConfig);
262265

263266
const rateLimiter = new RateLimiter({
264267
redisClient,

0 commit comments

Comments
 (0)