FN-5778: throttle OAuth expiry notifications per provider
Limit repeated oauth-token-expired alerts by enforcing a per-provider minimum notification interval. - add a 12-hour default min notify interval to OAuthExpiryMonitor and track last notification time by provider - skip dispatch when a provider was already notified within the configured window, even if expiry timestamps change - clear per-provider notification timestamps when no OAuth providers are configured - add regression coverage for 12-hour throttling and changed-expiry throttling behavior - add a patch changeset for @runfusion/fusion documenting the notification throttle Files changed: .changeset/fn-5778-oauth-notify-throttle.md | 5 +++ packages/engine/src/notification/__tests__/oauth-expiry-monitor.test.ts | 40 ++++++++++++++++++++++ packages/engine/src/notification/oauth-expiry-monitor.ts | 15 ++++++++ 3 files changed, 60 insertions(+) Fusion-Task-Id: FN-5778 Fusion-Task-Lineage: a9741101-e8da-4dae-ac7f-817e564b4f23
This commit is contained in:
@@ -121,6 +121,46 @@ describe("OAuthExpiryMonitor", () => {
|
||||
now += 2_000;
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
now += 12 * 60 * 60 * 1000;
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
monitor.stop();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("throttles changed expiries until min notify interval elapses", async () => {
|
||||
vi.useFakeTimers();
|
||||
let now = Date.now();
|
||||
const authStorage = createAuthStorage({ type: "oauth", expires: now - 1 });
|
||||
const dispatch = vi.fn(async () => undefined);
|
||||
|
||||
const monitor = new OAuthExpiryMonitor({
|
||||
authStorage,
|
||||
notificationService: { dispatch } as any,
|
||||
intervalMs: 100,
|
||||
minNotifyIntervalMs: 1_000,
|
||||
clock: () => now,
|
||||
});
|
||||
|
||||
await monitor.start();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
authStorage.credential = { type: "oauth", expires: now + 10_000 };
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
now += 500;
|
||||
authStorage.credential = { type: "oauth", expires: now - 1 };
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
now += 500;
|
||||
authStorage.credential = { type: "oauth", expires: now - 2 };
|
||||
await vi.advanceTimersByTimeAsync(100);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(2);
|
||||
monitor.stop();
|
||||
vi.useRealTimers();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { schedulerLog } from "../logger.js";
|
||||
import type { NotificationService } from "./notification-service.js";
|
||||
|
||||
const DEFAULT_INTERVAL_MS = 5 * 60_000;
|
||||
const DEFAULT_MIN_NOTIFY_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
||||
|
||||
interface OAuthProviderInfo {
|
||||
id: string;
|
||||
@@ -26,19 +27,23 @@ export interface OAuthExpiryMonitorOptions {
|
||||
intervalMs?: number;
|
||||
clock?: () => number;
|
||||
warnBeforeMs?: number;
|
||||
minNotifyIntervalMs?: number;
|
||||
}
|
||||
|
||||
export class OAuthExpiryMonitor {
|
||||
private readonly intervalMs: number;
|
||||
private readonly clock: () => number;
|
||||
private readonly warnBeforeMs: number;
|
||||
private readonly minNotifyIntervalMs: number;
|
||||
private timer: NodeJS.Timeout | null = null;
|
||||
private readonly dispatchedExpiryKeys = new Set<string>();
|
||||
private readonly lastNotifiedAt = new Map<string, number>();
|
||||
|
||||
constructor(private readonly opts: OAuthExpiryMonitorOptions) {
|
||||
this.intervalMs = opts.intervalMs ?? DEFAULT_INTERVAL_MS;
|
||||
this.clock = opts.clock ?? Date.now;
|
||||
this.warnBeforeMs = opts.warnBeforeMs ?? 0;
|
||||
this.minNotifyIntervalMs = opts.minNotifyIntervalMs ?? DEFAULT_MIN_NOTIFY_INTERVAL_MS;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
@@ -68,6 +73,7 @@ export class OAuthExpiryMonitor {
|
||||
const providers = this.opts.authStorage.getOAuthProviders?.();
|
||||
if (!providers?.length) {
|
||||
this.dispatchedExpiryKeys.clear();
|
||||
this.lastNotifiedAt.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,6 +96,14 @@ export class OAuthExpiryMonitor {
|
||||
continue;
|
||||
}
|
||||
|
||||
const previousNotificationAt = this.lastNotifiedAt.get(provider.id);
|
||||
if (
|
||||
typeof previousNotificationAt === "number" &&
|
||||
now - previousNotificationAt < this.minNotifyIntervalMs
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const payload: NotificationPayload = {
|
||||
event: "oauth-token-expired",
|
||||
metadata: {
|
||||
@@ -102,6 +116,7 @@ export class OAuthExpiryMonitor {
|
||||
try {
|
||||
await this.opts.notificationService.dispatch("oauth-token-expired", payload);
|
||||
this.dispatchedExpiryKeys.add(expiryKey);
|
||||
this.lastNotifiedAt.set(provider.id, now);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
schedulerLog.warn(`OAuth expiry notification dispatch failed provider=${provider.id}: ${message}`);
|
||||
|
||||
Reference in New Issue
Block a user