fix(add-gmail): graceful startup when credentials missing + poll backoff
- connect() warns and returns instead of throwing when ~/.gmail-mcp/ credentials are missing, preventing app crash - index.ts wraps gmail.connect() in try/catch as a safety net - Poll loop uses exponential backoff on consecutive errors (caps at 30m) instead of hammering the Gmail API every 60s on auth failures - Switch from setInterval to setTimeout chain for proper backoff timing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -34,9 +34,10 @@ export class GmailChannel implements Channel {
|
|||||||
private gmail: gmail_v1.Gmail | null = null;
|
private gmail: gmail_v1.Gmail | null = null;
|
||||||
private opts: GmailChannelOpts;
|
private opts: GmailChannelOpts;
|
||||||
private pollIntervalMs: number;
|
private pollIntervalMs: number;
|
||||||
private pollTimer: ReturnType<typeof setInterval> | null = null;
|
private pollTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
private processedIds = new Set<string>();
|
private processedIds = new Set<string>();
|
||||||
private threadMeta = new Map<string, ThreadMeta>();
|
private threadMeta = new Map<string, ThreadMeta>();
|
||||||
|
private consecutiveErrors = 0;
|
||||||
private userEmail = '';
|
private userEmail = '';
|
||||||
|
|
||||||
constructor(opts: GmailChannelOpts, pollIntervalMs = 60000) {
|
constructor(opts: GmailChannelOpts, pollIntervalMs = 60000) {
|
||||||
@@ -50,9 +51,10 @@ export class GmailChannel implements Channel {
|
|||||||
const tokensPath = path.join(credDir, 'credentials.json');
|
const tokensPath = path.join(credDir, 'credentials.json');
|
||||||
|
|
||||||
if (!fs.existsSync(keysPath) || !fs.existsSync(tokensPath)) {
|
if (!fs.existsSync(keysPath) || !fs.existsSync(tokensPath)) {
|
||||||
throw new Error(
|
logger.warn(
|
||||||
'Gmail credentials not found in ~/.gmail-mcp/. Run Gmail OAuth setup first.',
|
'Gmail credentials not found in ~/.gmail-mcp/. Skipping Gmail channel. Run /add-gmail to set up.',
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = JSON.parse(fs.readFileSync(keysPath, 'utf-8'));
|
const keys = JSON.parse(fs.readFileSync(keysPath, 'utf-8'));
|
||||||
@@ -86,17 +88,23 @@ export class GmailChannel implements Channel {
|
|||||||
this.userEmail = profile.data.emailAddress || '';
|
this.userEmail = profile.data.emailAddress || '';
|
||||||
logger.info({ email: this.userEmail }, 'Gmail channel connected');
|
logger.info({ email: this.userEmail }, 'Gmail channel connected');
|
||||||
|
|
||||||
// Start polling
|
// Start polling with error backoff
|
||||||
this.pollTimer = setInterval(
|
const schedulePoll = () => {
|
||||||
() =>
|
const backoffMs = this.consecutiveErrors > 0
|
||||||
this.pollForMessages().catch((err) =>
|
? Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveErrors), 30 * 60 * 1000)
|
||||||
logger.error({ err }, 'Gmail poll error'),
|
: this.pollIntervalMs;
|
||||||
),
|
this.pollTimer = setTimeout(() => {
|
||||||
this.pollIntervalMs,
|
this.pollForMessages()
|
||||||
);
|
.catch((err) => logger.error({ err }, 'Gmail poll error'))
|
||||||
|
.finally(() => {
|
||||||
|
if (this.gmail) schedulePoll();
|
||||||
|
});
|
||||||
|
}, backoffMs);
|
||||||
|
};
|
||||||
|
|
||||||
// Initial poll
|
// Initial poll
|
||||||
await this.pollForMessages();
|
await this.pollForMessages();
|
||||||
|
schedulePoll();
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(jid: string, text: string): Promise<void> {
|
async sendMessage(jid: string, text: string): Promise<void> {
|
||||||
@@ -158,7 +166,7 @@ export class GmailChannel implements Channel {
|
|||||||
|
|
||||||
async disconnect(): Promise<void> {
|
async disconnect(): Promise<void> {
|
||||||
if (this.pollTimer) {
|
if (this.pollTimer) {
|
||||||
clearInterval(this.pollTimer);
|
clearTimeout(this.pollTimer);
|
||||||
this.pollTimer = null;
|
this.pollTimer = null;
|
||||||
}
|
}
|
||||||
this.gmail = null;
|
this.gmail = null;
|
||||||
@@ -197,8 +205,12 @@ export class GmailChannel implements Channel {
|
|||||||
const ids = [...this.processedIds];
|
const ids = [...this.processedIds];
|
||||||
this.processedIds = new Set(ids.slice(ids.length - 2500));
|
this.processedIds = new Set(ids.slice(ids.length - 2500));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.consecutiveErrors = 0;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ err }, 'Gmail poll failed');
|
this.consecutiveErrors++;
|
||||||
|
const backoffMs = Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveErrors), 30 * 60 * 1000);
|
||||||
|
logger.error({ err, consecutiveErrors: this.consecutiveErrors, nextPollMs: backoffMs }, 'Gmail poll failed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -452,7 +452,11 @@ async function main(): Promise<void> {
|
|||||||
|
|
||||||
const gmail = new GmailChannel(channelOpts);
|
const gmail = new GmailChannel(channelOpts);
|
||||||
channels.push(gmail);
|
channels.push(gmail);
|
||||||
await gmail.connect();
|
try {
|
||||||
|
await gmail.connect();
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn({ err }, 'Gmail channel failed to connect, continuing without it');
|
||||||
|
}
|
||||||
|
|
||||||
// Start subsystems (independently of connection handler)
|
// Start subsystems (independently of connection handler)
|
||||||
startSchedulerLoop({
|
startSchedulerLoop({
|
||||||
|
|||||||
Reference in New Issue
Block a user