sliftutils 1.7.105 → 1.7.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/treeSummary.ts.cache +2 -2
- package/index.d.ts +7 -4
- package/misc/dist/environment.ts.cache +2 -2
- package/misc/dist/getSecret.ts.cache +2 -2
- package/misc/dist/strings.ts.cache +2 -2
- package/misc/dist/zip.ts.cache +2 -2
- package/misc/https/dist/certs.ts.cache +2 -2
- package/misc/https/dist/persistentLocalStorage.ts.cache +2 -2
- package/package.json +1 -1
- package/render-utils/dist/observer.tsx.cache +2 -2
- package/storage/ArchivesDisk.ts +1 -1
- package/storage/dist/ArchivesDisk.ts.cache +3 -3
- package/storage/dist/FileFolderAPI.tsx.cache +2 -2
- package/storage/dist/IArchives.ts.cache +2 -2
- package/storage/dist/IndexedDBFileFolderAPI.ts.cache +2 -2
- package/storage/dist/JSONStorage.ts.cache +2 -2
- package/storage/dist/PendingManager.tsx.cache +2 -2
- package/storage/dist/StreamingLogs.ts.cache +2 -2
- package/storage/dist/TransactionFile.ts.cache +2 -2
- package/storage/dist/TransactionStorage.ts.cache +2 -2
- package/storage/dist/archiveHelpers.ts.cache +2 -2
- package/storage/dist/backblaze.ts.cache +2 -2
- package/storage/dist/fileSystemPointer.ts.cache +2 -2
- package/storage/dist/remoteFileStorage.ts.cache +2 -2
- package/storage/remoteStorage/dist/ArchivesDelayed.ts.cache +2 -2
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +2 -2
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +2 -2
- package/storage/remoteStorage/dist/accessStats.ts.cache +2 -2
- package/storage/remoteStorage/dist/blobStore.ts.cache +2 -2
- package/storage/remoteStorage/dist/bucketDisk.ts.cache +2 -2
- package/storage/remoteStorage/dist/certTrustModal.tsx.cache +2 -2
- package/storage/remoteStorage/dist/chainStartup.ts.cache +2 -2
- package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
- package/storage/remoteStorage/dist/createArchives.ts.cache +2 -2
- package/storage/remoteStorage/dist/deployTakeover.ts.cache +2 -2
- package/storage/remoteStorage/dist/intermediateManagement.ts.cache +2 -2
- package/storage/remoteStorage/dist/intermediateSources.ts.cache +2 -2
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +2 -2
- package/storage/remoteStorage/dist/serverConfig.ts.cache +2 -2
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +2 -2
- package/storage/remoteStorage/dist/sourcesList.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageController.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageLogs.ts.cache +61 -42
- package/storage/remoteStorage/dist/storageServerState.ts.cache +2 -2
- package/storage/remoteStorage/dist/storeConfig.ts.cache +2 -2
- package/storage/remoteStorage/dist/storePlan.ts.cache +2 -2
- package/storage/remoteStorage/dist/storeSources.ts.cache +2 -2
- package/storage/remoteStorage/dist/storeSync.ts.cache +72 -61
- package/storage/remoteStorage/dist/validation.ts.cache +2 -2
- package/storage/remoteStorage/storageLogs.d.ts +3 -2
- package/storage/remoteStorage/storageLogs.ts +56 -35
- package/storage/remoteStorage/storeSync.d.ts +4 -2
- package/storage/remoteStorage/storeSync.ts +69 -56
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";
|
|
1
|
+
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true , configurable: true});
|
|
3
3
|
//exports.assertValidArgs = exports.assertValidPath = exports.assertValidSourceName = exports.assertValidName = void 0;
|
|
4
4
|
function assertValidName(value, kind) {
|
|
@@ -47,4 +47,4 @@ function assertValidArgs(target, key, descriptor) {
|
|
|
47
47
|
}
|
|
48
48
|
exports.assertValidArgs = assertValidArgs;
|
|
49
49
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbInZhbGlkYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEsU0FBZ0IsZUFBZSxDQUFDLEtBQWEsRUFBRSxJQUFZO0lBQ3ZELElBQUksQ0FBQyxlQUFlLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDL0IsTUFBTSxJQUFJLEtBQUssQ0FBQyxXQUFXLElBQUksSUFBSSxJQUFJLENBQUMsU0FBUyxDQUFDLEtBQUssQ0FBQywrREFBK0QsQ0FBQyxDQUFDO0lBQzdILENBQUM7QUFDTCxDQUFDO0FBSkQsMENBSUM7QUFFRCx1VEFBdVQ7QUFDdlQsU0FBZ0IscUJBQXFCLENBQUMsS0FBYTtJQUMvQyxJQUFJLENBQUMsZ0JBQWdCLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDaEMsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsbUVBQW1FLENBQUMsQ0FBQztJQUNySSxDQUFDO0lBQ0QsSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDdEIsTUFBTSxJQUFJLEtBQUssQ0FBQyx1QkFBdUIsSUFBSSxDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsb0VBQW9FLENBQUMsQ0FBQztJQUN0SSxDQUFDO0FBQ0wsQ0FBQztBQVBELHNEQU9DO0FBRUQsU0FBZ0IsZUFBZSxDQUFDLElBQVk7SUFDeEMsSUFBSSxNQUFNLENBQUMsSUFBSSxDQUFDLElBQUksRUFBRSxNQUFNLENBQUMsQ0FBQyxNQUFNLEdBQUcsSUFBSSxFQUFFLENBQUM7UUFDMUMsTUFBTSxJQUFJLEtBQUssQ0FBQyxrQkFBa0IsSUFBSSxDQUFDLE1BQU0sNkJBQTZCLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUNwRyxDQUFDO0lBQ0QsSUFBSSxDQUFDLElBQUksSUFBSSxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsQ0FBQyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsSUFBSSxJQUFJLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxJQUFJLElBQUksQ0FBQyxRQUFRLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQztRQUM3SCxNQUFNLElBQUksS0FBSyxDQUFDLGdCQUFnQixJQUFJLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDLDhGQUE4RixDQUFDLENBQUM7SUFDdEssQ0FBQztJQUNELElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxJQUFJLEtBQUssR0FBRyxJQUFJLElBQUksS0FBSyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQzlELE1BQU0sSUFBSSxLQUFLLENBQUMsZ0JBQWdCLElBQUksQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMseUNBQXlDLENBQUMsQ0FBQztJQUNqSCxDQUFDO0FBQ0wsQ0FBQztBQVZELDBDQVVDO0FBRUQsNFdBQTRXO0FBQzVXLFNBQWdCLGVBQWUsQ0FBQyxNQUFlLEVBQUUsR0FBVyxFQUFFLFVBQThCO0lBQ3hGLElBQUksUUFBUSxHQUFHLFVBQVUsQ0FBQyxLQUF3QyxDQUFDO0lBQ25FLFVBQVUsQ0FBQyxLQUFLLEdBQUcsVUFBVSxHQUFHLElBQWU7UUFDM0MsSUFBSSxNQUFNLEdBQUcsSUFBSSxDQUFDLENBQUMsQ0FBeUUsQ0FBQztRQUM3RixJQUFJLE1BQU0sSUFBSSxPQUFPLE1BQU0sS0FBSyxRQUFRLEVBQUUsQ0FBQztZQUN2QyxJQUFJLFNBQVMsSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsT0FBaUIsRUFBRSxTQUFTLENBQUMsQ0FBQztZQUM5RSxJQUFJLFlBQVksSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsVUFBb0IsRUFBRSxhQUFhLENBQUMsQ0FBQztZQUN4RixJQUFJLE1BQU0sSUFBSSxNQUFNO2dCQUFFLGVBQWUsQ0FBQyxNQUFNLENBQUMsSUFBYyxDQUFDLENBQUM7UUFDakUsQ0FBQztRQUNELE9BQU8sUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFDLENBQUM7SUFDdEMsQ0FBQyxDQUFDO0FBQ04sQ0FBQztBQVhELDBDQVdDIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IGZ1bmN0aW9uIGFzc2VydFZhbGlkTmFtZSh2YWx1ZTogc3RyaW5nLCBraW5kOiBzdHJpbmcpOiB2b2lkIHtcbiAgICBpZiAoIS9eW1xcdy1dezEsNjR9JC8udGVzdCh2YWx1ZSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBJbnZhbGlkICR7a2luZH0gJHtKU09OLnN0cmluZ2lmeSh2YWx1ZSl9LCBleHBlY3RlZCAxLTY0IGNoYXJhY3RlcnMgb2YgbGV0dGVycy9udW1iZXJzL3VuZGVyc2NvcmUvZGFzaGApO1xuICAgIH1cbn1cblxuLyoqIEEgc3RvcmUncyBuYW1lIChzZWUgQ29tbW9uQ29uZmlnLm5hbWUpLCB3aGljaCBhbHNvIGFsbG93cyBkb3RzIC0gYSBuYW1lIGlzIG9mdGVuIGEgaG9zdCBvciBhIHZlcnNpb24sIGFuZCBib3RoIHJlYWQgd3Jvbmcgd2l0aG91dCB0aGVtLiBJdCBpcyBvbmUgcGF0aCBzZWdtZW50IG9mIHRoZSBzdG9yZSdzIGZvbGRlciwgc28gdGhlIHR3byBuYW1lcyB0aGF0IHdvdWxkIG1lYW4gYSBkaWZmZXJlbnQgZm9sZGVyIGVudGlyZWx5IGFyZSByZWplY3RlZDogZXZlcnl0aGluZyBlbHNlIGNvbnRhaW5pbmcgZG90cyBpcyBqdXN0IGEgbmFtZS4gKi9cbmV4cG9ydCBmdW5jdGlvbiBhc3NlcnRWYWxpZFNvdXJjZU5hbWUodmFsdWU6IHN0cmluZyk6IHZvaWQge1xuICAgIGlmICghL15bXFx3Li1dezEsNjR9JC8udGVzdCh2YWx1ZSkpIHtcbiAgICAgICAgdGhyb3cgbmV3IEVycm9yKGBJbnZhbGlkIHNvdXJjZSBuYW1lICR7SlNPTi5zdHJpbmdpZnkodmFsdWUpfSwgZXhwZWN0ZWQgMS02NCBjaGFyYWN0ZXJzIG9mIGxldHRlcnMvbnVtYmVycy91bmRlcnNjb3JlL2Rhc2gvZG90YCk7XG4gICAgfVxuICAgIGlmICgvXlxcLiskLy50ZXN0KHZhbHVlKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgc291cmNlIG5hbWUgJHtKU09OLnN0cmluZ2lmeSh2YWx1ZSl9OiBhIG5hbWUgb2Ygb25seSBkb3RzIHJlZmVycyB0byBhIGRpcmVjdG9yeSByYXRoZXIgdGhhbiBuYW1pbmcgb25lYCk7XG4gICAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gYXNzZXJ0VmFsaWRQYXRoKHBhdGg6IHN0cmluZyk6IHZvaWQge1xuICAgIGlmIChCdWZmZXIuZnJvbShwYXRoLCBcInV0ZjhcIikubGVuZ3RoID4gMTAwMCkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYFBhdGggdG9vIGxvbmc6ICR7cGF0aC5sZW5ndGh9IGNoYXJhY3RlcnMgPiAxMDAwLiBQYXRoOiAke3BhdGguc2xpY2UoMCwgMjAwKX1gKTtcbiAgICB9XG4gICAgaWYgKCFwYXRoIHx8IHBhdGguc3RhcnRzV2l0aChcIi9cIikgfHwgcGF0aC5lbmRzV2l0aChcIi9cIikgfHwgcGF0aC5pbmNsdWRlcyhcIi8vXCIpIHx8IHBhdGguaW5jbHVkZXMoXCJcXFxcXCIpIHx8IHBhdGguaW5jbHVkZXMoXCJcXHgwMFwiKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYEludmFsaWQgcGF0aCAke0pTT04uc3RyaW5naWZ5KHBhdGguc2xpY2UoMCwgMjAwKSl9LCBwYXRocyBjYW5ub3QgYmUgZW1wdHksIHN0YXJ0IG9yIGVuZCB3aXRoIC8sIG9yIGNvbnRhaW4gLy8sIGJhY2tzbGFzaGVzLCBvciBudWxsIGNoYXJhY3RlcnNgKTtcbiAgICB9XG4gICAgaWYgKHBhdGguc3BsaXQoXCIvXCIpLnNvbWUocGFydCA9PiBwYXJ0ID09PSBcIi5cIiB8fCBwYXJ0ID09PSBcIi4uXCIpKSB7XG4gICAgICAgIHRocm93IG5ldyBFcnJvcihgSW52YWxpZCBwYXRoICR7SlNPTi5zdHJpbmdpZnkocGF0aC5zbGljZSgwLCAyMDApKX0sIHBhdGhzIGNhbm5vdCBjb250YWluIC4gb3IgLi4gc2VnbWVudHNgKTtcbiAgICB9XG59XG5cbi8qKiBNZXRob2QgZGVjb3JhdG9yOiB2YWxpZGF0ZXMgdGhlIHdlbGwta25vd24gZmllbGRzIG9mIHRoZSBtZXRob2QncyBzaW5nbGUgY29uZmlnLW9iamVjdCBhcmd1bWVudCAtIGFjY291bnQvYnVja2V0TmFtZSBhcyBuYW1lcywgcGF0aCBhcyBhIHBhdGggLSBiZWZvcmUgdGhlIG1ldGhvZCBydW5zLiBGaWVsZHMgdGhlIGNvbmZpZyBkb2Vzbid0IGhhdmUgYXJlIHNraXBwZWQsIHNvIGl0IGFwcGxpZXMgdG8gZXZlcnkgQVBJIG1ldGhvZCB1bmlmb3JtbHkuIHByZWZpeCBpcyBkZWxpYmVyYXRlbHkgTk9UIHZhbGlkYXRlZDogcHJlZml4ZXMgbWF5IGJlIGVtcHR5IG9yIGVuZCB3aXRoIFwiL1wiLCBib3RoIGludmFsaWQgZm9yIHBhdGhzLiAqL1xuZXhwb3J0IGZ1bmN0aW9uIGFzc2VydFZhbGlkQXJncyh0YXJnZXQ6IHVua25vd24sIGtleTogc3RyaW5nLCBkZXNjcmlwdG9yOiBQcm9wZXJ0eURlc2NyaXB0b3IpOiB2b2lkIHtcbiAgICBsZXQgb3JpZ2luYWwgPSBkZXNjcmlwdG9yLnZhbHVlIGFzICguLi5hcmdzOiB1bmtub3duW10pID0+IHVua25vd247XG4gICAgZGVzY3JpcHRvci52YWx1ZSA9IGZ1bmN0aW9uICguLi5hcmdzOiB1bmtub3duW10pOiB1bmtub3duIHtcbiAgICAgICAgbGV0IGNvbmZpZyA9IGFyZ3NbMF0gYXMgeyBhY2NvdW50Pzogc3RyaW5nOyBidWNrZXROYW1lPzogc3RyaW5nOyBwYXRoPzogc3RyaW5nIH0gfCB1bmRlZmluZWQ7XG4gICAgICAgIGlmIChjb25maWcgJiYgdHlwZW9mIGNvbmZpZyA9PT0gXCJvYmplY3RcIikge1xuICAgICAgICAgICAgaWYgKFwiYWNjb3VudFwiIGluIGNvbmZpZykgYXNzZXJ0VmFsaWROYW1lKGNvbmZpZy5hY2NvdW50IGFzIHN0cmluZywgXCJhY2NvdW50XCIpO1xuICAgICAgICAgICAgaWYgKFwiYnVja2V0TmFtZVwiIGluIGNvbmZpZykgYXNzZXJ0VmFsaWROYW1lKGNvbmZpZy5idWNrZXROYW1lIGFzIHN0cmluZywgXCJidWNrZXQgbmFtZVwiKTtcbiAgICAgICAgICAgIGlmIChcInBhdGhcIiBpbiBjb25maWcpIGFzc2VydFZhbGlkUGF0aChjb25maWcucGF0aCBhcyBzdHJpbmcpO1xuICAgICAgICB9XG4gICAgICAgIHJldHVybiBvcmlnaW5hbC5hcHBseSh0aGlzLCBhcmdzKTtcbiAgICB9O1xufVxuIl19
|
|
50
|
-
/* _JS_SOURCE_HASH = "
|
|
50
|
+
/* _JS_SOURCE_HASH = "52f73d0ef7e2f65848acc68373c50e34bd39c017c3ff908220fc66a6f3d0296f"; */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
import { LogFileInfo } from "../StreamingLogs";
|
|
4
4
|
export declare const LOGS_FOLDER_NAME = "logs";
|
|
5
|
-
/** One mutation the server performed: set/del/move/undelete/setLarge/routingConfig, plus the per-file synchronization writes ("sync get"/"sync set"). Sizes and times, never the data. internal marks writes pushed by a peer's synchronization rather than a client. */
|
|
5
|
+
/** One mutation the server performed: set/del/move/undelete/setLarge/routingConfig, plus the per-file synchronization writes ("sync get"/"sync set"). Sizes and times, never the data. internal marks writes pushed by a peer's synchronization rather than a client. Also printed to the console. */
|
|
6
6
|
export declare function logMutation(entry: {
|
|
7
7
|
op: string;
|
|
8
8
|
account: string;
|
|
@@ -15,13 +15,14 @@ export declare function logMutation(entry: {
|
|
|
15
15
|
callerId?: string;
|
|
16
16
|
internal?: boolean;
|
|
17
17
|
}): void;
|
|
18
|
-
/** A synchronization key point: scans and full syncs starting/finishing, reconciles, boundary scans - what an operator greps for to see whether the fleet is converging. */
|
|
18
|
+
/** A synchronization key point: scans and full syncs starting/finishing, reconciles, boundary scans - what an operator greps for to see whether the fleet is converging. Also printed to the console. */
|
|
19
19
|
export declare function logSyncEvent(entry: {
|
|
20
20
|
event: string;
|
|
21
21
|
store: string;
|
|
22
22
|
source?: string;
|
|
23
23
|
[key: string]: unknown;
|
|
24
24
|
}): void;
|
|
25
|
+
/** One console.error is all an error takes (console.error and console.warn are HOOKED to feed the stream) - this just guarantees the hook is installed first, for very-early callers. */
|
|
25
26
|
export declare function logStorageError(message: string): void;
|
|
26
27
|
/** The log files this server holds - see StreamingLogs.listFiles. Empty on processes with no storage folder. */
|
|
27
28
|
export declare function listStorageLogFiles(): Promise<LogFileInfo[]>;
|
|
@@ -9,22 +9,20 @@ import { getOwnThreadId } from "../../misc/https/certs";
|
|
|
9
9
|
|
|
10
10
|
export const LOGS_FOLDER_NAME = "logs";
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// NOT lazy: a failure to resolve the folder (the server config may simply not be SET yet this early in startup) must be retried on the next log call, not cached as "no logs ever"
|
|
13
|
+
let logsInstance: StreamingLogs | undefined;
|
|
14
|
+
function getLogs(): StreamingLogs | undefined {
|
|
15
|
+
if (logsInstance) return logsInstance;
|
|
13
16
|
let folder: string;
|
|
14
17
|
try {
|
|
15
18
|
folder = path.join(getStorageFolder(), LOGS_FOLDER_NAME);
|
|
16
19
|
} catch {
|
|
17
20
|
return undefined;
|
|
18
21
|
}
|
|
19
|
-
let threadId: string | undefined;
|
|
20
|
-
try {
|
|
21
|
-
threadId = getOwnThreadId(getStorageServerConfigOptional()?.rootDomain || "");
|
|
22
|
-
} catch {
|
|
23
|
-
threadId = undefined;
|
|
24
|
-
}
|
|
25
22
|
hookErrorLogging();
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
logsInstance = new StreamingLogs({ folder, threadId: getThreadIdSafe() });
|
|
24
|
+
return logsInstance;
|
|
25
|
+
}
|
|
28
26
|
|
|
29
27
|
function firstExternalIPv4(): string | undefined {
|
|
30
28
|
for (let addresses of Object.values(os.networkInterfaces())) {
|
|
@@ -35,48 +33,71 @@ function firstExternalIPv4(): string | undefined {
|
|
|
35
33
|
return undefined;
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
36
|
+
// Memoized on SUCCESS only: the thread cert may not be loaded yet when the first entries are written, and that must not permanently strip the thread id from every later entry
|
|
37
|
+
let cachedThreadId: string | undefined;
|
|
38
|
+
function getThreadIdSafe(): string | undefined {
|
|
39
|
+
if (cachedThreadId) return cachedThreadId;
|
|
40
|
+
try {
|
|
41
|
+
cachedThreadId = getOwnThreadId(getStorageServerConfigOptional()?.rootDomain || "");
|
|
42
|
+
} catch { }
|
|
43
|
+
return cachedThreadId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const cachedIp = lazy(() => firstExternalIPv4());
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
// threadId and domain are re-resolved until known (early entries may predate the config/certs); the rest never changes
|
|
49
|
+
function baseFields() {
|
|
50
|
+
return {
|
|
51
|
+
pid: process.pid,
|
|
52
|
+
threadId: getThreadIdSafe(),
|
|
53
|
+
entryPoint: process.argv[1],
|
|
54
|
+
ip: cachedIp(),
|
|
55
|
+
domain: getStorageServerConfigOptional()?.domain,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Everything logged here goes BOTH to the storage log stream and the console - one call, never two. The console line carries only the entry (the base fields are constant per process, so they would just be noise there); the stream gets everything.
|
|
60
|
+
function write(kind: string, entry: { [key: string]: unknown }, alsoConsole: boolean): void {
|
|
61
|
+
if (alsoConsole) {
|
|
62
|
+
console.log(`[${kind}] ${JSON.stringify(entry)}`);
|
|
63
|
+
}
|
|
47
64
|
let logs = getLogs();
|
|
48
65
|
if (!logs) return;
|
|
49
66
|
logs.log({ kind, time: Date.now(), ...baseFields(), ...entry });
|
|
50
67
|
}
|
|
51
68
|
|
|
52
|
-
/** One mutation the server performed: set/del/move/undelete/setLarge/routingConfig, plus the per-file synchronization writes ("sync get"/"sync set"). Sizes and times, never the data. internal marks writes pushed by a peer's synchronization rather than a client. */
|
|
69
|
+
/** One mutation the server performed: set/del/move/undelete/setLarge/routingConfig, plus the per-file synchronization writes ("sync get"/"sync set"). Sizes and times, never the data. internal marks writes pushed by a peer's synchronization rather than a client. Also printed to the console. */
|
|
53
70
|
export function logMutation(entry: { op: string; account: string; bucketName: string; store?: string; path: string; toPath?: string; size?: number; writeTime?: number; callerId?: string; internal?: boolean }): void {
|
|
54
|
-
write("mutation", entry);
|
|
71
|
+
write("mutation", entry, true);
|
|
55
72
|
}
|
|
56
73
|
|
|
57
|
-
/** A synchronization key point: scans and full syncs starting/finishing, reconciles, boundary scans - what an operator greps for to see whether the fleet is converging. */
|
|
74
|
+
/** A synchronization key point: scans and full syncs starting/finishing, reconciles, boundary scans - what an operator greps for to see whether the fleet is converging. Also printed to the console. */
|
|
58
75
|
export function logSyncEvent(entry: { event: string; store: string; source?: string; [key: string]: unknown }): void {
|
|
59
|
-
write("sync", entry);
|
|
76
|
+
write("sync", entry, true);
|
|
60
77
|
}
|
|
61
78
|
|
|
79
|
+
/** One console.error is all an error takes (console.error and console.warn are HOOKED to feed the stream) - this just guarantees the hook is installed first, for very-early callers. */
|
|
62
80
|
export function logStorageError(message: string): void {
|
|
63
|
-
|
|
81
|
+
getLogs();
|
|
82
|
+
console.error(message);
|
|
64
83
|
}
|
|
65
84
|
|
|
66
|
-
// Everything console.errored also lands in the log stream (with a reentrancy guard: a failure INSIDE logging must not log itself forever)
|
|
67
|
-
let
|
|
85
|
+
// Everything console.errored or console.warned also lands in the log stream (with a reentrancy guard: a failure INSIDE logging must not log itself forever)
|
|
86
|
+
let writingHooked = false;
|
|
68
87
|
const hookErrorLogging = lazy(() => {
|
|
69
|
-
let
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
for (let level of ["error", "warn"] as const) {
|
|
89
|
+
let original = console[level].bind(console);
|
|
90
|
+
console[level] = (...args: unknown[]) => {
|
|
91
|
+
original(...args);
|
|
92
|
+
if (writingHooked) return;
|
|
93
|
+
writingHooked = true;
|
|
94
|
+
try {
|
|
95
|
+
write(level, { message: args.map(x => typeof x === "string" && x || ((x as Error)?.stack ?? JSON.stringify(x))).join(" ").slice(0, 5000) }, false);
|
|
96
|
+
} catch { } finally {
|
|
97
|
+
writingHooked = false;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
80
101
|
});
|
|
81
102
|
|
|
82
103
|
/** The log files this server holds - see StreamingLogs.listFiles. Empty on processes with no storage folder. */
|
|
@@ -37,14 +37,16 @@ export declare class StoreSync {
|
|
|
37
37
|
waitForRequiredScans(): Promise<void>;
|
|
38
38
|
/** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
|
|
39
39
|
rescanBase(): Promise<void>;
|
|
40
|
+
/** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. */
|
|
41
|
+
private syncSource;
|
|
40
42
|
/** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
|
|
41
43
|
boundaryScanRemote(source: IArchives, config: {
|
|
42
44
|
since: number;
|
|
43
45
|
route?: [number, number];
|
|
44
46
|
}): Promise<void>;
|
|
45
47
|
private runSourceSync;
|
|
46
|
-
private
|
|
47
|
-
private
|
|
48
|
+
private pullSource;
|
|
49
|
+
private pushSource;
|
|
48
50
|
private updateScanIndex;
|
|
49
51
|
private pollChanges;
|
|
50
52
|
private copySourceFiles;
|
|
@@ -48,6 +48,8 @@ type SourceState = {
|
|
|
48
48
|
supportsChangesAfter: boolean;
|
|
49
49
|
initialScan: ReturnType<typeof promiseObj>;
|
|
50
50
|
scanComplete: boolean;
|
|
51
|
+
// Whether a scan of this source has ever actually SUCCEEDED - what full syncs gate on: without one, the index's view of this source can be arbitrarily stale (e.g. it was restarting when we first tried), and syncing from stale information churns forever
|
|
52
|
+
scanSucceeded: boolean;
|
|
51
53
|
// Files seen in this source's scans / change polls so far
|
|
52
54
|
scannedCount: number;
|
|
53
55
|
// Watermark for getChangesAfter2 polls
|
|
@@ -63,6 +65,7 @@ function newSourceState(): SourceState {
|
|
|
63
65
|
supportsChangesAfter: false,
|
|
64
66
|
initialScan: promiseObj(),
|
|
65
67
|
scanComplete: false,
|
|
68
|
+
scanSucceeded: false,
|
|
66
69
|
scannedCount: 0,
|
|
67
70
|
changesAfterTime: 0,
|
|
68
71
|
stopped: { stop: false },
|
|
@@ -75,10 +78,6 @@ type ScanTally = Record<ScanOutcome, number>;
|
|
|
75
78
|
function newScanTally(): ScanTally {
|
|
76
79
|
return { filtered: 0, new: 0, updated: 0, tombstone: 0, unchanged: 0 };
|
|
77
80
|
}
|
|
78
|
-
function formatScanTally(tally: ScanTally, total: number): string {
|
|
79
|
-
let pct = (n: number) => `${Math.round(n / Math.max(total, 1) * 1000) / 10}%`;
|
|
80
|
-
return `${tally.new} new paths (${pct(tally.new)}), ${tally.updated} newer writes (${pct(tally.updated)}), ${tally.tombstone} deletions (${pct(tally.tombstone)}), ${tally.unchanged} unchanged (${pct(tally.unchanged)}), ${tally.filtered} outside route (${pct(tally.filtered)})`;
|
|
81
|
-
}
|
|
82
81
|
|
|
83
82
|
export class StoreSync {
|
|
84
83
|
// Per source slot, in step with the store's sources array. Slots are never spliced out - a removed one is marked dead - so a slot number held by a running loop always means the same source.
|
|
@@ -220,13 +219,24 @@ export class StoreSync {
|
|
|
220
219
|
|
|
221
220
|
/** Rescans our own disk's metadata into the index - used around valid window handoffs, where another process wrote files to the shared folder that our index hasn't seen. */
|
|
222
221
|
public async rescanBase(): Promise<void> {
|
|
223
|
-
await this.
|
|
222
|
+
await this.syncSource(0);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** One synchronization round of a source: the PULL direction always (its listing, applied to our index), and with "push" the push direction too (what our index says the source is missing, written to it). Push is an argument rather than a separate call because it cannot run without the pull's listing - the index alone cannot say what the source already holds. Listings unblock (initialScan) between the halves, so they never wait behind a push. */
|
|
226
|
+
private async syncSource(sourceIndex: number, push?: "push"): Promise<void> {
|
|
227
|
+
let listing = await this.pullSource(sourceIndex);
|
|
228
|
+
let state = this.states[sourceIndex];
|
|
229
|
+
state.scanComplete = true;
|
|
230
|
+
state.initialScan.resolve(undefined);
|
|
231
|
+
if (push && !this.store.stopped.stop && !state.stopped.stop) {
|
|
232
|
+
await this.pushSource(sourceIndex, listing);
|
|
233
|
+
}
|
|
224
234
|
}
|
|
225
235
|
|
|
226
236
|
/** A boundary scan of the node that owned (part of) our route in the valid window before ours, when that node is different storage (a disk rescan can't see its writes): just its changes since the boundary neighborhood, with matching values pulled onto our own disk. */
|
|
227
237
|
public async boundaryScanRemote(source: IArchives, config: { since: number; route?: [number, number] }): Promise<void> {
|
|
228
238
|
let scanStart = Date.now();
|
|
229
|
-
|
|
239
|
+
logSyncEvent({ event: "boundaryScanStart", store: this.store.folder, source: source.getDebugName(), since: config.since, route: config.route || FULL_ROUTE });
|
|
230
240
|
let changes = await source.getChangesAfter2({ time: config.since, routes: config.route && [config.route] || undefined });
|
|
231
241
|
let tally = newScanTally();
|
|
232
242
|
for (let file of changes) {
|
|
@@ -246,7 +256,10 @@ export class StoreSync {
|
|
|
246
256
|
continue;
|
|
247
257
|
}
|
|
248
258
|
let copied = await copyArchiveFile({ from: source, to: this.store.sources[0].source, path: file.path, size: file.size, writeTime: file.createTime, forceSetImmutable: true, noChecks: true, internal: true });
|
|
249
|
-
if (!copied)
|
|
259
|
+
if (!copied) {
|
|
260
|
+
console.warn(`Boundary scan could not copy ${file.path} from ${source.getDebugName()} (store ${this.store.folder}): its change feed listed it (${file.size} bytes, writeTime ${new Date(file.createTime).toISOString()}) but the read found nothing`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
250
263
|
this.store.noteSyncTransfer("sync get", file.path, copied.size);
|
|
251
264
|
if (copied.size === 0) {
|
|
252
265
|
this.store.setIndexDeleted(file.path, copied.writeTime);
|
|
@@ -260,8 +273,7 @@ export class StoreSync {
|
|
|
260
273
|
tally.new++;
|
|
261
274
|
}
|
|
262
275
|
}
|
|
263
|
-
|
|
264
|
-
logSyncEvent({ event: "boundaryScanFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - scanStart, since: config.since, changes: changes.length, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone });
|
|
276
|
+
logSyncEvent({ event: "boundaryScanFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - scanStart, since: config.since, changes: changes.length, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone, unchanged: tally.unchanged });
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
// ── per-source loops ──
|
|
@@ -270,9 +282,10 @@ export class StoreSync {
|
|
|
270
282
|
await this.store.registerSlot(sourceIndex);
|
|
271
283
|
let { source } = this.store.sources[sourceIndex];
|
|
272
284
|
let state = this.states[sourceIndex];
|
|
285
|
+
// If a slot ever gets TWO of these, its loops are doubled - every poll and full sync runs twice
|
|
286
|
+
logSyncEvent({ event: "sourceSyncStart", store: this.store.folder, source: source.getDebugName(), sourceIndex, url: this.store.sources[sourceIndex].url, intermediate: this.store.sources[sourceIndex].intermediate });
|
|
273
287
|
// Read live for every pass, not captured - the store's source list can change while loops run
|
|
274
288
|
let noFullSync = () => this.store.sources[sourceIndex].noFullSync;
|
|
275
|
-
let listing: Map<string, number> | undefined;
|
|
276
289
|
// An intermediate is a deploy switchover's temporary alternate PORT onto a source we already have: the same bucket, reachable another way for a few minutes. Scanning it would list exactly what scanning that source lists, against a port that is about to disappear - so it is never scanned, and the source it was split out of covers it for as long as it exists and after it is gone.
|
|
277
290
|
if (this.store.sources[sourceIndex].intermediate) {
|
|
278
291
|
state.scanComplete = true;
|
|
@@ -283,19 +296,17 @@ export class StoreSync {
|
|
|
283
296
|
try {
|
|
284
297
|
let config = await source.getConfig();
|
|
285
298
|
state.supportsChangesAfter = !!config.supportsChangesAfter;
|
|
286
|
-
|
|
299
|
+
await this.syncSource(sourceIndex, "push");
|
|
287
300
|
break;
|
|
288
301
|
} catch (e) {
|
|
289
|
-
|
|
302
|
+
logSyncEvent({ event: "initialScanFailed", store: this.store.folder, source: source.getDebugName(), retryInMs: SCAN_RETRY_DELAY, error: String((e as Error).stack ?? e).slice(0, 2000) });
|
|
290
303
|
await delay(SCAN_RETRY_DELAY);
|
|
291
304
|
}
|
|
292
305
|
}
|
|
306
|
+
// The loop can also exit by the stop tokens, and nothing may wait forever on a stopped source's initial scan
|
|
293
307
|
state.scanComplete = true;
|
|
294
308
|
state.initialScan.resolve(undefined);
|
|
295
309
|
if (this.store.stopped.stop || state.stopped.stop) return;
|
|
296
|
-
if (listing) {
|
|
297
|
-
await this.reconcileSource(sourceIndex, listing);
|
|
298
|
-
}
|
|
299
310
|
if (!noFullSync()) {
|
|
300
311
|
try {
|
|
301
312
|
await this.copySourceFiles(sourceIndex);
|
|
@@ -305,31 +316,27 @@ export class StoreSync {
|
|
|
305
316
|
}
|
|
306
317
|
if (state.supportsChangesAfter) {
|
|
307
318
|
runInfinitePoll(CHANGES_POLL_INTERVAL, async () => {
|
|
319
|
+
// A scan that has not succeeded yet is retried HERE, before anything else - polling changes and copying against a never-scanned source would run on arbitrarily stale information
|
|
320
|
+
if (!state.scanSucceeded) {
|
|
321
|
+
await this.syncSource(sourceIndex, "push");
|
|
322
|
+
}
|
|
308
323
|
await this.pollChanges(sourceIndex);
|
|
309
324
|
if (!noFullSync()) await this.copySourceFiles(sourceIndex);
|
|
310
325
|
}, state.stopped);
|
|
311
|
-
// Change polls only show what the source HAS, never what it's missing, so pushes run on the full-rescan cadence (findInfo on an index-backed source is cheap)
|
|
312
|
-
runInfinitePoll(FULL_RESCAN_INTERVAL, async () => {
|
|
313
|
-
let files = await source.findInfo("");
|
|
314
|
-
await this.reconcileSource(sourceIndex, new Map(files.map(x => [x.path, x.createTime])));
|
|
315
|
-
}, state.stopped);
|
|
316
|
-
} else {
|
|
317
|
-
runInfinitePoll(FULL_RESCAN_UNINDEXED_INTERVAL, async () => {
|
|
318
|
-
let rescan = await this.scanSource(sourceIndex);
|
|
319
|
-
await this.reconcileSource(sourceIndex, rescan);
|
|
320
|
-
if (!noFullSync()) await this.copySourceFiles(sourceIndex);
|
|
321
|
-
}, state.stopped);
|
|
322
326
|
}
|
|
327
|
+
runInfinitePoll(FULL_RESCAN_UNINDEXED_INTERVAL, async () => {
|
|
328
|
+
await this.syncSource(sourceIndex, "push");
|
|
329
|
+
if (!noFullSync()) await this.copySourceFiles(sourceIndex);
|
|
330
|
+
}, state.stopped);
|
|
323
331
|
}
|
|
324
332
|
|
|
325
|
-
//
|
|
326
|
-
private async
|
|
333
|
+
// The PULL direction: a full metadata scan (size, writeTime, path) of one source, applied to our index. Returns the source's listing (path -> write time), which pushSource uses for the opposite direction.
|
|
334
|
+
private async pullSource(sourceIndex: number): Promise<Map<string, number>> {
|
|
327
335
|
let { source, route } = this.store.sources[sourceIndex];
|
|
328
336
|
let state = this.states[sourceIndex];
|
|
329
337
|
let scanStart = Date.now();
|
|
330
338
|
let activity: SyncActivity = { type: "metadataScan", sourceDebugName: source.getDebugName(), startTime: scanStart };
|
|
331
339
|
this.activities.add(activity);
|
|
332
|
-
console.log(`Metadata scan of ${source.getDebugName()} starting (store ${this.store.folder})`);
|
|
333
340
|
logSyncEvent({ event: "scanStart", store: this.store.folder, source: source.getDebugName() });
|
|
334
341
|
let progressTimer = setInterval(() => {
|
|
335
342
|
console.log(`Metadata scan of ${source.getDebugName()} still running (${Math.round((Date.now() - scanStart) / 1000)}s, store ${this.store.folder})`);
|
|
@@ -392,17 +399,14 @@ export class StoreSync {
|
|
|
392
399
|
if (!routeContains(route, getRoute(key))) continue;
|
|
393
400
|
missingOnSource++;
|
|
394
401
|
}
|
|
395
|
-
|
|
396
|
-
let union = indexSizeBefore + newPaths;
|
|
397
|
-
let pct = (n: number) => `${Math.round(n / Math.max(union, 1) * 1000) / 10}%`;
|
|
398
|
-
console.log(`Metadata scan of ${source.getDebugName()} finished in ${Math.round((Date.now() - scanStart) / 1000)}s (store ${this.store.folder}): ${files.length} listed vs ${indexSizeBefore} indexed (union ${union}): ${formatScanTally(tally, union)}, ${missingOnSource} in index but missing on source (${pct(missingOnSource)}), ${removedFromIndex} removed from index (${pct(removedFromIndex)})`);
|
|
399
|
-
logSyncEvent({ event: "scanFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - scanStart, listed: files.length, indexedBefore: indexSizeBefore, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone, missingOnSource, removedFromIndex });
|
|
402
|
+
logSyncEvent({ event: "scanFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - scanStart, listed: files.length, indexedBefore: indexSizeBefore, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone, unchanged: tally.unchanged, outsideRoute: tally.filtered, missingOnSource, removedFromIndex });
|
|
400
403
|
state.changesAfterTime = Math.max(state.changesAfterTime, scanStart - CHANGES_POLL_OVERLAP);
|
|
404
|
+
state.scanSucceeded = true;
|
|
401
405
|
return seen;
|
|
402
406
|
}
|
|
403
407
|
|
|
404
408
|
// The push direction of synchronization: everything we know that the source is missing (or holds an older copy of) is written to it — including deletions, as tombstone writes. This is what heals a source whose background writes failed (e.g. it was down): the next scan sees what's missing and re-sends it. A failing file is skipped, not fatal (immutable targets are handled by forceSetImmutable, and one unreadable value must not stop the rest of the pass) - only a run of consecutive failures (the source itself is down) aborts until the next scan cycle.
|
|
405
|
-
private async
|
|
409
|
+
private async pushSource(sourceIndex: number, listing: Map<string, number>): Promise<void> {
|
|
406
410
|
let { source, validWindows, route } = this.store.sources[sourceIndex];
|
|
407
411
|
let state = this.states[sourceIndex];
|
|
408
412
|
let acceptsWrites = windowsAcceptWrites(validWindows);
|
|
@@ -462,12 +466,8 @@ export class StoreSync {
|
|
|
462
466
|
}
|
|
463
467
|
if (failed) {
|
|
464
468
|
console.error(`Reconciling sync source ${source.getDebugName()} (store ${this.store.folder}): pushed ${pushed} files, ${failed} failed${aborted && ` before aborting the pass (${consecutiveFailures} consecutive failures - the source looks down; the next scan cycle retries)` || ""}. First errors: ${errors.join(" | ")}`);
|
|
465
|
-
} else if (pushed) {
|
|
466
|
-
console.log(`Reconciled sync source ${source.getDebugName()} (store ${this.store.folder}): pushed ${pushed} files it was missing or held older copies of`);
|
|
467
|
-
}
|
|
468
|
-
if (pushed || failed) {
|
|
469
|
-
logSyncEvent({ event: "reconcileFinish", store: this.store.folder, source: source.getDebugName(), pushed, failed, aborted });
|
|
470
469
|
}
|
|
470
|
+
logSyncEvent({ event: "reconcileFinish", store: this.store.folder, source: source.getDebugName(), pushed, failed, aborted });
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
private updateScanIndex(sourceIndex: number, file: ArchiveFileInfo): ScanOutcome {
|
|
@@ -503,9 +503,9 @@ export class StoreSync {
|
|
|
503
503
|
for (let file of changes) {
|
|
504
504
|
tally[this.updateScanIndex(sourceIndex, file)]++;
|
|
505
505
|
}
|
|
506
|
-
// Polls run constantly, so only the ones that actually changed the index get
|
|
506
|
+
// Polls run constantly, so only the ones that actually changed the index get an entry
|
|
507
507
|
if (tally.new || tally.updated || tally.tombstone) {
|
|
508
|
-
|
|
508
|
+
logSyncEvent({ event: "changesPoll", store: this.store.folder, source: source.getDebugName(), changes: changes.length, newPaths: tally.new, updated: tally.updated, tombstones: tally.tombstone, unchanged: tally.unchanged });
|
|
509
509
|
}
|
|
510
510
|
state.scannedCount += changes.length;
|
|
511
511
|
state.changesAfterTime = pollStart - CHANGES_POLL_OVERLAP;
|
|
@@ -535,7 +535,7 @@ export class StoreSync {
|
|
|
535
535
|
totalBytes,
|
|
536
536
|
};
|
|
537
537
|
this.activities.add(activity);
|
|
538
|
-
logSyncEvent({ event: "fullSyncStart", store: this.store.folder, source: source.getDebugName(), files: pending.length, bytes: totalBytes });
|
|
538
|
+
logSyncEvent({ event: "fullSyncStart", store: this.store.folder, source: source.getDebugName(), diffBased: state.supportsChangesAfter, files: pending.length, bytes: totalBytes });
|
|
539
539
|
let progressLogged = false;
|
|
540
540
|
let logProgress = () => {
|
|
541
541
|
progressLogged = true;
|
|
@@ -557,12 +557,14 @@ export class StoreSync {
|
|
|
557
557
|
console.warn(`Full sync from ${source.getDebugName()} (store ${this.store.folder}) has been running for ${formatTime(elapsed)}: ${doneFiles}/${pending.length} files (${(doneFiles / pending.length * 100).toFixed(1)}%), ${formatNumber(doneBytes)}B/${formatNumber(totalBytes)}B (${(totalBytes && doneBytes / totalBytes * 100 || 100).toFixed(1)}%), ${formatNumber(bytesPerSecond)}B/s. Estimated ${etaText}.`);
|
|
558
558
|
}, FULL_SYNC_SLOW_ERROR_INTERVAL);
|
|
559
559
|
(slowErrorTimer as { unref?: () => void }).unref?.();
|
|
560
|
+
let failed = 0;
|
|
561
|
+
let copiedFiles = 0;
|
|
562
|
+
let copiedBytes = 0;
|
|
563
|
+
let missingOnSource = 0;
|
|
564
|
+
let aborted = false;
|
|
560
565
|
try {
|
|
561
566
|
let nextIndex = 0;
|
|
562
|
-
let failed = 0;
|
|
563
567
|
let consecutiveFailures = 0;
|
|
564
|
-
let errors: string[] = [];
|
|
565
|
-
let aborted = false;
|
|
566
568
|
let copyWorker = async () => {
|
|
567
569
|
while (!aborted && !this.store.stopped.stop && !state.stopped.stop) {
|
|
568
570
|
let index = nextIndex++;
|
|
@@ -571,10 +573,26 @@ export class StoreSync {
|
|
|
571
573
|
try {
|
|
572
574
|
let copied = await copyArchiveFile({ from: source, to: this.store.sources[0].source, path: key, size: entry.size, writeTime: entry.writeTime, forceSetImmutable: true, noChecks: true, internal: true });
|
|
573
575
|
if (copied) {
|
|
576
|
+
copiedFiles++;
|
|
577
|
+
copiedBytes += copied.size;
|
|
574
578
|
this.store.noteSyncTransfer("sync get", key, copied.size);
|
|
575
|
-
//
|
|
576
|
-
if (this.
|
|
577
|
-
this.store.
|
|
579
|
+
// The copy carries the source's write time, and the index commits it under the normal ordering rule (>= the current time wins) - it refusing means a NEWER write landed while we copied, which must be said, not swallowed
|
|
580
|
+
if (!this.store.setIndexEntry(key, { writeTime: copied.writeTime, size: copied.size, sourcesListIndex: this.store.sourcesListIndexOfSlot(0) })) {
|
|
581
|
+
logSyncEvent({ event: "fullSyncCommitSuperseded", store: this.store.folder, source: source.getDebugName(), path: key, copiedWriteTime: copied.writeTime, currentWriteTime: this.store.currentWriteTime(key) });
|
|
582
|
+
}
|
|
583
|
+
} else {
|
|
584
|
+
missingOnSource++;
|
|
585
|
+
// The source's listing vouched for the file but its read has nothing - and asking again forever is not an answer. If our own disk holds a copy, IT is the real holder, so the entry repoints there; if neither side has bytes, the entry was simply wrong, and it is purged - forgotten, not deleted, exactly like a read that comes up empty everywhere (a scan re-finds the file if it exists anywhere else).
|
|
586
|
+
let local = await this.store.sources[0].source.getInfo(key);
|
|
587
|
+
if (local) {
|
|
588
|
+
this.store.setIndexEntry(key, { writeTime: entry.writeTime, size: local.size, sourcesListIndex: this.store.sourcesListIndexOfSlot(0) });
|
|
589
|
+
logSyncEvent({ event: "fullSyncMissingOnSource", store: this.store.folder, source: source.getDebugName(), path: key, expectedSize: entry.size, expectedWriteTime: entry.writeTime, resolution: "repointed to our local copy", localSize: local.size });
|
|
590
|
+
} else if (this.store.currentWriteTime(key) <= entry.writeTime) {
|
|
591
|
+
this.store.purgeIndexEntry(key);
|
|
592
|
+
logSyncEvent({ event: "fullSyncMissingOnSource", store: this.store.folder, source: source.getDebugName(), path: key, expectedSize: entry.size, expectedWriteTime: entry.writeTime, resolution: "purged - neither the source nor our disk has it, so it does not exist" });
|
|
593
|
+
} else {
|
|
594
|
+
// A newer write landed while we were checking - the entry is no longer the one we found missing, so it is left alone
|
|
595
|
+
logSyncEvent({ event: "fullSyncMissingOnSource", store: this.store.folder, source: source.getDebugName(), path: key, expectedSize: entry.size, expectedWriteTime: entry.writeTime, resolution: "kept - a newer write appeared while checking", currentWriteTime: this.store.currentWriteTime(key) });
|
|
578
596
|
}
|
|
579
597
|
}
|
|
580
598
|
consecutiveFailures = 0;
|
|
@@ -582,9 +600,7 @@ export class StoreSync {
|
|
|
582
600
|
// One failing file must not abort the sync: it stays on its source (reads still reach it there), and the next sync pass retries it
|
|
583
601
|
failed++;
|
|
584
602
|
consecutiveFailures++;
|
|
585
|
-
|
|
586
|
-
errors.push(`${key}: ${(e as Error).stack ?? e}`);
|
|
587
|
-
}
|
|
603
|
+
logSyncEvent({ event: "fullSyncCopyFailed", store: this.store.folder, source: source.getDebugName(), path: key, size: entry.size, error: String((e as Error).stack ?? e).slice(0, 2000) });
|
|
588
604
|
if (consecutiveFailures >= SYNC_MAX_CONSECUTIVE_FAILURES) {
|
|
589
605
|
aborted = true;
|
|
590
606
|
return;
|
|
@@ -601,9 +617,6 @@ export class StoreSync {
|
|
|
601
617
|
workers.push(copyWorker());
|
|
602
618
|
}
|
|
603
619
|
await Promise.all(workers);
|
|
604
|
-
if (failed) {
|
|
605
|
-
console.error(`Full sync from ${source.getDebugName()} (store ${this.store.folder}): ${failed} of ${pending.length} files failed to copy${aborted && ` before aborting the pass (${SYNC_MAX_CONSECUTIVE_FAILURES} consecutive failures - the source looks down; the next sync pass retries)` || " (they stay on their source, and the next sync pass retries them)"}. First errors: ${errors.join(" | ")}`);
|
|
606
|
-
}
|
|
607
620
|
} finally {
|
|
608
621
|
clearInterval(progressTimer);
|
|
609
622
|
clearInterval(slowErrorTimer);
|
|
@@ -612,7 +625,7 @@ export class StoreSync {
|
|
|
612
625
|
if (progressLogged) {
|
|
613
626
|
logProgress();
|
|
614
627
|
}
|
|
615
|
-
logSyncEvent({ event: "fullSyncFinish", store: this.store.folder, source: source.getDebugName(), durationMs: Date.now() - activity.startTime,
|
|
628
|
+
logSyncEvent({ event: "fullSyncFinish", store: this.store.folder, source: source.getDebugName(), diffBased: state.supportsChangesAfter, durationMs: Date.now() - activity.startTime, totalFiles: pending.length, totalBytes, processedFiles: activity.doneFiles, copiedFiles, copiedBytes, missingOnSource, failed, aborted });
|
|
616
629
|
}
|
|
617
630
|
}
|
|
618
631
|
|