mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 23:25:24 +00:00
fix: only show connection lost when the server check fails too
This commit is contained in:
1 parent
c759a147c6
commit
f37f8511fa
6 files changed
+126
-9
No files matched your search
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## 26.10.7
|
||||
|
||||
### Fixed
|
||||
|
||||
- One slow request, like a plugin waiting on an SSH host, no longer shows Server connection lost. The app now checks the server first
|
||||
- Canceled requests no longer show Server connection lost
|
||||
|
||||
## 26.10.6
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "termix",
|
||||
"version": "26.10.6",
|
||||
"version": "26.10.7",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "termix",
|
||||
"version": "26.10.6",
|
||||
"version": "26.10.7",
|
||||
"hasInstallScript": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "26.10.6",
|
||||
"version": "26.10.7",
|
||||
"description": "Self-hosted, plugin-based server management.",
|
||||
"author": "Karmaa",
|
||||
"main": "electron/main.cjs",
|
||||
|
||||
@@ -31,6 +31,8 @@ class DatabaseHealthMonitor {
|
||||
private static instance: DatabaseHealthMonitor;
|
||||
private listeners: Map<string, EventListener[]> = new Map();
|
||||
private degradedActive: boolean = false;
|
||||
private probe: (() => Promise<boolean>) | null = null;
|
||||
private probing: Promise<void> | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -83,22 +85,48 @@ class DatabaseHealthMonitor {
|
||||
errorCode === "DATABASE_ERROR" ||
|
||||
errorCode === "DB_CONNECTION_FAILED";
|
||||
|
||||
// A cancel is on purpose and says nothing about the backend.
|
||||
if (errorCode === "ERR_CANCELED" || lowerMessage === "canceled") return;
|
||||
|
||||
const isBackendUnreachable =
|
||||
errorCode === "ERR_NETWORK" ||
|
||||
errorCode === "ECONNREFUSED" ||
|
||||
errorCode === "ECONNABORTED" ||
|
||||
errorCode === "ECONNRESET" ||
|
||||
errorCode === "ETIMEDOUT" ||
|
||||
errorCode === "ERR_CANCELED" ||
|
||||
(lowerMessage.includes("network error") &&
|
||||
errorLike.response === undefined) ||
|
||||
errorLike.response === undefined);
|
||||
|
||||
// One slow request (like a plugin waiting on an SSH host) is not a lost
|
||||
// backend, so a timeout only counts when a quick check fails too.
|
||||
const isTimeout =
|
||||
errorCode === "ECONNABORTED" ||
|
||||
errorCode === "ETIMEDOUT" ||
|
||||
lowerMessage.includes("request aborted") ||
|
||||
lowerMessage.includes("timeout");
|
||||
|
||||
if (!(isDatabaseError || isBackendUnreachable)) {
|
||||
return;
|
||||
if (isDatabaseError || isBackendUnreachable) {
|
||||
this.markDegraded(errorMessage, errorCode);
|
||||
} else if (isTimeout) {
|
||||
if (!this.probe) {
|
||||
this.markDegraded(errorMessage, errorCode);
|
||||
return;
|
||||
}
|
||||
this.probing ??= this.probe()
|
||||
.catch(() => false)
|
||||
.then((reachable) => {
|
||||
if (!reachable) this.markDegraded(errorMessage, errorCode);
|
||||
})
|
||||
.finally(() => {
|
||||
this.probing = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Sets the quick check used after a request times out. */
|
||||
setReachabilityProbe(probe: (() => Promise<boolean>) | null) {
|
||||
this.probe = probe;
|
||||
}
|
||||
|
||||
private markDegraded(errorMessage: string, errorCode: string | undefined) {
|
||||
if (!this.degradedActive) {
|
||||
this.degradedActive = true;
|
||||
this.emit("database-connection-degraded", {
|
||||
|
||||
@@ -676,6 +676,18 @@ function initializeApiInstances() {
|
||||
|
||||
// Authentication API (port 30001)
|
||||
authApi = createApiInstance(getApiUrl("", 30001), "AUTH");
|
||||
const healthApi = authApi;
|
||||
dbHealthMonitor.setReachabilityProbe(() =>
|
||||
healthApi
|
||||
.get("/health", {
|
||||
timeout: 5000,
|
||||
__silentRetry: true,
|
||||
} as AxiosRequestConfigExtended)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
),
|
||||
);
|
||||
|
||||
// RBAC API (port 30001)
|
||||
rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC");
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { dbHealthMonitor } from "@/lib/db-health-monitor";
|
||||
|
||||
function watch() {
|
||||
const degraded = vi.fn<(...args: unknown[]) => void>();
|
||||
dbHealthMonitor.on("database-connection-degraded", degraded);
|
||||
return degraded;
|
||||
}
|
||||
|
||||
describe("dbHealthMonitor", () => {
|
||||
let degraded: ReturnType<typeof watch>;
|
||||
|
||||
afterEach(() => {
|
||||
dbHealthMonitor.off("database-connection-degraded", degraded);
|
||||
dbHealthMonitor.setReachabilityProbe(null);
|
||||
dbHealthMonitor.reset();
|
||||
});
|
||||
|
||||
it("flags a network error at once", () => {
|
||||
degraded = watch();
|
||||
dbHealthMonitor.reportDatabaseError({ code: "ERR_NETWORK" });
|
||||
expect(degraded).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("ignores canceled requests", () => {
|
||||
degraded = watch();
|
||||
dbHealthMonitor.reportDatabaseError({
|
||||
code: "ERR_CANCELED",
|
||||
message: "canceled",
|
||||
});
|
||||
expect(degraded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not flag a timeout when the backend answers", async () => {
|
||||
degraded = watch();
|
||||
const probe = vi.fn(async () => true);
|
||||
dbHealthMonitor.setReachabilityProbe(probe);
|
||||
dbHealthMonitor.reportDatabaseError({
|
||||
code: "ECONNABORTED",
|
||||
message: "timeout of 30000ms exceeded",
|
||||
});
|
||||
await vi.waitFor(() => expect(probe).toHaveBeenCalled());
|
||||
await Promise.resolve();
|
||||
expect(degraded).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("flags a timeout when the check fails too", async () => {
|
||||
degraded = watch();
|
||||
dbHealthMonitor.setReachabilityProbe(async () => false);
|
||||
dbHealthMonitor.reportDatabaseError({
|
||||
code: "ECONNABORTED",
|
||||
message: "timeout of 30000ms exceeded",
|
||||
});
|
||||
await vi.waitFor(() => expect(degraded).toHaveBeenCalledTimes(1));
|
||||
});
|
||||
|
||||
it("runs one check for timeouts at the same time", async () => {
|
||||
degraded = watch();
|
||||
let answer: (ok: boolean) => void = () => {};
|
||||
const probe = vi.fn(
|
||||
() => new Promise<boolean>((resolve) => (answer = resolve)),
|
||||
);
|
||||
dbHealthMonitor.setReachabilityProbe(probe);
|
||||
dbHealthMonitor.reportDatabaseError({ code: "ECONNABORTED" });
|
||||
dbHealthMonitor.reportDatabaseError({ code: "ECONNABORTED" });
|
||||
answer(true);
|
||||
await vi.waitFor(() => expect(probe).toHaveBeenCalledTimes(1));
|
||||
expect(degraded).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user