Skip to content

Commit 5cb4511

Browse files
authored
Sync delay (#1707)
# READ CAREFULLY THEN REMOVE Remove bullet points that are not relevant. PLEASE REFRAIN FROM USING AI TO WRITE YOUR CODE AND PR DESCRIPTION. IF YOU DO USE AI TO WRITE YOUR CODE PLEASE PROVIDE A DESCRIPTION AND REVIEW IT CAREFULLY. MAKE SURE YOU UNDERSTAND THE CODE YOU ARE SUBMITTING USING AI. - Pull requests that do not follow these guidelines will be closed without review or comment. - If you use AI to write your PR description your pr will be close without review or comment. - If you are unsure about anything, feel free to ask for clarification. ## Description Please provide a clear description of your changes. --- ## Type of Change Please delete options that are not relevant. - [ ] 🐛 Bug fix (non-breaking change which fixes an issue) - [ ] ✨ New feature (non-breaking change which adds functionality) - [ ] 💥 Breaking change (fix or feature with breaking changes) - [ ] 📝 Documentation update - [ ] 🎨 UI/UX improvement - [ ] 🔒 Security enhancement - [ ] ⚡ Performance improvement ## Areas Affected Please check all that apply: - [ ] Email Integration (Gmail, IMAP, etc.) - [ ] User Interface/Experience - [ ] Authentication/Authorization - [ ] Data Storage/Management - [ ] API Endpoints - [ ] Documentation - [ ] Testing Infrastructure - [ ] Development Workflow - [ ] Deployment/Infrastructure ## Testing Done Describe the tests you've done: - [ ] Unit tests added/updated - [ ] Integration tests added/updated - [ ] Manual testing performed - [ ] Cross-browser testing (if UI changes) - [ ] Mobile responsiveness verified (if UI changes) ## Security Considerations For changes involving data or authentication: - [ ] No sensitive data is exposed - [ ] Authentication checks are in place - [ ] Input validation is implemented - [ ] Rate limiting is considered (if applicable) ## Checklist - [ ] I have read the [CONTRIBUTING](https://github.com/Mail-0/Zero/blob/staging/.github/CONTRIBUTING.md) document - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in complex areas - [ ] I have updated the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix/feature works - [ ] All tests pass locally - [ ] Any dependent changes are merged and published ## Additional Notes Add any other context about the pull request here. ## Screenshots/Recordings Add screenshots or recordings here if applicable. --- _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._ <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Added automatic retry logic for Gmail API rate limit errors to reduce sync failures and improve reliability. - **Bug Fixes** - Detects Gmail rate limit errors and retries failed requests up to 10 times with a 60-second delay between attempts. <!-- End of auto-generated description by cubic. -->
1 parent c357fbf commit 5cb4511

File tree

2 files changed

+67
-2
lines changed

2 files changed

+67
-2
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Effect, Duration, Schedule } from 'effect';
2+
3+
/**
4+
* Gmail signals per-user quota problems in two ways:
5+
* – HTTP 429 Too Many Requests
6+
* – HTTP 403 with reason == userRateLimitExceeded or quotaExceeded
7+
*/
8+
export function isGmailRateLimit(err: unknown): boolean {
9+
const e: any = err || {};
10+
const status = e.code ?? e.status ?? e.response?.status;
11+
12+
if (status === 429) return true;
13+
if (status === 403) {
14+
const errors = e.errors ??
15+
e.response?.data?.error?.errors ??
16+
[];
17+
return errors.some((x: any) =>
18+
['userRateLimitExceeded', 'rateLimitExceeded', 'quotaExceeded',
19+
'dailyLimitExceeded', 'backendError', 'limitExceeded'].includes(x.reason)
20+
);
21+
}
22+
return false;
23+
}
24+
25+
/**
26+
* A schedule that:
27+
* – retries while the error *is* a rate-limit error (max 10 attempts)
28+
* – waits 60 seconds between retries (conservative for Gmail user quotas)
29+
* – stops immediately for any other error
30+
*/
31+
export const gmailRateLimitSchedule = Schedule
32+
.recurWhile(isGmailRateLimit)
33+
.pipe(Schedule.intersect(Schedule.recurs(10))) // max 10 attempts
34+
.pipe(Schedule.addDelay(() => Duration.seconds(60))); // 60s delay between retries
35+
36+
/**
37+
* Generic wrapper that applies the schedule
38+
*/
39+
export function withGmailRetry<A>(
40+
eff: Effect.Effect<A, unknown, never>,
41+
): Effect.Effect<A, unknown, never> {
42+
return eff.pipe(Effect.retry(gmailRateLimitSchedule));
43+
}

apps/server/src/routes/chat.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import { McpAgent } from 'agents/mcp';
4747

4848
import { createDb } from '../db';
4949
import { z } from 'zod';
50+
import { Effect } from 'effect';
51+
import { withGmailRetry } from '../lib/gmail-rate-limit';
5052

5153
const decoder = new TextDecoder();
5254

@@ -881,7 +883,7 @@ export class ZeroAgent extends AIChatAgent<typeof env> {
881883
this.syncThreadsInProgress.set(threadId, true);
882884

883885
try {
884-
const threadData = await this.driver.get(threadId);
886+
const threadData = await this.getWithRetry(threadId);
885887
const latest = threadData.latest;
886888

887889
if (latest) {
@@ -935,6 +937,26 @@ export class ZeroAgent extends AIChatAgent<typeof env> {
935937
return `${this.name}/${threadId}.json`;
936938
}
937939

940+
private async listWithRetry(params: Parameters<MailManager['list']>[0]) {
941+
if (!this.driver) throw new Error('No driver available');
942+
943+
return Effect.runPromise(
944+
withGmailRetry(
945+
Effect.tryPromise(() => this.driver!.list(params))
946+
),
947+
);
948+
}
949+
950+
private async getWithRetry(threadId: string): Promise<IGetThreadResponse> {
951+
if (!this.driver) throw new Error('No driver available');
952+
953+
return Effect.runPromise(
954+
withGmailRetry(
955+
Effect.tryPromise(() => this.driver!.get(threadId))
956+
),
957+
);
958+
}
959+
938960
async syncThreads(folder: string) {
939961
if (!this.driver) {
940962
console.error('No driver available for syncThreads');
@@ -963,7 +985,7 @@ export class ZeroAgent extends AIChatAgent<typeof env> {
963985
while (hasMore) {
964986
_pageCount++;
965987

966-
const result = await this.driver.list({
988+
const result = await this.listWithRetry({
967989
folder,
968990
maxResults: maxCount,
969991
pageToken: pageToken || undefined,

0 commit comments

Comments
 (0)