fix(app): jitter gateway reconnects and recover status nagbar (#2439)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-03 23:21:32 +02:00
1 parent b0645300ec
commit cc3a9c8613
3 files changed
+24 -13

No files matched your search

@@ -86,6 +86,12 @@ export function useConnectionNotice(): ConnectionNotice | null {
}, BOOT_NOTICE_DELAY_MS); }, BOOT_NOTICE_DELAY_MS);
return () => window.clearTimeout(timer); return () => window.clearTimeout(timer);
}, [booting]); }, [booting]);
useEffect(() => {
if (!reconnecting) {
return;
}
StatusPage.refreshForConnectionIssue();
}, [reconnecting]);
if (Nagbar.forceHideConnectionNotice) { if (Nagbar.forceHideConnectionNotice) {
return null; return null;
} }
@@ -32,6 +32,7 @@ const GATEWAY_TIMEOUTS = {
ResumeWindow: 180000, ResumeWindow: 180000,
MinReconnect: 1000, MinReconnect: 1000,
MaxReconnect: 60000, MaxReconnect: 60000,
ReconnectSpread: 2000,
Hello: 20000, Hello: 20000,
} as const; } as const;
export const GatewayState = { export const GatewayState = {
@@ -1034,7 +1035,7 @@ export class GatewaySocket extends EventEmitter<GatewaySocketEvents> {
} }
const allowImmediate = options.allowImmediate ?? true; const allowImmediate = options.allowImmediate ?? true;
const wasImmediate = allowImmediate && this.shouldReconnectImmediately; const wasImmediate = allowImmediate && this.shouldReconnectImmediately;
const delay = wasImmediate ? 0 : this.nextReconnectDelay(); const delay = wasImmediate ? this.reconnectSpread() : this.nextReconnectDelay();
this.shouldReconnectImmediately = false; this.shouldReconnectImmediately = false;
this.log.info(`Scheduling reconnect in ${delay}ms${wasImmediate ? ' (immediate)' : ''}`); this.log.info(`Scheduling reconnect in ${delay}ms${wasImmediate ? ' (immediate)' : ''}`);
this.reconnectTimeoutId = window.setTimeout(() => { this.reconnectTimeoutId = window.setTimeout(() => {
@@ -1047,12 +1048,16 @@ export class GatewaySocket extends EventEmitter<GatewaySocketEvents> {
}, delay); }, delay);
} }
private reconnectSpread(): number {
return Math.floor(Math.random() * GATEWAY_TIMEOUTS.ReconnectSpread);
}
private nextReconnectDelay(): number { private nextReconnectDelay(): number {
const now = Date.now(); const now = Date.now();
const elapsed = now - this.lastReconnectAt; const elapsed = now - this.lastReconnectAt;
if (elapsed < GATEWAY_TIMEOUTS.MinReconnect) { if (elapsed < GATEWAY_TIMEOUTS.MinReconnect) {
this.log.debug(`Last reconnect ${elapsed}ms ago, enforcing minimum delay (${GATEWAY_TIMEOUTS.MinReconnect}ms)`); this.log.debug(`Last reconnect ${elapsed}ms ago, enforcing minimum delay (${GATEWAY_TIMEOUTS.MinReconnect}ms)`);
return GATEWAY_TIMEOUTS.MinReconnect; return GATEWAY_TIMEOUTS.MinReconnect + this.reconnectSpread();
} }
this.lastReconnectAt = now; this.lastReconnectAt = now;
const delay = this.reconnectBackoff.next(); const delay = this.reconnectBackoff.next();
@@ -90,7 +90,11 @@ const POLL_ACTIVE_MAINTENANCE_JITTER_MS = 15 * 1000;
const POLL_AFTER_MAINTENANCE_START_MS = 5 * 1000; const POLL_AFTER_MAINTENANCE_START_MS = 5 * 1000;
const POLL_MIN_DELAY_MS = 10 * 1000; const POLL_MIN_DELAY_MS = 10 * 1000;
const POLL_RESUME_STALE_MS = 30 * 1000; const POLL_RESUME_STALE_MS = 30 * 1000;
const STATUS_PAGE_FETCH_OPTIONS: RequestInit = {cache: 'no-store'}; const STATUS_PAGE_FETCH_TIMEOUT_MS = 10 * 1000;
function statusPageFetchOptions(): RequestInit {
return {cache: 'no-store', signal: AbortSignal.timeout(STATUS_PAGE_FETCH_TIMEOUT_MS)};
}
export function computePollDelay(scheduledMaintenance: StatusPageMaintenance | null): number { export function computePollDelay(scheduledMaintenance: StatusPageMaintenance | null): number {
if (scheduledMaintenance?.status === 'in_progress') { if (scheduledMaintenance?.status === 'in_progress') {
@@ -256,12 +260,8 @@ export class StatusPage {
private async fetchIncidents(): Promise<void> { private async fetchIncidents(): Promise<void> {
try { try {
const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/summary.json`, STATUS_PAGE_FETCH_OPTIONS); const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/summary.json`, statusPageFetchOptions());
if (!response.ok) { if (!response.ok) {
runInAction(() => {
this.incident = null;
this.scheduledMaintenance = null;
});
return; return;
} }
const data: InstatusSummary = await response.json(); const data: InstatusSummary = await response.json();
@@ -296,16 +296,12 @@ export class StatusPage {
}); });
} catch { } catch {
logger.warn('Failed to fetch status page'); logger.warn('Failed to fetch status page');
runInAction(() => {
this.incident = null;
this.scheduledMaintenance = null;
});
} }
} }
private async fetchComponentMaintenances(): Promise<Array<InstatusMaintenance>> { private async fetchComponentMaintenances(): Promise<Array<InstatusMaintenance>> {
try { try {
const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/components.json`, STATUS_PAGE_FETCH_OPTIONS); const response = await fetch(`${ExternalUrls.SERVICE_STATUS}/components.json`, statusPageFetchOptions());
if (!response.ok) { if (!response.ok) {
return []; return [];
} }
@@ -321,6 +317,10 @@ export class StatusPage {
this.incident = null; this.incident = null;
} }
refreshForConnectionIssue(): void {
this.refreshIfStale();
}
private async refreshAndReschedule(): Promise<void> { private async refreshAndReschedule(): Promise<void> {
try { try {
await this.checkIncidents(); await this.checkIncidents();