Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8fd0a1112 |
@@ -9,6 +9,7 @@ import { useServerStatus } from '../../hooks/useServerStatus';
|
|||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||||
import { serverStatusAtom } from '../../state/serverStatus';
|
import { serverStatusAtom } from '../../state/serverStatus';
|
||||||
import { pickBanner, resolveStatusPage, StatusBanner } from '../../utils/kumaStatus';
|
import { pickBanner, resolveStatusPage, StatusBanner } from '../../utils/kumaStatus';
|
||||||
|
import { homeserverStalledAtom, useHomeserverStall } from '../../hooks/useHomeserverStall';
|
||||||
|
|
||||||
const DISMISS_KEY = 'lotus-status-banner-dismissed';
|
const DISMISS_KEY = 'lotus-status-banner-dismissed';
|
||||||
const DISMISS_MAX = 50;
|
const DISMISS_MAX = 50;
|
||||||
@@ -163,7 +164,12 @@ export function ServerStatusFeature({ mx }: { mx: MatrixClient }) {
|
|||||||
});
|
});
|
||||||
}, []),
|
}, []),
|
||||||
);
|
);
|
||||||
const syncLost = lostAt !== null;
|
// [Gitea #317] A hung server keeps sync "SYNCING" for up to 110 s; the stall
|
||||||
|
// watchdog flags it sooner, and Kuma is asked at once.
|
||||||
|
const stalled = useHomeserverStall(mx);
|
||||||
|
const setStalled = useSetAtom(homeserverStalledAtom);
|
||||||
|
useEffect(() => setStalled(stalled), [setStalled, stalled]);
|
||||||
|
const syncLost = lostAt !== null || stalled;
|
||||||
const status = useServerStatus(page, syncLost);
|
const status = useServerStatus(page, syncLost);
|
||||||
const setAtom = useSetAtom(serverStatusAtom);
|
const setAtom = useSetAtom(serverStatusAtom);
|
||||||
useEffect(() => setAtom({ status, syncLost, lostAt }), [setAtom, status, syncLost, lostAt]);
|
useEffect(() => setAtom({ status, syncLost, lostAt }), [setAtom, status, syncLost, lostAt]);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { SyncState } from 'matrix-js-sdk';
|
||||||
|
import { STALL_AFTER_MS, syncOverdue } from './useHomeserverStall';
|
||||||
|
|
||||||
|
test('syncOverdue: only while SYNCING and past the stall threshold (#317)', () => {
|
||||||
|
const now = 1_000_000;
|
||||||
|
assert.equal(syncOverdue(now - STALL_AFTER_MS - 1, now, SyncState.Syncing), true);
|
||||||
|
assert.equal(syncOverdue(now - 20_000, now, SyncState.Syncing), false, 'a normal long-poll');
|
||||||
|
assert.equal(
|
||||||
|
syncOverdue(now - 120_000, now, SyncState.Reconnecting),
|
||||||
|
false,
|
||||||
|
'SyncStatus already shows that',
|
||||||
|
);
|
||||||
|
assert.equal(syncOverdue(now - 120_000, now, null), false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { useSyncState } from '../../hooks/useSyncState';
|
|||||||
import { ContainerColor } from '../../styles/ContainerColor.css';
|
import { ContainerColor } from '../../styles/ContainerColor.css';
|
||||||
import { serverStatusAtom } from '../../state/serverStatus';
|
import { serverStatusAtom } from '../../state/serverStatus';
|
||||||
import { serverConfirmedUpSince } from '../../utils/kumaStatus';
|
import { serverConfirmedUpSince } from '../../utils/kumaStatus';
|
||||||
|
import { homeserverStalledAtom } from '../../hooks/useHomeserverStall';
|
||||||
|
|
||||||
type StateData = {
|
type StateData = {
|
||||||
current: SyncState | null;
|
current: SyncState | null;
|
||||||
@@ -17,6 +18,7 @@ type SyncStatusProps = {
|
|||||||
};
|
};
|
||||||
export function SyncStatus({ mx }: SyncStatusProps) {
|
export function SyncStatus({ mx }: SyncStatusProps) {
|
||||||
const serverStatus = useAtomValue(serverStatusAtom);
|
const serverStatus = useAtomValue(serverStatusAtom);
|
||||||
|
const stalled = useAtomValue(homeserverStalledAtom);
|
||||||
const [stateData, setStateData] = useState<StateData>({
|
const [stateData, setStateData] = useState<StateData>({
|
||||||
current: null,
|
current: null,
|
||||||
previous: undefined,
|
previous: undefined,
|
||||||
@@ -34,6 +36,35 @@ export function SyncStatus({ mx }: SyncStatusProps) {
|
|||||||
}, []),
|
}, []),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// [Gitea #124] When Kuma confirms the homeserver is down, the status banner
|
||||||
|
// says so ("Lotus Chat's server is down…") instead of "Connection Lost!".
|
||||||
|
const serverDown = serverStatus.status?.homeserver === 'down';
|
||||||
|
// Only blame the user's connection when Kuma has checked the server since
|
||||||
|
// the drop and it was fine (a stale "up" from before proves nothing).
|
||||||
|
const serverUp = serverConfirmedUpSince(serverStatus.status, serverStatus.lostAt);
|
||||||
|
|
||||||
|
// [Gitea #317] Sync still says SYNCING, but the server has stopped answering.
|
||||||
|
if (stalled && stateData.current === SyncState.Syncing && !serverDown) {
|
||||||
|
return (
|
||||||
|
<Box direction="Column" shrink="No">
|
||||||
|
<Box
|
||||||
|
className={ContainerColor({ variant: 'Warning' })}
|
||||||
|
style={{ padding: `${config.space.S100} 0` }}
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<Text size="L400">
|
||||||
|
Lotus Chat's server isn't responding. Messages will send when it's back.
|
||||||
|
Still trying…
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Line variant="Warning" size="300" />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(stateData.current === SyncState.Prepared ||
|
(stateData.current === SyncState.Prepared ||
|
||||||
stateData.current === SyncState.Syncing ||
|
stateData.current === SyncState.Syncing ||
|
||||||
@@ -57,13 +88,6 @@ export function SyncStatus({ mx }: SyncStatusProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// [Gitea #124] When Kuma confirms the homeserver is down, the status banner
|
|
||||||
// says so ("Lotus Chat's server is down…") instead of "Connection Lost!".
|
|
||||||
const serverDown = serverStatus.status?.homeserver === 'down';
|
|
||||||
// Only blame the user's connection when Kuma has checked the server since
|
|
||||||
// the drop and it was fine (a stale "up" from before proves nothing).
|
|
||||||
const serverUp = serverConfirmedUpSince(serverStatus.status, serverStatus.lostAt);
|
|
||||||
|
|
||||||
if (stateData.current === SyncState.Reconnecting && !serverDown) {
|
if (stateData.current === SyncState.Reconnecting && !serverDown) {
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" shrink="No">
|
<Box direction="Column" shrink="No">
|
||||||
|
|||||||
Reference in New Issue
Block a user