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 { createPinia, setActivePinia } from "pinia";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||||
|
import { runSidebarSearchTasks } from "@/components/sidebar/sidebarSearchTaskRunner";
|
||||||
|
|
||||||
function installLocalStorage() {
|
function installLocalStorage() {
|
||||||
const data = new Map<string, string>();
|
const data = new Map<string, string>();
|
||||||
@@ -93,6 +94,99 @@ describe("connectionStore timeout recovery", () => {
|
|||||||
expect(store.connectedIds.has(connection.id)).toBe(true);
|
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 () => {
|
it("normalizes missing keepalive interval to 30 seconds", async () => {
|
||||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||||
vi.doMock("@/lib/backend/api", () => ({
|
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.
|
// a health probe makes an otherwise local tab switch take up to 5s.
|
||||||
if (options.verifyHealth === false) return;
|
if (options.verifyHealth === false) return;
|
||||||
if (hasRecentConnectionHealthCheck(connectionId)) return;
|
if (hasRecentConnectionHealthCheck(connectionId)) return;
|
||||||
|
const stateRevision = connectionStateRevision(connectionId);
|
||||||
// Optimistic: verify backend pool is actually healthy
|
// Optimistic: verify backend pool is actually healthy
|
||||||
try {
|
try {
|
||||||
await withConnectionHealthTimeout(connectionId, api.checkConnectionHealth(connectionId));
|
await withConnectionHealthTimeout(connectionId, api.checkConnectionHealth(connectionId));
|
||||||
|
if (!isCurrentConnectionStateRevision(connectionId, stateRevision)) throw new Error(CONNECTION_ATTEMPT_CANCELLED_MESSAGE);
|
||||||
markConnectionHealthChecked(connectionId);
|
markConnectionHealthChecked(connectionId);
|
||||||
return;
|
return;
|
||||||
} catch {
|
} 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.
|
// Backend pool is dead — remove from connectedIds and reconnect.
|
||||||
// 死池重连同样跨越了连接生命周期:shared 表元数据缓存与数据标签页的
|
// 死池重连同样跨越了连接生命周期:shared 表元数据缓存与数据标签页的
|
||||||
// 元数据 freshness 都必须作废,否则自动重连后仍可能复用断链前的旧
|
// 元数据 freshness 都必须作废,否则自动重连后仍可能复用断链前的旧
|
||||||
@@ -5694,6 +5698,8 @@ export const useConnectionStore = defineStore("connection", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjectGroupChildren(node: TreeNode, options?: LoadTreeOptions) {
|
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 packageOwnerId = packageMemberGroupOwnerId(node);
|
||||||
const packageConfig = node.connectionId ? getConfig(node.connectionId) : undefined;
|
const packageConfig = node.connectionId ? getConfig(node.connectionId) : undefined;
|
||||||
if (packageOwnerId && effectiveDatabaseTypeForConnection(packageConfig) === "xugu") {
|
if (packageOwnerId && effectiveDatabaseTypeForConnection(packageConfig) === "xugu") {
|
||||||
|
|||||||
Reference in new issue
Block a user