mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-25 05:53:00 +00:00
Harden Garbage Collection V3 validation
This commit is contained in:
@@ -19,14 +19,14 @@ After the user starts Garbage Collection V3, LiveSync:
|
||||
|
||||
1. completes a one-shot bidirectional CouchDB synchronisation;
|
||||
2. reads the accepted-device list and current progress recorded on the remote;
|
||||
3. warns when an accepted device has no current information or device progress differs, then requires explicit confirmation;
|
||||
3. requires parseable progress information, warns when an accepted device has no current information or device progress differs, then requires explicit confirmation;
|
||||
4. computes the chunks reachable from the local PouchDB revision tree;
|
||||
5. creates a logical deletion for each locally present chunk which is not reachable;
|
||||
6. completes a push-only replication so that those deletions reach CouchDB;
|
||||
7. requests CouchDB compaction and waits for it for up to two minutes; and
|
||||
8. clears the local chunk caches.
|
||||
|
||||
If the initial synchronisation, device inspection, or confirmation fails, the workflow stops before collection. If push-only replication fails, the local logical deletions have already been created, but remote compaction is not started; synchronise again before retrying or using another device. A compaction failure is reported separately.
|
||||
If the initial synchronisation, device inspection, or confirmation fails, the workflow stops before collection. Missing or invalid device progress is treated as a failed inspection rather than offered as a confirmation override. If push-only replication fails, the local logical deletions have already been created, but remote compaction is not started; synchronise again before retrying or using another device. A compaction failure or completion timeout is reported separately and is not also reported as a successful completion.
|
||||
|
||||
## Reachability rules
|
||||
|
||||
|
||||
@@ -760,7 +760,7 @@ Success: ${successCount}, Errored: ${errored}`;
|
||||
timeout -= 2000;
|
||||
if (timeout <= 0) {
|
||||
this._notice("Compaction on remote database timed out.", "gc-compact");
|
||||
break;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
@@ -883,9 +883,14 @@ It is preferable to update all devices if possible. If you have any devices that
|
||||
}
|
||||
|
||||
//2. Check whether the progress values in NodeData are roughly the same (only the numerical part is needed).
|
||||
const progressValues = Object.values(node_info)
|
||||
.map((e) => e.progress.split("-")[0])
|
||||
.map((e) => parseInt(e));
|
||||
const progressValues = Object.values(node_info).map((entry) => {
|
||||
const progress = typeof entry.progress === "string" ? entry.progress.split("-")[0] : "";
|
||||
return /^\d+$/u.test(progress) ? Number(progress) : Number.NaN;
|
||||
});
|
||||
if (progressValues.length === 0 || progressValues.some((progress) => !Number.isSafeInteger(progress))) {
|
||||
this._notice("No connected device information found. Cancelling Garbage Collection.");
|
||||
return;
|
||||
}
|
||||
const maxProgress = Math.max(...progressValues);
|
||||
const minProgress = Math.min(...progressValues);
|
||||
const progressDifference = maxProgress - minProgress;
|
||||
|
||||
@@ -9,6 +9,13 @@ vi.mock("octagonal-wheels/concurrency/lock_v2", () => ({
|
||||
vi.mock("octagonal-wheels/collection", () => ({
|
||||
arrayToChunkedArray: vi.fn((values: unknown[]) => [values]),
|
||||
}));
|
||||
vi.mock("@vrtmrz/livesync-commonlib/compat/common/utils", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@vrtmrz/livesync-commonlib/compat/common/utils")>();
|
||||
return {
|
||||
...actual,
|
||||
delay: vi.fn(async () => undefined),
|
||||
};
|
||||
});
|
||||
vi.mock("@/features/LiveSyncCommands", () => ({
|
||||
LiveSyncCommands: class LiveSyncCommands {
|
||||
core!: { settings: unknown };
|
||||
@@ -209,6 +216,108 @@ describe("LocalDatabaseMaintenance prerequisites", () => {
|
||||
});
|
||||
|
||||
describe("LocalDatabaseMaintenance Garbage Collection V3", () => {
|
||||
it("does not report remote compaction as successful after its completion wait times out", async () => {
|
||||
const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance;
|
||||
const notice = vi.fn();
|
||||
const remoteDatabase = {
|
||||
compact: vi.fn(async () => ({ ok: true })),
|
||||
info: vi.fn(async () => ({ compact_running: true })),
|
||||
};
|
||||
Object.assign(maintenance, {
|
||||
core: {
|
||||
replicator: {
|
||||
connectRemoteCouchDBWithSetting: vi.fn(async () => ({ db: remoteDatabase })),
|
||||
},
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
},
|
||||
},
|
||||
_notice: notice,
|
||||
});
|
||||
|
||||
await maintenance.compactDatabase();
|
||||
|
||||
expect(notice).toHaveBeenCalledWith("Compaction on remote database timed out.", "gc-compact");
|
||||
expect(notice).not.toHaveBeenCalledWith(
|
||||
"Compaction on remote database completed successfully.",
|
||||
"gc-compact"
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["no device progress entries", {}],
|
||||
[
|
||||
"an unparseable device progress entry",
|
||||
{
|
||||
"device-a": {
|
||||
progress: "",
|
||||
device_name: "Device A",
|
||||
app_version: "1.12.7",
|
||||
plugin_version: "1.0.0-beta.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"a missing device progress entry",
|
||||
{
|
||||
"device-a": {
|
||||
device_name: "Device A",
|
||||
app_version: "1.12.7",
|
||||
plugin_version: "1.0.0-beta.0",
|
||||
},
|
||||
},
|
||||
],
|
||||
] as const)("cancels before collection when the milestone has %s", async (_case, nodeInfo) => {
|
||||
const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance;
|
||||
const pushModes: string[] = [];
|
||||
const allChunks = vi.fn(async () => ({
|
||||
used: new Set<string>(),
|
||||
existing: new Map(),
|
||||
}));
|
||||
const replicator = {
|
||||
openOneShotReplication: vi.fn(async (...args: unknown[]) => {
|
||||
pushModes.push(String(args[3]));
|
||||
return true;
|
||||
}),
|
||||
getConnectedDeviceList: vi.fn(async () => ({
|
||||
accepted_nodes: Object.keys(nodeInfo),
|
||||
node_info: nodeInfo,
|
||||
})),
|
||||
};
|
||||
const notice = vi.fn();
|
||||
Object.assign(maintenance, {
|
||||
core: {
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
remoteType: REMOTE_COUCHDB,
|
||||
},
|
||||
replicator,
|
||||
confirm: {
|
||||
askSelectStringDialogue: vi.fn(async () => "Proceed Garbage Collection"),
|
||||
},
|
||||
},
|
||||
localDatabase: {
|
||||
allChunks,
|
||||
localDatabase: {
|
||||
bulkDocs: vi.fn(async () => []),
|
||||
},
|
||||
},
|
||||
_notice: notice,
|
||||
});
|
||||
vi.spyOn(maintenance, "ensureAvailable").mockResolvedValue(true);
|
||||
vi.spyOn(maintenance, "compactDatabase").mockResolvedValue(undefined);
|
||||
vi.spyOn(maintenance, "clearHash").mockImplementation(() => undefined);
|
||||
|
||||
await maintenance.gcv3();
|
||||
|
||||
expect(allChunks).not.toHaveBeenCalled();
|
||||
expect(pushModes).toEqual(["sync"]);
|
||||
expect(notice).toHaveBeenCalledWith(
|
||||
"No connected device information found. Cancelling Garbage Collection."
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps chunks referenced by a live conflict revision and deletes only unreachable chunks", async () => {
|
||||
const maintenance = Object.create(LocalDatabaseMaintenance.prototype) as LocalDatabaseMaintenance;
|
||||
const pushModes: string[] = [];
|
||||
|
||||
+2
-2
@@ -25,13 +25,13 @@ Earlier releases remain available in the 0.25 release history and the legacy rel
|
||||
### Fixed
|
||||
|
||||
- An unreadable conflict revision is no longer deleted automatically merely because its chunks are unavailable on the current device.
|
||||
- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow.
|
||||
- Garbage Collection V3 now protects chunks required by every live conflict branch and the available revision ancestry needed to review and merge conflicts, instead of considering only the database winner. The action is offered only for CouchDB because P2P has no central database to compact and does not provide the device inventory required by the workflow. Collection now stops when device progress cannot be verified, and a compaction timeout is no longer followed by a contradictory success message.
|
||||
- Choosing **Apply settings to this device, and fetch again** for a compatible configuration mismatch now applies the remote settings before Fetch, instead of updating the remote database with this device's settings.
|
||||
- Accepted settings which control how new chunks are created now take effect before synchronisation is retried, rather than leaving the previous hash or splitter active until restart.
|
||||
|
||||
### Testing
|
||||
|
||||
- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, mobile dialogues, conflict-aware chunk reachability, shared chunks, collection propagation, and content-addressed chunk recreation.
|
||||
- Added regressions for revision repair, P2P configuration, the distinction between setting up the first device and using Fetch on an additional device, the P2P status pane, CouchDB setup policy, mobile dialogues, conflict-aware chunk reachability, device-progress safeguards, compaction timeouts, shared chunks, collection propagation, and content-addressed chunk recreation.
|
||||
|
||||
## 1.0.0-beta.2
|
||||
|
||||
|
||||
Reference in New Issue
Block a user