Skip to content

Commit f57fcab

Browse files
committed
v3: recover from server rate limiting errors in a more reliable way
- Changing from sliding window to token bucket in the API rate limiter, to help smooth out traffic - Adding spans to the API Client core & SDK functions - Added waiting spans when retrying in the API Client - Retrying in the API Client now respects the x-ratelimit-reset - Retrying ApiError’s in tasks now respects the x-ratelimit-reset - Added AbortTaskRunError that when thrown will stop retries - Added idempotency keys SDK functions and automatically injecting the run ID when inside a task - Added the ability to configure ApiRequestOptions (retries only for now) globally and on specific calls - Implement the maxAttempts TaskRunOption (it wasn’t doing anything before)
1 parent a0fb890 commit f57fcab

File tree

31 files changed

+1867
-518
lines changed

31 files changed

+1867
-518
lines changed

apps/webapp/app/env.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,9 @@ const EnvironmentSchema = z.object({
9797
* @example "1000ms"
9898
* @example "1000s"
9999
*/
100-
API_RATE_LIMIT_WINDOW: z.string().default("60s"),
101-
API_RATE_LIMIT_MAX: z.coerce.number().int().default(600),
100+
API_RATE_LIMIT_REFILL_INTERVAL: z.string().default("10s"), // refill 250 tokens every 10 seconds
101+
API_RATE_LIMIT_MAX: z.coerce.number().int().default(750), // allow bursts of 750 requests
102+
API_RATE_LIMIT_REFILL_RATE: z.coerce.number().int().default(250), // refix 250 tokens every 10 seconds
102103
API_RATE_LIMIT_REQUEST_LOGS_ENABLED: z.string().default("0"),
103104
API_RATE_LIMIT_REJECTION_LOGS_ENABLED: z.string().default("1"),
104105

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,10 @@ export function authorizationRateLimitMiddleware({
106106
hashedAuthorizationValue
107107
);
108108

109+
const $remaining = Math.max(0, remaining); // remaining can be negative if the user has exceeded the limit, so clamp it to 0
110+
109111
res.set("x-ratelimit-limit", limit.toString());
110-
res.set("x-ratelimit-remaining", remaining.toString());
112+
res.set("x-ratelimit-remaining", $remaining.toString());
111113
res.set("x-ratelimit-reset", reset.toString());
112114

113115
if (success) {
@@ -122,12 +124,12 @@ export function authorizationRateLimitMiddleware({
122124
title: "Rate Limit Exceeded",
123125
status: 429,
124126
type: "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429",
125-
detail: `Rate limit exceeded ${remaining}/${limit} requests remaining. Retry in ${secondsUntilReset} seconds.`,
127+
detail: `Rate limit exceeded ${$remaining}/${limit} requests remaining. Retry in ${secondsUntilReset} seconds.`,
126128
reset,
127129
limit,
128130
remaining,
129131
secondsUntilReset,
130-
error: `Rate limit exceeded ${remaining}/${limit} requests remaining. Retry in ${secondsUntilReset} seconds.`,
132+
error: `Rate limit exceeded ${$remaining}/${limit} requests remaining. Retry in ${secondsUntilReset} seconds.`,
131133
},
132134
null,
133135
2
@@ -138,7 +140,11 @@ export function authorizationRateLimitMiddleware({
138140

139141
export const apiRateLimiter = authorizationRateLimitMiddleware({
140142
keyPrefix: "api",
141-
limiter: Ratelimit.slidingWindow(env.API_RATE_LIMIT_MAX, env.API_RATE_LIMIT_WINDOW as Duration),
143+
limiter: Ratelimit.tokenBucket(
144+
env.API_RATE_LIMIT_REFILL_RATE,
145+
env.API_RATE_LIMIT_REFILL_INTERVAL as Duration,
146+
env.API_RATE_LIMIT_MAX
147+
),
142148
pathMatchers: [/^\/api/],
143149
// Allow /api/v1/tasks/:id/callback/:secret
144150
pathWhiteList: [
@@ -152,6 +158,8 @@ export const apiRateLimiter = authorizationRateLimitMiddleware({
152158
/^\/api\/v1\/sources\/http\/[^\/]+$/, // /api/v1/sources/http/$id
153159
/^\/api\/v1\/endpoints\/[^\/]+\/[^\/]+\/index\/[^\/]+$/, // /api/v1/endpoints/$environmentId/$endpointSlug/index/$indexHookIdentifier
154160
"/api/v1/timezones",
161+
"/api/v1/usage/ingest",
162+
/^\/api\/v1\/runs\/[^\/]+\/attempts$/, // /api/v1/runs/$runFriendlyId/attempts
155163
],
156164
log: {
157165
rejections: env.API_RATE_LIMIT_REJECTION_LOGS_ENABLED === "1",

apps/webapp/app/v3/services/createTaskRunAttempt.server.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,14 @@ export class CreateTaskRunAttemptService extends BaseService {
5050
},
5151
lockedBy: {
5252
include: {
53-
worker: true,
53+
worker: {
54+
select: {
55+
id: true,
56+
version: true,
57+
sdkVersion: true,
58+
cliVersion: true,
59+
},
60+
},
5461
},
5562
},
5663
batchItems: {
@@ -172,6 +179,7 @@ export class CreateTaskRunAttemptService extends BaseService {
172179
durationMs: taskRun.usageDurationMs,
173180
costInCents: taskRun.costInCents,
174181
baseCostInCents: taskRun.baseCostInCents,
182+
maxAttempts: taskRun.maxAttempts ?? undefined,
175183
},
176184
queue: {
177185
id: queue.friendlyId,

apps/webapp/app/v3/services/triggerTask.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ export class TriggerTaskService extends BaseService {
206206
isTest: body.options?.test ?? false,
207207
delayUntil,
208208
queuedAt: delayUntil ? undefined : new Date(),
209+
maxAttempts: body.options?.maxAttempts,
209210
ttl,
210211
},
211212
});

docs/v3/errors-retrying.mdx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,11 +255,39 @@ export const openaiTask = task({
255255
});
256256
```
257257

258-
## Using try/catch to prevent retries
258+
## Preventing retries
259+
260+
### Using `AbortTaskRunError`
261+
262+
You can prevent retries by throwing an `AbortTaskRunError`. This will fail the task attempt and disable retrying.
263+
264+
```ts /trigger/myTasks.ts
265+
import { task, AbortTaskRunError } from "@trigger.dev/sdk/v3";
266+
267+
export const openaiTask = task({
268+
id: "openai-task",
269+
run: async (payload: { prompt: string }) => {
270+
//if this fails, it will throw an error and stop retrying
271+
const chatCompletion = await openai.chat.completions.create({
272+
messages: [{ role: "user", content: payload.prompt }],
273+
model: "gpt-3.5-turbo",
274+
});
275+
276+
if (chatCompletion.choices[0]?.message.content === undefined) {
277+
// If OpenAI returns an empty response, abort retrying
278+
throw new AbortTaskRunError("OpenAI call failed");
279+
}
280+
281+
return chatCompletion.choices[0].message.content;
282+
},
283+
});
284+
```
285+
286+
### Using try/catch
259287

260288
Sometimes you want to catch an error and don't want to retry the task. You can use try/catch as you normally would. In this example we fallback to using Replicate if OpenAI fails.
261289

262-
```ts /trigger/
290+
```ts /trigger/myTasks.ts
263291
import { task } from "@trigger.dev/sdk/v3";
264292

265293
export const openaiTask = task({

0 commit comments

Comments
 (0)