sliftutils 1.7.47 → 1.7.49
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/index.d.ts +86 -35
- 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.d.ts +3 -4
- package/storage/ArchivesDisk.ts +9 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseBase.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseFormat.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseMerge.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/BulkDatabaseReader.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/LoadedIndex.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/WriteOverlay.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/blockCache.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeLock.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/mergeMarkers.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/streamLog.ts.cache +2 -2
- package/storage/BulkDatabase2/dist/syncClient.ts.cache +2 -2
- package/storage/IArchives.d.ts +21 -6
- package/storage/IArchives.ts +25 -6
- package/storage/backblaze.d.ts +3 -4
- package/storage/backblaze.ts +87 -35
- package/storage/dist/ArchivesDisk.ts.cache +4 -3
- package/storage/dist/FileFolderAPI.tsx.cache +2 -2
- package/storage/dist/IArchives.ts.cache +3 -3
- 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/TransactionStorage.ts.cache +2 -2
- package/storage/dist/backblaze.ts.cache +9 -3
- package/storage/dist/fileSystemPointer.ts.cache +2 -2
- package/storage/dist/remoteFileStorage.ts.cache +2 -2
- package/storage/remoteStorage/ArchivesRemote.d.ts +3 -5
- package/storage/remoteStorage/ArchivesRemote.ts +5 -5
- package/storage/remoteStorage/ArchivesUrl.d.ts +2 -1
- package/storage/remoteStorage/ArchivesUrl.ts +13 -3
- package/storage/remoteStorage/blobStore.d.ts +13 -4
- package/storage/remoteStorage/blobStore.ts +191 -105
- package/storage/remoteStorage/createArchives.d.ts +12 -7
- package/storage/remoteStorage/createArchives.ts +27 -18
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +4 -4
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +2 -2
- package/storage/remoteStorage/dist/blobStore.ts.cache +179 -92
- package/storage/remoteStorage/dist/cliArgs.ts.cache +2 -2
- package/storage/remoteStorage/dist/createArchives.ts.cache +22 -11
- package/storage/remoteStorage/dist/deployTakeover.ts.cache +2 -2
- package/storage/remoteStorage/dist/intermediateSources.ts.cache +2 -2
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +13 -6
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +9 -3
- package/storage/remoteStorage/dist/storageClientController.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageController.ts.cache +5 -5
- package/storage/remoteStorage/dist/storageServerState.ts.cache +47 -13
- package/storage/remoteStorage/remoteConfig.d.ts +3 -1
- package/storage/remoteStorage/remoteConfig.ts +20 -5
- package/storage/remoteStorage/sourceWrapper.d.ts +1 -0
- package/storage/remoteStorage/sourceWrapper.ts +7 -1
- package/storage/remoteStorage/sourcesList.d.ts +15 -0
- package/storage/remoteStorage/sourcesList.ts +69 -0
- package/storage/remoteStorage/storageController.d.ts +3 -3
- package/storage/remoteStorage/storageController.ts +6 -9
- package/storage/remoteStorage/storageServer.ts +1 -0
- package/storage/remoteStorage/storageServerState.d.ts +3 -0
- package/storage/remoteStorage/storageServerState.ts +37 -12
|
@@ -4,7 +4,7 @@ import { runInfinitePoll, delay } from "socket-function/src/batching";
|
|
|
4
4
|
import { timeInMinute, sort, promiseObj } from "socket-function/src/misc";
|
|
5
5
|
import { formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
6
6
|
import {
|
|
7
|
-
IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, assertValidLastModified,
|
|
7
|
+
IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, ChangesAfterConfig, assertValidLastModified,
|
|
8
8
|
windowAcceptsWrites, SyncActivity, FULL_ROUTE,
|
|
9
9
|
} from "../IArchives";
|
|
10
10
|
import { ArchivesDisk, applyFindInfoShape } from "../ArchivesDisk";
|
|
@@ -12,6 +12,7 @@ import { ArchivesBackblaze } from "../backblaze";
|
|
|
12
12
|
import { ROUTING_FILE, getRoute, routeContains } from "./remoteConfig";
|
|
13
13
|
import { BulkDatabaseBase, noopReactiveDeps } from "../BulkDatabase2/BulkDatabaseBase";
|
|
14
14
|
import { wrapHandle, NodeJSDirectoryHandleWrapper, DirectoryWrapper } from "../FileFolderAPI";
|
|
15
|
+
import { SourcesList } from "./sourcesList";
|
|
15
16
|
|
|
16
17
|
// The storage engine of the remote storage server. Data lives in synchronization sources (at minimum an ArchivesDisk, the local disk); BlobStore keeps an index of every file (path, last modified time, size, and which source currently holds the data) in a BulkDatabase2, and synchronizes the index from all sources (see ArchivesSource in IArchives.ts). Every startup fully rescans each source's metadata, so the index self-heals; the file with the highest write time wins across all sources, so multiple sources need no stacking order.
|
|
17
18
|
|
|
@@ -21,10 +22,11 @@ const FAST_FLUSH_POLL = 1000 * 15;
|
|
|
21
22
|
export const WINDOW_END_FLUSH_MARGIN = timeInMinute * 5;
|
|
22
23
|
// Index changes are buffered in memory and written to the BulkDatabase2 in batches
|
|
23
24
|
const INDEX_FLUSH_INTERVAL = 1000 * 30;
|
|
24
|
-
// Sources
|
|
25
|
+
// Sources with a native (index-backed) change feed are polled this often
|
|
25
26
|
const CHANGES_POLL_INTERVAL = 1000 * 60;
|
|
26
|
-
//
|
|
27
|
+
// Full metadata rescans. supportsChangesAfter is the heuristic for "one of our own storage servers": their index-backed listings are cheap, so hourly is fine. Everything else (backblaze, plain disk) pays the full listing cost, so it rescans much less often.
|
|
27
28
|
const FULL_RESCAN_INTERVAL = 1000 * 60 * 60;
|
|
29
|
+
const FULL_RESCAN_UNINDEXED_INTERVAL = 1000 * 60 * 60 * 6;
|
|
28
30
|
// On a request for a file the index doesn't know, changes-after sources are re-polled, at most this often
|
|
29
31
|
const MISS_CHECK_INTERVAL = 1000 * 5;
|
|
30
32
|
// Change polls re-request this much overlap, so clock skew between us and a source can't drop changes
|
|
@@ -40,6 +42,9 @@ const DISK_LIMIT_CHECK_INTERVAL = 1000 * 60;
|
|
|
40
42
|
const FULL_SYNC_PARALLEL = 8;
|
|
41
43
|
// A full sync running longer than this is console.errored (and again every interval after), so a sync that will take days is loud instead of a quiet console.log every minute
|
|
42
44
|
const FULL_SYNC_SLOW_ERROR_INTERVAL = 1000 * 60 * 60;
|
|
45
|
+
// A reconcile pass skips failing files (one bad value must not stop the rest), but this many failures in a row means the target itself is down, so the pass aborts until the next scan cycle
|
|
46
|
+
const RECONCILE_MAX_CONSECUTIVE_FAILURES = 5;
|
|
47
|
+
const RECONCILE_ERROR_LOG_LIMIT = 3;
|
|
43
48
|
|
|
44
49
|
export type WriteConfig = {
|
|
45
50
|
// Resolve once the write is in memory; flush to the sources after writeDelay, coalescing writes to the same key (only the latest is written). Data is lost if the process crashes first.
|
|
@@ -57,7 +62,7 @@ export type IBucketStore = {
|
|
|
57
62
|
del(fileName: string, config?: WriteConfig): Promise<void>;
|
|
58
63
|
getInfo(fileName: string): Promise<{ writeTime: number; size: number } | undefined>;
|
|
59
64
|
findInfo(prefix: string, config?: { shallow?: boolean; type?: "files" | "folders" }): Promise<ArchiveFileInfo[]>;
|
|
60
|
-
|
|
65
|
+
getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]>;
|
|
61
66
|
getSyncStatus?(): Promise<ArchivesSyncStatus>;
|
|
62
67
|
getSyncProgress?(): {
|
|
63
68
|
index: { fileCount: number; byteCount: number };
|
|
@@ -84,14 +89,14 @@ type BlobIndexEntry = {
|
|
|
84
89
|
key: string;
|
|
85
90
|
writeTime: number;
|
|
86
91
|
size: number;
|
|
87
|
-
// Which synchronization source currently holds the data (
|
|
88
|
-
|
|
92
|
+
// Which synchronization source currently holds the data: the line number of the source's URL in the store's append-only sources list (see SourcesList) - NOT a position in the in-memory sources array, which changes between runs
|
|
93
|
+
sourcesListIndex: number;
|
|
89
94
|
};
|
|
90
95
|
type IndexEntry = {
|
|
91
96
|
writeTime: number;
|
|
92
97
|
size: number;
|
|
93
|
-
|
|
94
|
-
// When WE last changed this entry (not the file's write time) — what
|
|
98
|
+
sourcesListIndex: number;
|
|
99
|
+
// When WE last changed this entry (not the file's write time) — what getChangesAfter2 filters on, so late-arriving files with old write times are still reported as changes
|
|
95
100
|
changedAt: number;
|
|
96
101
|
// In-memory only (not persisted): when the file was last served, for readerDiskLimit's LRU eviction. Starts as changedAt on load.
|
|
97
102
|
lastAccess: number;
|
|
@@ -103,12 +108,12 @@ type SourceState = {
|
|
|
103
108
|
scanComplete: boolean;
|
|
104
109
|
// Files seen in this source's scans / change polls so far
|
|
105
110
|
scannedCount: number;
|
|
106
|
-
// Watermark for
|
|
111
|
+
// Watermark for getChangesAfter2 polls
|
|
107
112
|
changesAfterTime: number;
|
|
108
113
|
lastMissCheck: number;
|
|
109
114
|
// Per-slot stop token: a removed source's loops stop without touching the rest of the store
|
|
110
115
|
stopped: { stop: boolean };
|
|
111
|
-
//
|
|
116
|
+
// A removed source's slot stays in the arrays (they are never spliced, so slot numbers held by running loops stay meaningful), marked dead - never scanned, written, or read. Index entries don't reference slots at all; they persist the sources list's sourcesListIndex.
|
|
112
117
|
dead?: boolean;
|
|
113
118
|
};
|
|
114
119
|
|
|
@@ -116,6 +121,8 @@ type SourceState = {
|
|
|
116
121
|
export type BlobSourceSpec = {
|
|
117
122
|
// Matched against ArchivesSource.identity ("disk" for the base slot); equal identities pair off in order
|
|
118
123
|
identity: string;
|
|
124
|
+
// See ArchivesSource.url
|
|
125
|
+
url: string;
|
|
119
126
|
validWindow: [number, number];
|
|
120
127
|
route?: [number, number];
|
|
121
128
|
noFullSync?: boolean;
|
|
@@ -157,13 +164,15 @@ export class BlobStore implements IBucketStore {
|
|
|
157
164
|
readerDiskLimit?: number;
|
|
158
165
|
// Every accepted write ("original") and every write that actually reached the sources ("flushed"). Fast writes coalesce, so the two counts differ.
|
|
159
166
|
onWriteCounted?: (kind: "original" | "flushed", bytes: number) => void;
|
|
167
|
+
// Resolves a persisted source URL (see ArchivesSource.url) to a cached IArchives, so entries whose holder is no longer configured can still be read
|
|
168
|
+
resolveSourceUrl?: (url: string) => IArchives;
|
|
160
169
|
}
|
|
161
170
|
) { }
|
|
162
171
|
|
|
163
172
|
private stopped = { stop: false };
|
|
164
173
|
|
|
165
|
-
// The index's BulkDatabase2 files live under <folder>/index
|
|
166
|
-
private index = new BulkDatabaseBase<BlobIndexEntry>("
|
|
174
|
+
// The index's BulkDatabase2 files live under <folder>/index. "blobIndex2": the "blobIndex" generation persisted sources-array positions as the holding source, which are not stable across runs - its entries are unusable, so it is simply never read again.
|
|
175
|
+
private index = new BulkDatabaseBase<BlobIndexEntry>("blobIndex2", noopReactiveDeps, async (p: string) => {
|
|
167
176
|
let base: DirectoryWrapper = new NodeJSDirectoryHandleWrapper(path.join(this.folder, "index"));
|
|
168
177
|
for (let part of p.split("/")) {
|
|
169
178
|
if (part) base = await base.getDirectoryHandle(part, { create: true });
|
|
@@ -184,12 +193,56 @@ export class BlobStore implements IBucketStore {
|
|
|
184
193
|
private overlay = new Map<string, OverlayEntry>();
|
|
185
194
|
private sourceStates = this.sources.map(() => newSourceState());
|
|
186
195
|
private syncStarted = false;
|
|
196
|
+
// The persistent identities behind IndexEntry.sourcesListIndex (see SourcesList)
|
|
197
|
+
private sourcesList = new SourcesList(path.join(this.folder, "index", "sourcesList.txt"));
|
|
198
|
+
// Per slot: the persistent sourcesListIndex of that slot's URL, filled by registerSlot before the slot's sync runs
|
|
199
|
+
private slotSourcesListIndexes: number[] = [];
|
|
200
|
+
private slotRegistrations: Promise<void>[] = [];
|
|
187
201
|
|
|
188
202
|
private isLive(sourceIndex: number): boolean {
|
|
189
203
|
return !!this.sources[sourceIndex] && !this.sourceStates[sourceIndex].dead;
|
|
190
204
|
}
|
|
191
205
|
|
|
206
|
+
private registerSlot(slot: number): Promise<void> {
|
|
207
|
+
let existing = this.slotRegistrations[slot];
|
|
208
|
+
if (existing) return existing;
|
|
209
|
+
let registration = this.sourcesList.ensure(this.sources[slot].url).then(index => {
|
|
210
|
+
this.slotSourcesListIndexes[slot] = index;
|
|
211
|
+
});
|
|
212
|
+
this.slotRegistrations[slot] = registration;
|
|
213
|
+
return registration;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// The persistent sourcesListIndex of a slot - only valid once the slot's registration resolved (init and runSourceSync guarantee that before any indexing happens)
|
|
217
|
+
private sourcesListIndexOfSlot(slot: number): number {
|
|
218
|
+
let index = this.slotSourcesListIndexes[slot];
|
|
219
|
+
if (index === undefined) {
|
|
220
|
+
throw new Error(`Source slot ${slot} (${this.sources[slot]?.url}) has no registered sourcesListIndex yet (store ${this.folder})`);
|
|
221
|
+
}
|
|
222
|
+
return index;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// The live slot currently serving a persistent sourcesListIndex, or undefined when no configured source has that URL anymore. Linear, but the sources list is tiny and this is always current (slots dying, or several slots sharing one URL across valid windows, need no bookkeeping).
|
|
226
|
+
private slotForSourcesListIndex(sourcesListIndex: number): number | undefined {
|
|
227
|
+
for (let i = 0; i < this.slotSourcesListIndexes.length; i++) {
|
|
228
|
+
if (this.slotSourcesListIndexes[i] === sourcesListIndex && this.isLive(i)) return i;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// The IArchives currently holding an entry's bytes: the live slot when the holder is still configured, otherwise resolved (cached) straight from its persisted URL - windows/routes decide when a source is scanned or written, but for reading bytes we know it holds, the URL alone is enough
|
|
234
|
+
private async getEntryHolder(entry: IndexEntry): Promise<IArchives | undefined> {
|
|
235
|
+
let slot = this.slotForSourcesListIndex(entry.sourcesListIndex);
|
|
236
|
+
if (slot !== undefined) return this.sources[slot].source;
|
|
237
|
+
let url = this.sourcesList.getUrl(entry.sourcesListIndex) || await this.sourcesList.getUrlReloading(entry.sourcesListIndex);
|
|
238
|
+
if (!url) return undefined;
|
|
239
|
+
return this.config?.resolveSourceUrl?.(url);
|
|
240
|
+
}
|
|
241
|
+
|
|
192
242
|
public init = lazy(async () => {
|
|
243
|
+
for (let i = 0; i < this.sources.length; i++) {
|
|
244
|
+
await this.registerSlot(i);
|
|
245
|
+
}
|
|
193
246
|
await this.loadIndex();
|
|
194
247
|
this.syncStarted = true;
|
|
195
248
|
for (let i = 0; i < this.sources.length; i++) {
|
|
@@ -215,23 +268,23 @@ export class BlobStore implements IBucketStore {
|
|
|
215
268
|
}
|
|
216
269
|
|
|
217
270
|
private async loadIndex(): Promise<void> {
|
|
218
|
-
let [writeTimes, sizes,
|
|
271
|
+
let [writeTimes, sizes, sourcesListIndexes] = await Promise.all([
|
|
219
272
|
this.index.getColumn("writeTime"),
|
|
220
273
|
this.index.getColumn("size"),
|
|
221
|
-
this.index.getColumn("
|
|
274
|
+
this.index.getColumn("sourcesListIndex"),
|
|
222
275
|
]);
|
|
223
276
|
let sizeMap = new Map(sizes.map(x => [x.key, x.value]));
|
|
224
|
-
let
|
|
277
|
+
let sourcesListIndexMap = new Map(sourcesListIndexes.map(x => [x.key, x.value]));
|
|
225
278
|
for (let entry of writeTimes) {
|
|
226
279
|
let size = sizeMap.get(entry.key);
|
|
227
|
-
let
|
|
228
|
-
// Explicit checks, as 0 is a valid size and a valid
|
|
229
|
-
if (size === undefined ||
|
|
230
|
-
// The routing config is only ever read off our own disk (see
|
|
280
|
+
let sourcesListIndex = sourcesListIndexMap.get(entry.key);
|
|
281
|
+
// Explicit checks, as 0 is a valid size and a valid sourcesListIndex
|
|
282
|
+
if (size === undefined || sourcesListIndex === undefined) continue;
|
|
283
|
+
// The routing config is only ever read off our own disk (see updateScanIndex), and a loaded bucket always has it there - a persisted entry pointing elsewhere is stale
|
|
231
284
|
if (entry.key === ROUTING_FILE) {
|
|
232
|
-
|
|
285
|
+
sourcesListIndex = this.sourcesListIndexOfSlot(0);
|
|
233
286
|
}
|
|
234
|
-
let full: IndexEntry = { writeTime: entry.value, size,
|
|
287
|
+
let full: IndexEntry = { writeTime: entry.value, size, sourcesListIndex, changedAt: entry.time, lastAccess: entry.time };
|
|
235
288
|
this.mem.set(entry.key, full);
|
|
236
289
|
this.countEntry(full, 1);
|
|
237
290
|
}
|
|
@@ -241,14 +294,15 @@ export class BlobStore implements IBucketStore {
|
|
|
241
294
|
if (!entry || entry.size === 0) return;
|
|
242
295
|
this.indexFileCount += direction;
|
|
243
296
|
this.indexByteCount += entry.size * direction;
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.
|
|
297
|
+
// Entries can reference a source no longer configured (readable via getEntryHolder, but with no slot to count under)
|
|
298
|
+
let slot = this.slotForSourcesListIndex(entry.sourcesListIndex);
|
|
299
|
+
if (slot !== undefined) {
|
|
300
|
+
this.sourceFileCounts[slot] += direction;
|
|
301
|
+
this.sourceByteCounts[slot] += entry.size * direction;
|
|
248
302
|
}
|
|
249
303
|
}
|
|
250
304
|
|
|
251
|
-
private setIndexEntry(key: string, entry: { writeTime: number; size: number;
|
|
305
|
+
private setIndexEntry(key: string, entry: { writeTime: number; size: number; sourcesListIndex: number }): void {
|
|
252
306
|
let full: IndexEntry = { ...entry, changedAt: Date.now(), lastAccess: Date.now() };
|
|
253
307
|
this.countEntry(this.mem.get(key), -1);
|
|
254
308
|
this.countEntry(full, 1);
|
|
@@ -305,7 +359,7 @@ export class BlobStore implements IBucketStore {
|
|
|
305
359
|
continue;
|
|
306
360
|
}
|
|
307
361
|
let source = spec.create();
|
|
308
|
-
this.sources.push({ source, validWindow: spec.validWindow, route: spec.route, noFullSync: spec.noFullSync, identity: spec.identity });
|
|
362
|
+
this.sources.push({ source, url: spec.url, validWindow: spec.validWindow, route: spec.route, noFullSync: spec.noFullSync, identity: spec.identity });
|
|
309
363
|
this.sourceStates.push(newSourceState());
|
|
310
364
|
this.sourceFileCounts.push(0);
|
|
311
365
|
this.sourceByteCounts.push(0);
|
|
@@ -341,13 +395,19 @@ export class BlobStore implements IBucketStore {
|
|
|
341
395
|
state.stopped.stop = true;
|
|
342
396
|
state.scanComplete = true;
|
|
343
397
|
state.initialScan.resolve(undefined);
|
|
344
|
-
let
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
398
|
+
let sourcesListIndex = this.slotSourcesListIndexes[sourceIndex];
|
|
399
|
+
// The same URL can be another live slot (one entry per valid window) - the endpoint is still configured, so its entries stay
|
|
400
|
+
if (sourcesListIndex !== undefined && this.slotForSourcesListIndex(sourcesListIndex) === undefined) {
|
|
401
|
+
let dropped = 0;
|
|
402
|
+
for (let [key, entry] of this.mem) {
|
|
403
|
+
if (entry.sourcesListIndex !== sourcesListIndex) continue;
|
|
404
|
+
this.deleteIndexEntry(key);
|
|
405
|
+
dropped++;
|
|
406
|
+
}
|
|
407
|
+
console.log(`Removed sync source ${source.getDebugName()} (store ${this.folder}): its scans are stopped and ${dropped} index entries it held were dropped`);
|
|
408
|
+
return;
|
|
349
409
|
}
|
|
350
|
-
console.log(`Removed sync source ${source.getDebugName()} (store ${this.folder}): its scans are stopped
|
|
410
|
+
console.log(`Removed sync source ${source.getDebugName()} (store ${this.folder}): its scans are stopped (its URL is still served by another slot, so its index entries stay)`);
|
|
351
411
|
}
|
|
352
412
|
|
|
353
413
|
/** 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. */
|
|
@@ -361,15 +421,10 @@ export class BlobStore implements IBucketStore {
|
|
|
361
421
|
await this.init();
|
|
362
422
|
let scanStart = Date.now();
|
|
363
423
|
console.log(`Boundary scan of ${source.getDebugName()} starting: changes since ${new Date(config.since).toISOString()}, route ${JSON.stringify(config.route || FULL_ROUTE)} (store ${this.folder})`);
|
|
364
|
-
let changes:
|
|
365
|
-
if (source.getChangesAfter) {
|
|
366
|
-
changes = await source.getChangesAfter(config.since);
|
|
367
|
-
} else {
|
|
368
|
-
changes = (await source.findInfo("")).filter(x => x.createTime > config.since);
|
|
369
|
-
}
|
|
424
|
+
let changes = await source.getChangesAfter2({ time: config.since, routes: config.route && [config.route] || undefined });
|
|
370
425
|
let tally = newScanTally();
|
|
371
426
|
for (let file of changes) {
|
|
372
|
-
if (file.path === ROUTING_FILE
|
|
427
|
+
if (file.path === ROUTING_FILE) {
|
|
373
428
|
tally.filtered++;
|
|
374
429
|
continue;
|
|
375
430
|
}
|
|
@@ -382,19 +437,19 @@ export class BlobStore implements IBucketStore {
|
|
|
382
437
|
}
|
|
383
438
|
if (file.size === 0) {
|
|
384
439
|
// A tombstone stores nothing on our own source - the index entry alone records it
|
|
385
|
-
this.setIndexEntry(file.path, { writeTime: file.createTime, size: 0,
|
|
440
|
+
this.setIndexEntry(file.path, { writeTime: file.createTime, size: 0, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
386
441
|
tally.tombstone++;
|
|
387
442
|
continue;
|
|
388
443
|
}
|
|
389
444
|
let result = await source.get2(file.path);
|
|
390
445
|
if (!result) continue;
|
|
391
446
|
if (result.data.length === 0) {
|
|
392
|
-
this.setIndexEntry(file.path, { writeTime: result.writeTime, size: 0,
|
|
447
|
+
this.setIndexEntry(file.path, { writeTime: result.writeTime, size: 0, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
393
448
|
tally.tombstone++;
|
|
394
449
|
continue;
|
|
395
450
|
}
|
|
396
|
-
await this.sources[0].source.set(file.path, result.data, { lastModified: result.writeTime });
|
|
397
|
-
this.setIndexEntry(file.path, { writeTime: result.writeTime, size: result.data.length,
|
|
451
|
+
await this.sources[0].source.set(file.path, result.data, { lastModified: result.writeTime, forceSetImmutable: true, noChecks: true });
|
|
452
|
+
this.setIndexEntry(file.path, { writeTime: result.writeTime, size: result.data.length, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
398
453
|
if (entry || overlayEntry) {
|
|
399
454
|
tally.updated++;
|
|
400
455
|
} else {
|
|
@@ -437,10 +492,10 @@ export class BlobStore implements IBucketStore {
|
|
|
437
492
|
if (entry.size === 0) continue;
|
|
438
493
|
fileCount++;
|
|
439
494
|
byteCount += entry.size;
|
|
440
|
-
let
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
|
|
495
|
+
let slot = this.slotForSourcesListIndex(entry.sourcesListIndex);
|
|
496
|
+
if (slot !== undefined) {
|
|
497
|
+
sources[slot].fileCount++;
|
|
498
|
+
sources[slot].byteCount += entry.size;
|
|
444
499
|
}
|
|
445
500
|
}
|
|
446
501
|
return { fileCount, byteCount, sources: sources.filter((x, i) => this.isLive(i)) };
|
|
@@ -454,7 +509,7 @@ export class BlobStore implements IBucketStore {
|
|
|
454
509
|
let deletes: string[] = [];
|
|
455
510
|
for (let [key, entry] of dirty) {
|
|
456
511
|
if (entry) {
|
|
457
|
-
writes.push({ key, writeTime: entry.writeTime, size: entry.size,
|
|
512
|
+
writes.push({ key, writeTime: entry.writeTime, size: entry.size, sourcesListIndex: entry.sourcesListIndex });
|
|
458
513
|
} else {
|
|
459
514
|
deletes.push(key);
|
|
460
515
|
}
|
|
@@ -466,6 +521,7 @@ export class BlobStore implements IBucketStore {
|
|
|
466
521
|
// ── synchronization ──
|
|
467
522
|
|
|
468
523
|
private async runSourceSync(sourceIndex: number): Promise<void> {
|
|
524
|
+
await this.registerSlot(sourceIndex);
|
|
469
525
|
let { source } = this.sources[sourceIndex];
|
|
470
526
|
let state = this.sourceStates[sourceIndex];
|
|
471
527
|
// Read live for every pass, not captured - updateSources can change it while loops run
|
|
@@ -474,7 +530,7 @@ export class BlobStore implements IBucketStore {
|
|
|
474
530
|
while (!this.stopped.stop && !state.stopped.stop) {
|
|
475
531
|
try {
|
|
476
532
|
let config = await source.getConfig();
|
|
477
|
-
state.supportsChangesAfter = !!
|
|
533
|
+
state.supportsChangesAfter = !!config.supportsChangesAfter;
|
|
478
534
|
listing = await this.scanSource(sourceIndex);
|
|
479
535
|
break;
|
|
480
536
|
} catch (e) {
|
|
@@ -506,7 +562,7 @@ export class BlobStore implements IBucketStore {
|
|
|
506
562
|
await this.reconcileSource(sourceIndex, new Map(files.map(x => [x.path, x.createTime])));
|
|
507
563
|
}, state.stopped);
|
|
508
564
|
} else {
|
|
509
|
-
runInfinitePoll(
|
|
565
|
+
runInfinitePoll(FULL_RESCAN_UNINDEXED_INTERVAL, async () => {
|
|
510
566
|
let rescan = await this.scanSource(sourceIndex);
|
|
511
567
|
await this.reconcileSource(sourceIndex, rescan);
|
|
512
568
|
if (!noFullSync()) await this.copySourceFiles(sourceIndex);
|
|
@@ -526,6 +582,7 @@ export class BlobStore implements IBucketStore {
|
|
|
526
582
|
console.log(`Metadata scan of ${source.getDebugName()} still running (${Math.round((Date.now() - scanStart) / 1000)}s, store ${this.folder})`);
|
|
527
583
|
}, SYNC_PROGRESS_LOG_INTERVAL);
|
|
528
584
|
(progressTimer as { unref?: () => void }).unref?.();
|
|
585
|
+
// The listing request deliberately takes no time or route filters: our slowest sources (backblaze) support neither, so filtering would happen after the full fetch anyway - little benefit, more room for desynchronization. And if a full listing ever becomes too big to send over the network, it is also too big for the receiving process to hold in memory - the fix is more routing shards (each storing and sending less), not filtering.
|
|
529
586
|
let files: ArchiveFileInfo[];
|
|
530
587
|
try {
|
|
531
588
|
files = await source.findInfo("");
|
|
@@ -544,15 +601,16 @@ export class BlobStore implements IBucketStore {
|
|
|
544
601
|
if (!this.mem.has(file.path)) {
|
|
545
602
|
newPaths++;
|
|
546
603
|
}
|
|
547
|
-
tally[this.
|
|
604
|
+
tally[this.updateScanIndex(sourceIndex, file)]++;
|
|
548
605
|
}
|
|
549
606
|
state.scannedCount = files.length;
|
|
550
607
|
// Index entries this source was the holder of, but that vanished from it (e.g. deleted while we were offline), come out of the index. Entries changed after the scan started are kept — the scan listing may simply predate them. Tombstones have no physical file for a listing to vouch for, so they're exempt (cleanupTombstones expires them instead).
|
|
551
608
|
let removedFromIndex = 0;
|
|
552
609
|
let missingOnSource = 0;
|
|
610
|
+
let scannedSourcesListIndex = this.sourcesListIndexOfSlot(sourceIndex);
|
|
553
611
|
for (let [key, entry] of this.mem) {
|
|
554
612
|
if (seen.has(key)) continue;
|
|
555
|
-
if (entry.
|
|
613
|
+
if (entry.sourcesListIndex === scannedSourcesListIndex && entry.size !== 0 && entry.changedAt < scanStart) {
|
|
556
614
|
this.deleteIndexEntry(key);
|
|
557
615
|
removedFromIndex++;
|
|
558
616
|
continue;
|
|
@@ -571,38 +629,62 @@ export class BlobStore implements IBucketStore {
|
|
|
571
629
|
return seen;
|
|
572
630
|
}
|
|
573
631
|
|
|
574
|
-
// 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.
|
|
632
|
+
// 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.
|
|
575
633
|
private async reconcileSource(sourceIndex: number, listing: Map<string, number>): Promise<void> {
|
|
576
634
|
let { source, validWindow, route } = this.sources[sourceIndex];
|
|
577
635
|
let state = this.sourceStates[sourceIndex];
|
|
578
636
|
let acceptsWrites = windowAcceptsWrites(validWindow);
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
637
|
+
let targetSourcesListIndex = this.sourcesListIndexOfSlot(sourceIndex);
|
|
638
|
+
let pushed = 0;
|
|
639
|
+
let failed = 0;
|
|
640
|
+
let consecutiveFailures = 0;
|
|
641
|
+
let errors: string[] = [];
|
|
642
|
+
let aborted = false;
|
|
643
|
+
for (let [key, entry] of this.mem) {
|
|
644
|
+
if (this.stopped.stop || state.stopped.stop) return;
|
|
645
|
+
if (entry.sourcesListIndex === targetSourcesListIndex) continue;
|
|
646
|
+
// The routing file is NEVER synchronized between storage nodes - it is only ever written directly to each node, and only ever read off our own disk
|
|
647
|
+
if (key === ROUTING_FILE) continue;
|
|
648
|
+
if (!acceptsWrites) continue;
|
|
649
|
+
if (!routeContains(route, getRoute(key))) continue;
|
|
650
|
+
let theirTime = listing.get(key);
|
|
651
|
+
if (theirTime !== undefined && theirTime >= entry.writeTime) continue;
|
|
652
|
+
try {
|
|
589
653
|
if (entry.size === 0) {
|
|
590
654
|
// A deletion only needs pushing while the source still holds an older copy
|
|
591
655
|
if (theirTime === undefined) continue;
|
|
592
|
-
await source.set(key, Buffer.alloc(0), { lastModified: entry.writeTime });
|
|
656
|
+
await source.set(key, Buffer.alloc(0), { lastModified: entry.writeTime, forceSetImmutable: true, noChecks: true });
|
|
657
|
+
pushed++;
|
|
658
|
+
consecutiveFailures = 0;
|
|
593
659
|
continue;
|
|
594
660
|
}
|
|
595
|
-
let
|
|
661
|
+
let holder = await this.getEntryHolder(entry);
|
|
662
|
+
if (!holder) continue;
|
|
663
|
+
let result = await holder.get2(key);
|
|
596
664
|
if (!result) continue;
|
|
597
|
-
await source.set(key, result.data, { lastModified: result.writeTime });
|
|
665
|
+
await source.set(key, result.data, { lastModified: result.writeTime, forceSetImmutable: true, noChecks: true });
|
|
666
|
+
pushed++;
|
|
667
|
+
consecutiveFailures = 0;
|
|
668
|
+
} catch (e) {
|
|
669
|
+
failed++;
|
|
670
|
+
consecutiveFailures++;
|
|
671
|
+
if (errors.length < RECONCILE_ERROR_LOG_LIMIT) {
|
|
672
|
+
errors.push(`${key}: ${(e as Error).stack ?? e}`);
|
|
673
|
+
}
|
|
674
|
+
if (consecutiveFailures >= RECONCILE_MAX_CONSECUTIVE_FAILURES) {
|
|
675
|
+
aborted = true;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
598
678
|
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
console.error(`Reconciling sync source ${source.getDebugName()}
|
|
679
|
+
}
|
|
680
|
+
if (failed) {
|
|
681
|
+
console.error(`Reconciling sync source ${source.getDebugName()} (store ${this.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(" | ")}`);
|
|
682
|
+
} else if (pushed) {
|
|
683
|
+
console.log(`Reconciled sync source ${source.getDebugName()} (store ${this.folder}): pushed ${pushed} files it was missing or held older copies of`);
|
|
602
684
|
}
|
|
603
685
|
}
|
|
604
686
|
|
|
605
|
-
private
|
|
687
|
+
private updateScanIndex(sourceIndex: number, file: ArchiveFileInfo): ScanOutcome {
|
|
606
688
|
// An in-flight scan can outlive its source's removal; its results are dead
|
|
607
689
|
if (!this.isLive(sourceIndex)) return "filtered";
|
|
608
690
|
if (file.path === ROUTING_FILE) {
|
|
@@ -618,21 +700,20 @@ export class BlobStore implements IBucketStore {
|
|
|
618
700
|
let existing = this.mem.get(file.path);
|
|
619
701
|
// The highest write time wins across all sources (ties keep the existing entry)
|
|
620
702
|
if (existing && file.createTime <= existing.writeTime) return "unchanged";
|
|
621
|
-
this.setIndexEntry(file.path, { writeTime: file.createTime, size: file.size,
|
|
703
|
+
this.setIndexEntry(file.path, { writeTime: file.createTime, size: file.size, sourcesListIndex: this.sourcesListIndexOfSlot(sourceIndex) });
|
|
622
704
|
if (file.size === 0) return "tombstone";
|
|
623
705
|
if (existing) return "updated";
|
|
624
706
|
return "new";
|
|
625
707
|
}
|
|
626
708
|
|
|
627
709
|
private async pollChanges(sourceIndex: number): Promise<void> {
|
|
628
|
-
let { source } = this.sources[sourceIndex];
|
|
629
|
-
if (!source.getChangesAfter) return;
|
|
710
|
+
let { source, route } = this.sources[sourceIndex];
|
|
630
711
|
let state = this.sourceStates[sourceIndex];
|
|
631
712
|
let pollStart = Date.now();
|
|
632
|
-
let changes = await source.
|
|
713
|
+
let changes = await source.getChangesAfter2({ time: state.changesAfterTime, routes: route && [route] || undefined });
|
|
633
714
|
let tally = newScanTally();
|
|
634
715
|
for (let file of changes) {
|
|
635
|
-
tally[this.
|
|
716
|
+
tally[this.updateScanIndex(sourceIndex, file)]++;
|
|
636
717
|
}
|
|
637
718
|
// Polls run constantly, so only the ones that actually changed the index get a line
|
|
638
719
|
if (tally.new || tally.updated || tally.tombstone) {
|
|
@@ -649,8 +730,9 @@ export class BlobStore implements IBucketStore {
|
|
|
649
730
|
let state = this.sourceStates[sourceIndex];
|
|
650
731
|
let pending: { key: string; entry: IndexEntry }[] = [];
|
|
651
732
|
let totalBytes = 0;
|
|
733
|
+
let copiedSourcesListIndex = this.sourcesListIndexOfSlot(sourceIndex);
|
|
652
734
|
for (let [key, entry] of this.mem) {
|
|
653
|
-
if (entry.
|
|
735
|
+
if (entry.sourcesListIndex !== copiedSourcesListIndex) continue;
|
|
654
736
|
if (entry.size === 0) continue;
|
|
655
737
|
pending.push({ key, entry });
|
|
656
738
|
totalBytes += entry.size;
|
|
@@ -697,10 +779,10 @@ export class BlobStore implements IBucketStore {
|
|
|
697
779
|
let { key, entry } = pending[index];
|
|
698
780
|
let result = await source.get2(key);
|
|
699
781
|
if (result) {
|
|
700
|
-
await this.sources[0].source.set(key, result.data, { lastModified: result.writeTime });
|
|
782
|
+
await this.sources[0].source.set(key, result.data, { lastModified: result.writeTime, forceSetImmutable: true, noChecks: true });
|
|
701
783
|
// Only move the entry's source if it wasn't changed while we copied
|
|
702
784
|
if (this.mem.get(key) === entry) {
|
|
703
|
-
this.setIndexEntry(key, { writeTime: result.writeTime, size: result.data.length,
|
|
785
|
+
this.setIndexEntry(key, { writeTime: result.writeTime, size: result.data.length, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
704
786
|
}
|
|
705
787
|
}
|
|
706
788
|
activity.doneFiles = (activity.doneFiles || 0) + 1;
|
|
@@ -727,7 +809,7 @@ export class BlobStore implements IBucketStore {
|
|
|
727
809
|
}
|
|
728
810
|
}
|
|
729
811
|
|
|
730
|
-
// findInfo and
|
|
812
|
+
// findInfo and getChangesAfter2 list from the index, so they must wait for our own base source's initial scan (which might lag minutes) before the listing is trustworthy. The base (local disk) is implicitly required - remote sources are not, they come and go.
|
|
731
813
|
private async waitForRequiredScans(): Promise<void> {
|
|
732
814
|
await this.sourceStates[0].initialScan.promise;
|
|
733
815
|
}
|
|
@@ -741,7 +823,7 @@ export class BlobStore implements IBucketStore {
|
|
|
741
823
|
if (i === 0 && !state.scanComplete) {
|
|
742
824
|
let info = await source.getInfo(key);
|
|
743
825
|
if (info) {
|
|
744
|
-
this.
|
|
826
|
+
this.updateScanIndex(i, { path: key, createTime: info.writeTime, size: info.size });
|
|
745
827
|
}
|
|
746
828
|
continue;
|
|
747
829
|
}
|
|
@@ -753,12 +835,9 @@ export class BlobStore implements IBucketStore {
|
|
|
753
835
|
}
|
|
754
836
|
|
|
755
837
|
private async getIndexEntry(key: string): Promise<IndexEntry | undefined> {
|
|
838
|
+
// An entry whose holder is no longer in the source list is still valid - getEntryHolder resolves the persisted URL directly (and get2's fallback loop covers a holder that is gone entirely)
|
|
756
839
|
let entry = this.mem.get(key);
|
|
757
|
-
if (entry
|
|
758
|
-
if (entry) {
|
|
759
|
-
// The source list changed and this entry's source no longer exists; treat as missing
|
|
760
|
-
this.deleteIndexEntry(key);
|
|
761
|
-
}
|
|
840
|
+
if (entry) return entry;
|
|
762
841
|
await this.checkMissingKey(key);
|
|
763
842
|
return this.mem.get(key);
|
|
764
843
|
}
|
|
@@ -788,17 +867,19 @@ export class BlobStore implements IBucketStore {
|
|
|
788
867
|
if (!entry) return undefined;
|
|
789
868
|
if (entry.size === 0) return undefined;
|
|
790
869
|
entry.lastAccess = Date.now();
|
|
791
|
-
let
|
|
870
|
+
let holderArchives = await this.getEntryHolder(entry);
|
|
792
871
|
let result: { data: Buffer; writeTime: number; size: number } | undefined;
|
|
793
872
|
let holderError: Error | undefined;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
873
|
+
if (holderArchives) {
|
|
874
|
+
try {
|
|
875
|
+
result = await holderArchives.get2(key, { range });
|
|
876
|
+
} catch (e) {
|
|
877
|
+
holderError = e as Error;
|
|
878
|
+
}
|
|
798
879
|
}
|
|
799
880
|
if (result) {
|
|
800
881
|
// Ranged reads can't populate a cache (they're partial)
|
|
801
|
-
if (
|
|
882
|
+
if (this.slotForSourcesListIndex(entry.sourcesListIndex) !== 0 && !range) {
|
|
802
883
|
await this.cacheRead(key, result);
|
|
803
884
|
}
|
|
804
885
|
return result;
|
|
@@ -809,8 +890,9 @@ export class BlobStore implements IBucketStore {
|
|
|
809
890
|
return undefined;
|
|
810
891
|
}
|
|
811
892
|
// The holder is down or lost the file. ANY other source's copy beats no value - even an OLDER one - and it's copied onto our disk so the next read doesn't depend on luck.
|
|
893
|
+
let holderSlot = this.slotForSourcesListIndex(entry.sourcesListIndex);
|
|
812
894
|
for (let i = 0; i < this.sources.length; i++) {
|
|
813
|
-
if (i ===
|
|
895
|
+
if (i === holderSlot || !this.isLive(i)) continue;
|
|
814
896
|
let fallback: { data: Buffer; writeTime: number; size: number } | undefined;
|
|
815
897
|
try {
|
|
816
898
|
fallback = await this.sources[i].source.get2(key);
|
|
@@ -833,8 +915,8 @@ export class BlobStore implements IBucketStore {
|
|
|
833
915
|
|
|
834
916
|
// The read's bytes came from a remote source, so write them onto our own base source (the local disk), which becomes the entry's new holder - reads only pay the remote fetch once
|
|
835
917
|
private async cacheRead(key: string, result: { data: Buffer; writeTime: number }): Promise<void> {
|
|
836
|
-
await this.sources[0].source.set(key, result.data, { lastModified: result.writeTime });
|
|
837
|
-
this.setIndexEntry(key, { writeTime: result.writeTime, size: result.data.length,
|
|
918
|
+
await this.sources[0].source.set(key, result.data, { lastModified: result.writeTime, forceSetImmutable: true, noChecks: true });
|
|
919
|
+
this.setIndexEntry(key, { writeTime: result.writeTime, size: result.data.length, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
838
920
|
}
|
|
839
921
|
|
|
840
922
|
public async set(key: string, data: Buffer, config?: WriteConfig): Promise<string> {
|
|
@@ -901,15 +983,15 @@ export class BlobStore implements IBucketStore {
|
|
|
901
983
|
// A tombstone stores nothing on our own source - the index entry alone records it
|
|
902
984
|
await this.sources[first].source.del(key);
|
|
903
985
|
} else {
|
|
904
|
-
await this.sources[first].source.set(key, data, { lastModified: writeTime });
|
|
986
|
+
await this.sources[first].source.set(key, data, { lastModified: writeTime, noChecks: true });
|
|
905
987
|
}
|
|
906
|
-
this.setIndexEntry(key, { writeTime, size: data.length,
|
|
988
|
+
this.setIndexEntry(key, { writeTime, size: data.length, sourcesListIndex: this.sourcesListIndexOfSlot(first) });
|
|
907
989
|
if (isRouting) return;
|
|
908
990
|
let route = getRoute(key);
|
|
909
991
|
for (let i of writable) {
|
|
910
992
|
if (!routeContains(this.sources[i].route, route)) continue;
|
|
911
993
|
// Downstream sources receive tombstones as actual empty writes, so their listings show the deletion (size 0) and other stores scan it in as a tombstone
|
|
912
|
-
void this.sources[i].source.set(key, data, { lastModified: writeTime }).catch((e: Error) => {
|
|
994
|
+
void this.sources[i].source.set(key, data, { lastModified: writeTime, forceSetImmutable: true, noChecks: true }).catch((e: Error) => {
|
|
913
995
|
console.error(`Background write of ${key} to sync source ${this.sources[i].source.getDebugName()} failed: ${e.stack ?? e}`);
|
|
914
996
|
});
|
|
915
997
|
}
|
|
@@ -949,18 +1031,21 @@ export class BlobStore implements IBucketStore {
|
|
|
949
1031
|
return files;
|
|
950
1032
|
}
|
|
951
1033
|
|
|
952
|
-
// All files changed after
|
|
953
|
-
public async
|
|
1034
|
+
// All files changed after config.time — fast, straight from the in-memory index. Filters on when WE learned of the change (changedAt), so files synchronized late (with old write times) are still reported. Deletions ARE reported, as size-0 tombstone entries — that's how they propagate to stores syncing from us. config.routes lets a store syncing a partial shard ask for just its slice.
|
|
1035
|
+
public async getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
|
|
954
1036
|
await this.init();
|
|
955
1037
|
await this.waitForRequiredScans();
|
|
1038
|
+
let inRoutes = (key: string) => !config.routes || config.routes.some(route => routeContains(route, getRoute(key)));
|
|
956
1039
|
let files: ArchiveFileInfo[] = [];
|
|
957
1040
|
for (let [key, entry] of this.mem) {
|
|
958
|
-
if (entry.changedAt <= time) continue;
|
|
1041
|
+
if (entry.changedAt <= config.time) continue;
|
|
959
1042
|
if (this.overlay.has(key)) continue;
|
|
1043
|
+
if (!inRoutes(key)) continue;
|
|
960
1044
|
files.push({ path: key, createTime: entry.writeTime, size: entry.size });
|
|
961
1045
|
}
|
|
962
1046
|
for (let [key, overlayEntry] of this.overlay) {
|
|
963
|
-
if (overlayEntry.t <= time) continue;
|
|
1047
|
+
if (overlayEntry.t <= config.time) continue;
|
|
1048
|
+
if (!inRoutes(key)) continue;
|
|
964
1049
|
files.push({ path: key, createTime: overlayEntry.t, size: overlayEntry.data.length });
|
|
965
1050
|
}
|
|
966
1051
|
sort(files, x => x.path);
|
|
@@ -1007,7 +1092,7 @@ export class BlobStore implements IBucketStore {
|
|
|
1007
1092
|
this.overlay.delete(key);
|
|
1008
1093
|
let info = await disk.getInfo(key);
|
|
1009
1094
|
if (info) {
|
|
1010
|
-
this.setIndexEntry(key, { writeTime: info.writeTime, size: info.size,
|
|
1095
|
+
this.setIndexEntry(key, { writeTime: info.writeTime, size: info.size, sourcesListIndex: this.sourcesListIndexOfSlot(sourceIndex) });
|
|
1011
1096
|
}
|
|
1012
1097
|
}
|
|
1013
1098
|
public async cancelLargeUpload(id: string): Promise<void> {
|
|
@@ -1036,9 +1121,10 @@ export class BlobStore implements IBucketStore {
|
|
|
1036
1121
|
let evictedFiles = 0;
|
|
1037
1122
|
let evictedBytes = 0;
|
|
1038
1123
|
try {
|
|
1124
|
+
let baseSourcesListIndex = this.sourcesListIndexOfSlot(0);
|
|
1039
1125
|
let candidates: { key: string; entry: IndexEntry }[] = [];
|
|
1040
1126
|
for (let [key, entry] of this.mem) {
|
|
1041
|
-
if (entry.
|
|
1127
|
+
if (entry.sourcesListIndex !== baseSourcesListIndex || entry.size === 0 || key === ROUTING_FILE) continue;
|
|
1042
1128
|
candidates.push({ key, entry });
|
|
1043
1129
|
}
|
|
1044
1130
|
sort(candidates, x => x.entry.lastAccess);
|
|
@@ -1061,7 +1147,7 @@ export class BlobStore implements IBucketStore {
|
|
|
1061
1147
|
}
|
|
1062
1148
|
if (holder === undefined) continue;
|
|
1063
1149
|
await this.sources[0].source.del(key);
|
|
1064
|
-
this.setIndexEntry(key, { writeTime: entry.writeTime, size: entry.size,
|
|
1150
|
+
this.setIndexEntry(key, { writeTime: entry.writeTime, size: entry.size, sourcesListIndex: this.sourcesListIndexOfSlot(holder) });
|
|
1065
1151
|
evictedFiles++;
|
|
1066
1152
|
evictedBytes += entry.size;
|
|
1067
1153
|
}
|