fix(connection): stop background search auto-reconnect
This commit is contained in:
1 parent
a9ec07e651
commit
2b26bd6a1a
2 files changed
+100
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||
import { runSidebarSearchTasks } from "@/components/sidebar/sidebarSearchTaskRunner";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
@@ -93,6 +94,99 @@ describe("connectionStore timeout recovery", () => {
|
||||
expect(store.connectedIds.has(connection.id)).toBe(true);
|
||||
});
|
||||
|
||||
it.each(["failure", "success", "timeout"] as const)("ignores an older health check %s after disconnect", async (outcome) => {
|
||||
let resolveHealth!: () => void;
|
||||
let rejectHealth!: (error: Error) => void;
|
||||
const checkConnectionHealth = vi.fn(
|
||||
() =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
resolveHealth = resolve;
|
||||
rejectHealth = reject;
|
||||
}),
|
||||
);
|
||||
const connectDb = vi.fn().mockResolvedValue("pg-1");
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth,
|
||||
connectDb,
|
||||
disconnectDb: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
const { useConnectionStore, CONNECTION_ATTEMPT_CANCELLED_MESSAGE } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = postgresConnection();
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
const ensure = store.ensureConnected(connection.id).catch((error) => error);
|
||||
await store.disconnect(connection.id);
|
||||
if (outcome === "failure") rejectHealth(new Error("pool closed"));
|
||||
else if (outcome === "success") resolveHealth();
|
||||
else await vi.advanceTimersByTimeAsync(5001);
|
||||
expect(await ensure).toEqual(new Error(CONNECTION_ATTEMPT_CANCELLED_MESSAGE));
|
||||
expect(connectDb).not.toHaveBeenCalled();
|
||||
expect(store.connectedIds.has(connection.id)).toBe(false);
|
||||
// An explicit reconnect still works after the stale probe has been discarded.
|
||||
await store.ensureConnected(connection.id);
|
||||
expect(connectDb).toHaveBeenCalledTimes(1);
|
||||
expect(store.connectedIds.has(connection.id)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not reconnect for queued sidebar object loads after disconnect", async () => {
|
||||
let finishTables!: (tables: []) => void;
|
||||
const listTables = vi.fn(
|
||||
() =>
|
||||
new Promise<[]>((resolve) => {
|
||||
finishTables = resolve;
|
||||
}),
|
||||
);
|
||||
const connectDb = vi.fn().mockResolvedValue("pg-1");
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
connectDb,
|
||||
disconnectDb: vi.fn().mockResolvedValue(undefined),
|
||||
listTables,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = postgresConnection();
|
||||
const groups: TreeNode[] = ["TABLE", "VIEW"].map((kind) => ({
|
||||
id: `${connection.id}:app:public:__${kind.toLowerCase()}s`,
|
||||
label: kind,
|
||||
type: kind === "TABLE" ? "group-tables" : "group-views",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
schema: "public",
|
||||
children: [],
|
||||
}));
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: groups }];
|
||||
const queued = runSidebarSearchTasks(
|
||||
groups.map((node) => () => store.loadObjectGroupChildren(node, { force: true })),
|
||||
1,
|
||||
);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(listTables).toHaveBeenCalledTimes(1);
|
||||
await store.disconnect(connection.id);
|
||||
finishTables([]);
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
expect(connectDb).not.toHaveBeenCalled();
|
||||
await queued;
|
||||
expect(listTables).toHaveBeenCalledTimes(1);
|
||||
expect(store.connectedIds.has(connection.id)).toBe(false);
|
||||
expect(store.treeNodes[0].children).toEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes missing keepalive interval to 30 seconds", async () => {
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
|
||||
@@ -4093,12 +4093,16 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
// a health probe makes an otherwise local tab switch take up to 5s.
|
||||
if (options.verifyHealth === false) return;
|
||||
if (hasRecentConnectionHealthCheck(connectionId)) return;
|
||||
const stateRevision = connectionStateRevision(connectionId);
|
||||
// Optimistic: verify backend pool is actually healthy
|
||||
try {
|
||||
await withConnectionHealthTimeout(connectionId, api.checkConnectionHealth(connectionId));
|
||||
if (!isCurrentConnectionStateRevision(connectionId, stateRevision)) throw new Error(CONNECTION_ATTEMPT_CANCELLED_MESSAGE);
|
||||
markConnectionHealthChecked(connectionId);
|
||||
return;
|
||||
} catch {
|
||||
// A late probe must not undo an explicit disconnect or a newer reconnect.
|
||||
if (!isCurrentConnectionStateRevision(connectionId, stateRevision)) throw new Error(CONNECTION_ATTEMPT_CANCELLED_MESSAGE);
|
||||
// Backend pool is dead — remove from connectedIds and reconnect.
|
||||
// 死池重连同样跨越了连接生命周期:shared 表元数据缓存与数据标签页的
|
||||
// 元数据 freshness 都必须作废,否则自动重连后仍可能复用断链前的旧
|
||||
@@ -5694,6 +5698,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
|
||||
async function loadObjectGroupChildren(node: TreeNode, options?: LoadTreeOptions) {
|
||||
// Queued search/refresh tasks can outlive disconnect, which removes their nodes.
|
||||
if (!treeNodeInSidebarTree(node)) return;
|
||||
const packageOwnerId = packageMemberGroupOwnerId(node);
|
||||
const packageConfig = node.connectionId ? getConfig(node.connectionId) : undefined;
|
||||
if (packageOwnerId && effectiveDatabaseTypeForConnection(packageConfig) === "xugu") {
|
||||
|
||||
Reference in new issue
Block a user