mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-26 23:02:57 +08:00
feat(server): improve calendar sync queue (#14783)
#### PR Dependency Tree * **PR #14783** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Configurable request timeout for calendar integrations. * Calendar polling now enqueues per-subscription sync jobs (larger batch) for improved throughput. * **Bug Fixes / Improvements** * Persisted next-sync timestamps and retry counts for more reliable scheduling and retry behavior. * Exponential backoff and webhook renewal now update scheduling consistently. * **Refactor** * Calendar sync flow moved to a job-queue-driven design for better concurrency and observability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,72 +1,33 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { chunk } from 'lodash-es';
|
||||
|
||||
import { Mutex } from '../../base';
|
||||
import { JobQueue } from '../../base';
|
||||
import { Models } from '../../models';
|
||||
import { CalendarService } from './service';
|
||||
|
||||
const CALENDAR_POLL_LOCK_KEY = 'calendar:poll-accounts';
|
||||
const CALENDAR_POLL_BATCH_SIZE = 10;
|
||||
const CALENDAR_POLL_BATCH_SIZE = 200;
|
||||
|
||||
@Injectable()
|
||||
export class CalendarCronJobs {
|
||||
constructor(
|
||||
private readonly models: Models,
|
||||
private readonly calendar: CalendarService,
|
||||
private readonly mutex: Mutex
|
||||
private readonly queue: JobQueue
|
||||
) {}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async pollAccounts() {
|
||||
await using lock = await this.mutex.acquire(CALENDAR_POLL_LOCK_KEY);
|
||||
if (!lock) return;
|
||||
const subscriptions = await this.models.calendarSubscription.listDueForSync(
|
||||
new Date(),
|
||||
CALENDAR_POLL_BATCH_SIZE
|
||||
);
|
||||
|
||||
const subscriptions =
|
||||
await this.models.calendarSubscription.listAllWithAccountForSync();
|
||||
|
||||
const accountDueAt = new Map<
|
||||
string,
|
||||
{ refreshInterval: number; lastSyncAt: Date | null }
|
||||
>();
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
const interval = subscription.account.refreshIntervalMinutes ?? 60;
|
||||
const lastSyncAt = subscription.lastSyncAt ?? null;
|
||||
const existing = accountDueAt.get(subscription.accountId);
|
||||
if (!existing) {
|
||||
accountDueAt.set(subscription.accountId, {
|
||||
refreshInterval: interval,
|
||||
lastSyncAt,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const earliest =
|
||||
existing.lastSyncAt && lastSyncAt
|
||||
? existing.lastSyncAt < lastSyncAt
|
||||
? existing.lastSyncAt
|
||||
: lastSyncAt
|
||||
: (existing.lastSyncAt ?? lastSyncAt);
|
||||
accountDueAt.set(subscription.accountId, {
|
||||
refreshInterval: interval,
|
||||
lastSyncAt: earliest,
|
||||
});
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const dueAccountIds = Array.from(accountDueAt.entries())
|
||||
.filter(
|
||||
([, info]) =>
|
||||
!info.lastSyncAt ||
|
||||
now - info.lastSyncAt.getTime() >= info.refreshInterval * 60 * 1000
|
||||
await Promise.allSettled(
|
||||
subscriptions.map(({ id }) =>
|
||||
this.queue.add(
|
||||
'calendar.syncSubscription',
|
||||
{ subscriptionId: id, reason: 'polling' },
|
||||
{ jobId: id }
|
||||
)
|
||||
)
|
||||
.map(([accountId]) => accountId);
|
||||
|
||||
for (const accountIds of chunk(dueAccountIds, CALENDAR_POLL_BATCH_SIZE)) {
|
||||
await Promise.allSettled(
|
||||
accountIds.map(accountId => this.calendar.syncAccount(accountId))
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user