mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-08-31 15:57:06 +00:00
fix: close finite remote database connections
This commit is contained in:
@@ -58,7 +58,11 @@ async function verifyRemoteState(
|
|||||||
standardIo.writeStderr(`[Verification] Failed to connect to remote CouchDB: ${dbRet}\n`);
|
standardIo.writeStderr(`[Verification] Failed to connect to remote CouchDB: ${dbRet}\n`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
milestone = await dbRet.db.get(MILESTONE_DOCID);
|
try {
|
||||||
|
milestone = await dbRet.db.get(MILESTONE_DOCID);
|
||||||
|
} finally {
|
||||||
|
await dbRet.db.close();
|
||||||
|
}
|
||||||
} else if (settings.remoteType === REMOTE_MINIO) {
|
} else if (settings.remoteType === REMOTE_MINIO) {
|
||||||
milestone = await (replicator as LiveSyncJournalReplicator).client.downloadJson("_00000000-milestone.json");
|
milestone = await (replicator as LiveSyncJournalReplicator).client.downloadJson("_00000000-milestone.json");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -708,6 +708,18 @@ describe("runCommand abnormal cases", () => {
|
|||||||
describe("mark-resolved and unlock-remote commands", () => {
|
describe("mark-resolved and unlock-remote commands", () => {
|
||||||
it("mark-resolved without args runs on active database", async () => {
|
it("mark-resolved without args runs on active database", async () => {
|
||||||
const core = createCoreMock();
|
const core = createCoreMock();
|
||||||
|
const remoteDatabase = {
|
||||||
|
close: vi.fn(async () => undefined),
|
||||||
|
get: vi.fn(async () => ({
|
||||||
|
locked: false,
|
||||||
|
accepted_nodes: ["test-node-id"],
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
core.services.replicator.getActiveReplicator.mockReturnValueOnce({
|
||||||
|
nodeid: "test-node-id",
|
||||||
|
initializeDatabaseForReplication: vi.fn(async () => undefined),
|
||||||
|
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase })),
|
||||||
|
});
|
||||||
const result = await runCommand(makeOptions("mark-resolved", []), {
|
const result = await runCommand(makeOptions("mark-resolved", []), {
|
||||||
...context,
|
...context,
|
||||||
core,
|
core,
|
||||||
@@ -715,6 +727,7 @@ describe("runCommand abnormal cases", () => {
|
|||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(core.services.replication.markResolved).toHaveBeenCalledTimes(1);
|
expect(core.services.replication.markResolved).toHaveBeenCalledTimes(1);
|
||||||
expect(core.services.control.applySettings).not.toHaveBeenCalled();
|
expect(core.services.control.applySettings).not.toHaveBeenCalled();
|
||||||
|
expect(remoteDatabase.close).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mark-resolved with remote-id temporarily activates it and runs markResolved", async () => {
|
it("mark-resolved with remote-id temporarily activates it and runs markResolved", async () => {
|
||||||
|
|||||||
@@ -747,29 +747,33 @@ Success: ${successCount}, Errored: ${errored}`;
|
|||||||
this._notice(`Failed to connect to remote for compaction. ${remote}`, "gc-compact");
|
this._notice(`Failed to connect to remote for compaction. ${remote}`, "gc-compact");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const compactResult = await remote.db.compact({
|
try {
|
||||||
interval: 1000,
|
const compactResult = await remote.db.compact({
|
||||||
});
|
interval: 1000,
|
||||||
// Probably no need to wait, but just in case.
|
});
|
||||||
let timeout = 2 * 60 * 1000; // 2 minutes
|
// Probably no need to wait, but just in case.
|
||||||
for (;;) {
|
let timeout = 2 * 60 * 1000; // 2 minutes
|
||||||
const status = await remote.db.info();
|
for (;;) {
|
||||||
if ("compact_running" in status && status?.compact_running) {
|
const status = await remote.db.info();
|
||||||
this._notice("Compaction in progress on remote database...", "gc-compact");
|
if ("compact_running" in status && status?.compact_running) {
|
||||||
await delay(2000);
|
this._notice("Compaction in progress on remote database...", "gc-compact");
|
||||||
timeout -= 2000;
|
await delay(2000);
|
||||||
if (timeout <= 0) {
|
timeout -= 2000;
|
||||||
this._notice("Compaction on remote database timed out.", "gc-compact");
|
if (timeout <= 0) {
|
||||||
return;
|
this._notice("Compaction on remote database timed out.", "gc-compact");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
if (compactResult && "ok" in compactResult) {
|
||||||
if (compactResult && "ok" in compactResult) {
|
this._notice("Compaction on remote database completed successfully.", "gc-compact");
|
||||||
this._notice("Compaction on remote database completed successfully.", "gc-compact");
|
} else {
|
||||||
} else {
|
this._notice("Compaction on remote database failed.", "gc-compact");
|
||||||
this._notice("Compaction on remote database failed.", "gc-compact");
|
}
|
||||||
|
} finally {
|
||||||
|
await remote.db.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ describe("LocalDatabaseMaintenance Garbage Collection V3", () => {
|
|||||||
const remoteDatabase = {
|
const remoteDatabase = {
|
||||||
compact: vi.fn(async () => ({ ok: true })),
|
compact: vi.fn(async () => ({ ok: true })),
|
||||||
info: vi.fn(async () => ({ compact_running: true })),
|
info: vi.fn(async () => ({ compact_running: true })),
|
||||||
|
close: vi.fn(async () => undefined),
|
||||||
};
|
};
|
||||||
Object.assign(maintenance, {
|
Object.assign(maintenance, {
|
||||||
core: {
|
core: {
|
||||||
@@ -243,6 +244,7 @@ describe("LocalDatabaseMaintenance Garbage Collection V3", () => {
|
|||||||
"Compaction on remote database completed successfully.",
|
"Compaction on remote database completed successfully.",
|
||||||
"gc-compact"
|
"gc-compact"
|
||||||
);
|
);
|
||||||
|
expect(remoteDatabase.close).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
|
|||||||
@@ -187,27 +187,31 @@ Even if you choose to clean up, you will see this option again if you exit Obsid
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
try {
|
||||||
this.localDatabase.clearCaches();
|
|
||||||
// Perform the synchronisation once.
|
|
||||||
const replicated = await this.services.replicator.runFiniteReplicationActivity(
|
|
||||||
() => this.core.replicator.openReplication(this.settings, false, showMessage, true),
|
|
||||||
{ label: "replication" }
|
|
||||||
);
|
|
||||||
if (replicated) {
|
|
||||||
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
|
|
||||||
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
||||||
this.localDatabase.clearCaches();
|
this.localDatabase.clearCaches();
|
||||||
await this.services.replicator.getActiveReplicator()?.markRemoteResolved(this.settings);
|
// Perform the synchronisation once.
|
||||||
Logger(
|
const replicated = await this.services.replicator.runFiniteReplicationActivity(
|
||||||
"The local database has been cleaned up.",
|
() => this.core.replicator.openReplication(this.settings, false, showMessage, true),
|
||||||
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
|
{ label: "replication" }
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Logger(
|
|
||||||
"Replication has been cancelled. Please try it again.",
|
|
||||||
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
|
|
||||||
);
|
);
|
||||||
|
if (replicated) {
|
||||||
|
await balanceChunkPurgedDBs(this.localDatabase.localDatabase, remoteDB.db);
|
||||||
|
await purgeUnreferencedChunks(this.localDatabase.localDatabase, false);
|
||||||
|
this.localDatabase.clearCaches();
|
||||||
|
await this.services.replicator.getActiveReplicator()?.markRemoteResolved(this.settings);
|
||||||
|
Logger(
|
||||||
|
"The local database has been cleaned up.",
|
||||||
|
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Logger(
|
||||||
|
"Replication has been cancelled. Please try it again.",
|
||||||
|
showMessage ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await remoteDB.db.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ label: "database-cleanup" }
|
{ label: "database-cleanup" }
|
||||||
|
|||||||
@@ -128,8 +128,11 @@ describe("compatibility: cleaned-remote reconciliation for IndexedDB clients", (
|
|||||||
});
|
});
|
||||||
const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task());
|
const runFiniteReplicationActivity = vi.fn(async (task: () => unknown) => await task());
|
||||||
const openReplication = vi.fn(async () => true);
|
const openReplication = vi.fn(async () => true);
|
||||||
|
const remoteDatabase = {
|
||||||
|
close: vi.fn(async () => undefined),
|
||||||
|
};
|
||||||
const activeReplicator = Object.assign(new LiveSyncCouchDBReplicator({} as any), {
|
const activeReplicator = Object.assign(new LiveSyncCouchDBReplicator({} as any), {
|
||||||
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: {} })),
|
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase })),
|
||||||
markRemoteResolved: vi.fn(async () => undefined),
|
markRemoteResolved: vi.fn(async () => undefined),
|
||||||
});
|
});
|
||||||
const services = {
|
const services = {
|
||||||
@@ -177,5 +180,9 @@ describe("compatibility: cleaned-remote reconciliation for IndexedDB clients", (
|
|||||||
expect(openReplication).toHaveBeenCalledOnce();
|
expect(openReplication).toHaveBeenCalledOnce();
|
||||||
expect(openReplication.mock.invocationCallOrder[0]).toBeLessThan(activityFinished.mock.invocationCallOrder[0]);
|
expect(openReplication.mock.invocationCallOrder[0]).toBeLessThan(activityFinished.mock.invocationCallOrder[0]);
|
||||||
expect(chunkMocks.balanceChunkPurgedDBs).toHaveBeenCalledOnce();
|
expect(chunkMocks.balanceChunkPurgedDBs).toHaveBeenCalledOnce();
|
||||||
|
expect(remoteDatabase.close).toHaveBeenCalledOnce();
|
||||||
|
expect(remoteDatabase.close.mock.invocationCallOrder[0]).toBeLessThan(
|
||||||
|
activityFinished.mock.invocationCallOrder[0]
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -547,7 +547,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
if (typeof db === "string") {
|
if (typeof db === "string") {
|
||||||
Logger($msg("obsidianLiveSyncSettingTab.logCheckPassphraseFailed", { db }), LOG_LEVEL_NOTICE);
|
Logger($msg("obsidianLiveSyncSettingTab.logCheckPassphraseFailed", { db }), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
}
|
||||||
|
try {
|
||||||
if (await checkSyncInfo(db.db)) {
|
if (await checkSyncInfo(db.db)) {
|
||||||
// Logger($msg("obsidianLiveSyncSettingTab.logDatabaseConnected"), LOG_LEVEL_NOTICE);
|
// Logger($msg("obsidianLiveSyncSettingTab.logDatabaseConnected"), LOG_LEVEL_NOTICE);
|
||||||
return true;
|
return true;
|
||||||
@@ -555,6 +556,8 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
|
|||||||
Logger($msg("obsidianLiveSyncSettingTab.logPassphraseNotCompatible"), LOG_LEVEL_NOTICE);
|
Logger($msg("obsidianLiveSyncSettingTab.logPassphraseNotCompatible"), LOG_LEVEL_NOTICE);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
await db.db.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
isPassphraseValid = async () => {
|
isPassphraseValid = async () => {
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { DEFAULT_SETTINGS, REMOTE_COUCHDB } from "@vrtmrz/livesync-commonlib/compat/common/types";
|
||||||
|
|
||||||
|
const negotiationMocks = vi.hoisted(() => ({
|
||||||
|
checkSyncInfo: vi.fn(async () => true),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/deps.ts", () => ({
|
||||||
|
App: class {},
|
||||||
|
Component: class {},
|
||||||
|
PluginSettingTab: class {},
|
||||||
|
}));
|
||||||
|
vi.mock("@/main.ts", () => ({ default: class {} }));
|
||||||
|
vi.mock("@/common/events.ts", () => ({
|
||||||
|
EVENT_REQUEST_RELOAD_SETTING_TAB: "request-reload-setting-tab",
|
||||||
|
eventHub: { onEvent: vi.fn() },
|
||||||
|
}));
|
||||||
|
vi.mock("@vrtmrz/livesync-commonlib/compat/pouchdb/negotiation", () => negotiationMocks);
|
||||||
|
vi.mock("@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator", () => ({
|
||||||
|
LiveSyncCouchDBReplicator: class {},
|
||||||
|
}));
|
||||||
|
vi.mock("./LiveSyncSetting.ts", () => ({ LiveSyncSetting: class {} }));
|
||||||
|
vi.mock("./SettingPane.ts", () => ({
|
||||||
|
enableOnly: vi.fn(() => vi.fn()),
|
||||||
|
setLevelClass: vi.fn(),
|
||||||
|
setStyle: vi.fn(),
|
||||||
|
visibleOnly: vi.fn(() => vi.fn()),
|
||||||
|
}));
|
||||||
|
vi.mock("./PaneChangeLog.ts", () => ({ paneChangeLog: vi.fn() }));
|
||||||
|
vi.mock("./PaneSetup.ts", () => ({ paneSetup: vi.fn() }));
|
||||||
|
vi.mock("./PaneGeneral.ts", () => ({ paneGeneral: vi.fn() }));
|
||||||
|
vi.mock("./PaneRemoteConfig.ts", () => ({ paneRemoteConfig: vi.fn() }));
|
||||||
|
vi.mock("./PaneSelector.ts", () => ({ paneSelector: vi.fn() }));
|
||||||
|
vi.mock("./PaneSyncSettings.ts", () => ({ paneSyncSettings: vi.fn() }));
|
||||||
|
vi.mock("./PaneCustomisationSync.ts", () => ({ paneCustomisationSync: vi.fn() }));
|
||||||
|
vi.mock("./PaneHatch.ts", () => ({ paneHatch: vi.fn() }));
|
||||||
|
vi.mock("./PaneAdvanced.ts", () => ({ paneAdvanced: vi.fn() }));
|
||||||
|
vi.mock("./PanePowerUsers.ts", () => ({ panePowerUsers: vi.fn() }));
|
||||||
|
vi.mock("./PanePatches.ts", () => ({ panePatches: vi.fn() }));
|
||||||
|
vi.mock("./PaneMaintenance.ts", () => ({ paneMaintenance: vi.fn() }));
|
||||||
|
|
||||||
|
import { LiveSyncCouchDBReplicator } from "@vrtmrz/livesync-commonlib/compat/replication/couchdb/LiveSyncReplicator";
|
||||||
|
import { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab";
|
||||||
|
|
||||||
|
describe("ObsidianLiveSyncSettingTab passphrase verification", () => {
|
||||||
|
it("closes the finite remote connection after checking synchronisation information", async () => {
|
||||||
|
const remoteDatabase = {
|
||||||
|
close: vi.fn(async () => undefined),
|
||||||
|
};
|
||||||
|
const replicator = Object.assign(new LiveSyncCouchDBReplicator({} as never), {
|
||||||
|
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase })),
|
||||||
|
});
|
||||||
|
const plugin = {
|
||||||
|
app: {},
|
||||||
|
core: {
|
||||||
|
services: {
|
||||||
|
API: { isMobile: vi.fn(() => false) },
|
||||||
|
replicator: { getNewReplicator: vi.fn(() => replicator) },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const tab = new ObsidianLiveSyncSettingTab({} as never, plugin as never);
|
||||||
|
Object.assign(tab, {
|
||||||
|
_editingSettings: {
|
||||||
|
...DEFAULT_SETTINGS,
|
||||||
|
remoteType: REMOTE_COUCHDB,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(tab.checkWorkingPassphrase()).resolves.toBe(true);
|
||||||
|
|
||||||
|
expect(negotiationMocks.checkSyncInfo).toHaveBeenCalledWith(remoteDatabase);
|
||||||
|
expect(remoteDatabase.close).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@ export type CouchDBConnectionProbeResult = { ok: true } | { ok: false; reason: s
|
|||||||
type CouchDBConnectionResult =
|
type CouchDBConnectionResult =
|
||||||
| string
|
| string
|
||||||
| {
|
| {
|
||||||
db: unknown;
|
db: { close(): Promise<void> };
|
||||||
info: unknown;
|
info: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,7 +50,11 @@ export async function probeCouchDBConnection(
|
|||||||
if (typeof result === "string") {
|
if (typeof result === "string") {
|
||||||
return { ok: false, reason: result };
|
return { ok: false, reason: result };
|
||||||
}
|
}
|
||||||
return { ok: true };
|
try {
|
||||||
|
return { ok: true };
|
||||||
|
} finally {
|
||||||
|
await result.db.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidCouchDBServerURL(value: string): boolean {
|
export function isValidCouchDBServerURL(value: string): boolean {
|
||||||
|
|||||||
@@ -14,8 +14,9 @@ describe("CouchDB setup connection policy", () => {
|
|||||||
] as const)(
|
] as const)(
|
||||||
"%s can %s without changing the Commonlib connection contract",
|
"%s can %s without changing the Commonlib connection contract",
|
||||||
async (createIfMissing, _description) => {
|
async (createIfMissing, _description) => {
|
||||||
|
const close = vi.fn(async () => undefined);
|
||||||
const connectRemoteCouchDBWithSetting = vi.fn(async () => ({
|
const connectRemoteCouchDBWithSetting = vi.fn(async () => ({
|
||||||
db: {},
|
db: { close },
|
||||||
info: { db_name: "notes" },
|
info: { db_name: "notes" },
|
||||||
}));
|
}));
|
||||||
const replicator = {
|
const replicator = {
|
||||||
@@ -27,6 +28,7 @@ describe("CouchDB setup connection policy", () => {
|
|||||||
await expect(probeCouchDBConnection(replicator, settings, createIfMissing)).resolves.toEqual({ ok: true });
|
await expect(probeCouchDBConnection(replicator, settings, createIfMissing)).resolves.toEqual({ ok: true });
|
||||||
expect(connectRemoteCouchDBWithSetting).toHaveBeenCalledWith(settings, false, createIfMissing, false);
|
expect(connectRemoteCouchDBWithSetting).toHaveBeenCalledWith(settings, false, createIfMissing, false);
|
||||||
expect(replicator.tryConnectRemote).not.toHaveBeenCalled();
|
expect(replicator.tryConnectRemote).not.toHaveBeenCalled();
|
||||||
|
expect(close).toHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user