CI / Build & Quality Checks (pull_request) Successful in 2m40s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 8s
CI / Playwright smoke (e2e) (pull_request) Successful in 17m50s
A hung Synapse (accepts connections, never answers) kept sync SYNCING for up to 110 s (pollTimeout 30 s + the SDK's 80 s buffer), so no strip showed and Kuma was never asked. A watchdog now notices a /sync overdue by 45 s, confirms with a 5 s GET /_matrix/client/versions, then shows "Lotus Chat's server isn't responding…" and polls Kuma at once. Fixes #317 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
58 lines
2.1 KiB
TypeScript
58 lines
2.1 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { atom } from 'jotai';
|
|
import { ClientEvent, MatrixClient, SyncState } from 'matrix-js-sdk';
|
|
|
|
/**
|
|
* [Gitea #317] A hung homeserver (accepts connections, never answers) went
|
|
* unnoticed for up to 110 s: the SDK only gives up on a /sync long-poll after
|
|
* pollTimeout (30 s) + an 80 s buffer, and until then the sync state stays
|
|
* SYNCING, so no strip showed and Kuma wasn't asked. This watchdog notices a
|
|
* /sync that's overdue and confirms with a short probe of /versions.
|
|
*/
|
|
export const STALL_AFTER_MS = 45_000; // a healthy long-poll returns within ~30 s
|
|
const CHECK_EVERY_MS = 10_000;
|
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
|
|
export const homeserverStalledAtom = atom(false);
|
|
|
|
/** Pure: should we probe, given the last sync response time? */
|
|
export const syncOverdue = (lastSyncAt: number, now: number, state: SyncState | null): boolean =>
|
|
state === SyncState.Syncing && now - lastSyncAt > STALL_AFTER_MS;
|
|
|
|
export function useHomeserverStall(mx: MatrixClient): boolean {
|
|
const [stalled, setStalled] = useState(false);
|
|
useEffect(() => {
|
|
let lastSyncAt = Date.now();
|
|
let probing = false;
|
|
const onSync = () => {
|
|
lastSyncAt = Date.now();
|
|
setStalled(false);
|
|
};
|
|
const check = async () => {
|
|
if (probing || !syncOverdue(lastSyncAt, Date.now(), mx.getSyncState())) return;
|
|
probing = true;
|
|
let answered = false;
|
|
try {
|
|
const res = await fetch(`${mx.getHomeserverUrl()}/_matrix/client/versions`, {
|
|
cache: 'no-store',
|
|
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
});
|
|
answered = res.ok;
|
|
} catch {
|
|
answered = false;
|
|
}
|
|
probing = false;
|
|
// Still overdue and the server didn't answer a trivial request: stalled.
|
|
if (!answered && syncOverdue(lastSyncAt, Date.now(), mx.getSyncState())) setStalled(true);
|
|
if (answered) setStalled(false);
|
|
};
|
|
mx.on(ClientEvent.Sync, onSync);
|
|
const id = setInterval(check, CHECK_EVERY_MS);
|
|
return () => {
|
|
mx.removeListener(ClientEvent.Sync, onSync);
|
|
clearInterval(id);
|
|
};
|
|
}, [mx]);
|
|
return stalled;
|
|
}
|