sliftutils 1.7.48 → 1.7.50
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 +99 -38
- package/package.json +1 -1
- package/storage/ArchivesDisk.d.ts +5 -5
- package/storage/ArchivesDisk.ts +19 -5
- package/storage/IArchives.d.ts +36 -7
- package/storage/IArchives.ts +76 -8
- package/storage/backblaze.d.ts +4 -4
- package/storage/backblaze.ts +89 -36
- package/storage/dist/ArchivesDisk.ts.cache +8 -2
- package/storage/dist/IArchives.ts.cache +2 -2
- package/storage/dist/backblaze.ts.cache +86 -36
- package/storage/remoteStorage/ArchivesRemote.d.ts +4 -5
- package/storage/remoteStorage/ArchivesRemote.ts +7 -7
- package/storage/remoteStorage/ArchivesUrl.d.ts +2 -1
- package/storage/remoteStorage/ArchivesUrl.ts +13 -3
- package/storage/remoteStorage/blobStore.d.ts +15 -6
- package/storage/remoteStorage/blobStore.ts +198 -115
- package/storage/remoteStorage/createArchives.d.ts +4 -5
- package/storage/remoteStorage/createArchives.ts +6 -11
- package/storage/remoteStorage/dist/ArchivesRemote.ts.cache +5 -5
- package/storage/remoteStorage/dist/ArchivesUrl.ts.cache +15 -4
- package/storage/remoteStorage/dist/blobStore.ts.cache +194 -108
- package/storage/remoteStorage/dist/createArchives.ts.cache +4 -9
- package/storage/remoteStorage/dist/remoteConfig.ts.cache +15 -3
- package/storage/remoteStorage/dist/sourcesList.ts.cache +79 -0
- package/storage/remoteStorage/dist/storageController.ts.cache +7 -10
- package/storage/remoteStorage/dist/storageServerState.ts.cache +50 -18
- package/storage/remoteStorage/remoteConfig.d.ts +3 -1
- package/storage/remoteStorage/remoteConfig.ts +11 -1
- package/storage/remoteStorage/sourcesList.d.ts +15 -0
- package/storage/remoteStorage/sourcesList.ts +69 -0
- package/storage/remoteStorage/storageController.d.ts +4 -4
- package/storage/remoteStorage/storageController.ts +11 -14
- package/storage/remoteStorage/storageServerState.d.ts +3 -0
- package/storage/remoteStorage/storageServerState.ts +40 -15
|
@@ -4,14 +4,15 @@ 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,
|
|
8
|
-
windowAcceptsWrites, SyncActivity, FULL_ROUTE,
|
|
7
|
+
IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, ChangesAfterConfig, assertValidLastModified,
|
|
8
|
+
windowAcceptsWrites, SyncActivity, FULL_ROUTE, copyArchiveFile,
|
|
9
9
|
} from "../IArchives";
|
|
10
10
|
import { ArchivesDisk, applyFindInfoShape } from "../ArchivesDisk";
|
|
11
11
|
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 };
|
|
@@ -68,7 +73,7 @@ export type IBucketStore = {
|
|
|
68
73
|
computeIndexTotals?(): Promise<{ fileCount: number; byteCount: number; sources: { debugName: string; fileCount: number; byteCount: number }[] }>;
|
|
69
74
|
startLargeUpload(): Promise<string>;
|
|
70
75
|
appendLargeUpload(id: string, data: Buffer): Promise<void>;
|
|
71
|
-
finishLargeUpload(id: string, key: string): Promise<void>;
|
|
76
|
+
finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void>;
|
|
72
77
|
cancelLargeUpload(id: string): Promise<void>;
|
|
73
78
|
};
|
|
74
79
|
|
|
@@ -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,18 @@ 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
|
-
let
|
|
390
|
-
if (!
|
|
391
|
-
if (
|
|
392
|
-
this.setIndexEntry(file.path, { writeTime:
|
|
444
|
+
let copied = await copyArchiveFile({ from: source, to: this.sources[0].source, path: file.path, size: file.size, writeTime: file.createTime, forceSetImmutable: true, noChecks: true });
|
|
445
|
+
if (!copied) continue;
|
|
446
|
+
if (copied.size === 0) {
|
|
447
|
+
this.setIndexEntry(file.path, { writeTime: copied.writeTime, size: 0, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
393
448
|
tally.tombstone++;
|
|
394
449
|
continue;
|
|
395
450
|
}
|
|
396
|
-
|
|
397
|
-
this.setIndexEntry(file.path, { writeTime: result.writeTime, size: result.data.length, source: 0 });
|
|
451
|
+
this.setIndexEntry(file.path, { writeTime: copied.writeTime, size: copied.size, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
398
452
|
if (entry || overlayEntry) {
|
|
399
453
|
tally.updated++;
|
|
400
454
|
} else {
|
|
@@ -437,10 +491,10 @@ export class BlobStore implements IBucketStore {
|
|
|
437
491
|
if (entry.size === 0) continue;
|
|
438
492
|
fileCount++;
|
|
439
493
|
byteCount += entry.size;
|
|
440
|
-
let
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
|
|
494
|
+
let slot = this.slotForSourcesListIndex(entry.sourcesListIndex);
|
|
495
|
+
if (slot !== undefined) {
|
|
496
|
+
sources[slot].fileCount++;
|
|
497
|
+
sources[slot].byteCount += entry.size;
|
|
444
498
|
}
|
|
445
499
|
}
|
|
446
500
|
return { fileCount, byteCount, sources: sources.filter((x, i) => this.isLive(i)) };
|
|
@@ -454,7 +508,7 @@ export class BlobStore implements IBucketStore {
|
|
|
454
508
|
let deletes: string[] = [];
|
|
455
509
|
for (let [key, entry] of dirty) {
|
|
456
510
|
if (entry) {
|
|
457
|
-
writes.push({ key, writeTime: entry.writeTime, size: entry.size,
|
|
511
|
+
writes.push({ key, writeTime: entry.writeTime, size: entry.size, sourcesListIndex: entry.sourcesListIndex });
|
|
458
512
|
} else {
|
|
459
513
|
deletes.push(key);
|
|
460
514
|
}
|
|
@@ -466,6 +520,7 @@ export class BlobStore implements IBucketStore {
|
|
|
466
520
|
// ── synchronization ──
|
|
467
521
|
|
|
468
522
|
private async runSourceSync(sourceIndex: number): Promise<void> {
|
|
523
|
+
await this.registerSlot(sourceIndex);
|
|
469
524
|
let { source } = this.sources[sourceIndex];
|
|
470
525
|
let state = this.sourceStates[sourceIndex];
|
|
471
526
|
// Read live for every pass, not captured - updateSources can change it while loops run
|
|
@@ -474,7 +529,7 @@ export class BlobStore implements IBucketStore {
|
|
|
474
529
|
while (!this.stopped.stop && !state.stopped.stop) {
|
|
475
530
|
try {
|
|
476
531
|
let config = await source.getConfig();
|
|
477
|
-
state.supportsChangesAfter = !!
|
|
532
|
+
state.supportsChangesAfter = !!config.supportsChangesAfter;
|
|
478
533
|
listing = await this.scanSource(sourceIndex);
|
|
479
534
|
break;
|
|
480
535
|
} catch (e) {
|
|
@@ -506,7 +561,7 @@ export class BlobStore implements IBucketStore {
|
|
|
506
561
|
await this.reconcileSource(sourceIndex, new Map(files.map(x => [x.path, x.createTime])));
|
|
507
562
|
}, state.stopped);
|
|
508
563
|
} else {
|
|
509
|
-
runInfinitePoll(
|
|
564
|
+
runInfinitePoll(FULL_RESCAN_UNINDEXED_INTERVAL, async () => {
|
|
510
565
|
let rescan = await this.scanSource(sourceIndex);
|
|
511
566
|
await this.reconcileSource(sourceIndex, rescan);
|
|
512
567
|
if (!noFullSync()) await this.copySourceFiles(sourceIndex);
|
|
@@ -526,6 +581,7 @@ export class BlobStore implements IBucketStore {
|
|
|
526
581
|
console.log(`Metadata scan of ${source.getDebugName()} still running (${Math.round((Date.now() - scanStart) / 1000)}s, store ${this.folder})`);
|
|
527
582
|
}, SYNC_PROGRESS_LOG_INTERVAL);
|
|
528
583
|
(progressTimer as { unref?: () => void }).unref?.();
|
|
584
|
+
// 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
585
|
let files: ArchiveFileInfo[];
|
|
530
586
|
try {
|
|
531
587
|
files = await source.findInfo("");
|
|
@@ -544,15 +600,16 @@ export class BlobStore implements IBucketStore {
|
|
|
544
600
|
if (!this.mem.has(file.path)) {
|
|
545
601
|
newPaths++;
|
|
546
602
|
}
|
|
547
|
-
tally[this.
|
|
603
|
+
tally[this.updateScanIndex(sourceIndex, file)]++;
|
|
548
604
|
}
|
|
549
605
|
state.scannedCount = files.length;
|
|
550
606
|
// 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
607
|
let removedFromIndex = 0;
|
|
552
608
|
let missingOnSource = 0;
|
|
609
|
+
let scannedSourcesListIndex = this.sourcesListIndexOfSlot(sourceIndex);
|
|
553
610
|
for (let [key, entry] of this.mem) {
|
|
554
611
|
if (seen.has(key)) continue;
|
|
555
|
-
if (entry.
|
|
612
|
+
if (entry.sourcesListIndex === scannedSourcesListIndex && entry.size !== 0 && entry.changedAt < scanStart) {
|
|
556
613
|
this.deleteIndexEntry(key);
|
|
557
614
|
removedFromIndex++;
|
|
558
615
|
continue;
|
|
@@ -571,38 +628,61 @@ export class BlobStore implements IBucketStore {
|
|
|
571
628
|
return seen;
|
|
572
629
|
}
|
|
573
630
|
|
|
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.
|
|
631
|
+
// 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
632
|
private async reconcileSource(sourceIndex: number, listing: Map<string, number>): Promise<void> {
|
|
576
633
|
let { source, validWindow, route } = this.sources[sourceIndex];
|
|
577
634
|
let state = this.sourceStates[sourceIndex];
|
|
578
635
|
let acceptsWrites = windowAcceptsWrites(validWindow);
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
636
|
+
let targetSourcesListIndex = this.sourcesListIndexOfSlot(sourceIndex);
|
|
637
|
+
let pushed = 0;
|
|
638
|
+
let failed = 0;
|
|
639
|
+
let consecutiveFailures = 0;
|
|
640
|
+
let errors: string[] = [];
|
|
641
|
+
let aborted = false;
|
|
642
|
+
for (let [key, entry] of this.mem) {
|
|
643
|
+
if (this.stopped.stop || state.stopped.stop) return;
|
|
644
|
+
if (entry.sourcesListIndex === targetSourcesListIndex) continue;
|
|
645
|
+
// 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
|
|
646
|
+
if (key === ROUTING_FILE) continue;
|
|
647
|
+
if (!acceptsWrites) continue;
|
|
648
|
+
if (!routeContains(route, getRoute(key))) continue;
|
|
649
|
+
let theirTime = listing.get(key);
|
|
650
|
+
if (theirTime !== undefined && theirTime >= entry.writeTime) continue;
|
|
651
|
+
try {
|
|
589
652
|
if (entry.size === 0) {
|
|
590
653
|
// A deletion only needs pushing while the source still holds an older copy
|
|
591
654
|
if (theirTime === undefined) continue;
|
|
592
|
-
await source.set(key, Buffer.alloc(0), { lastModified: entry.writeTime });
|
|
655
|
+
await source.set(key, Buffer.alloc(0), { lastModified: entry.writeTime, forceSetImmutable: true, noChecks: true });
|
|
656
|
+
pushed++;
|
|
657
|
+
consecutiveFailures = 0;
|
|
593
658
|
continue;
|
|
594
659
|
}
|
|
595
|
-
let
|
|
596
|
-
if (!
|
|
597
|
-
await source
|
|
660
|
+
let holder = await this.getEntryHolder(entry);
|
|
661
|
+
if (!holder) continue;
|
|
662
|
+
let copied = await copyArchiveFile({ from: holder, to: source, path: key, size: entry.size, writeTime: entry.writeTime, forceSetImmutable: true, noChecks: true });
|
|
663
|
+
if (!copied) continue;
|
|
664
|
+
pushed++;
|
|
665
|
+
consecutiveFailures = 0;
|
|
666
|
+
} catch (e) {
|
|
667
|
+
failed++;
|
|
668
|
+
consecutiveFailures++;
|
|
669
|
+
if (errors.length < RECONCILE_ERROR_LOG_LIMIT) {
|
|
670
|
+
errors.push(`${key}: ${(e as Error).stack ?? e}`);
|
|
671
|
+
}
|
|
672
|
+
if (consecutiveFailures >= RECONCILE_MAX_CONSECUTIVE_FAILURES) {
|
|
673
|
+
aborted = true;
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
598
676
|
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
console.error(`Reconciling sync source ${source.getDebugName()}
|
|
677
|
+
}
|
|
678
|
+
if (failed) {
|
|
679
|
+
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(" | ")}`);
|
|
680
|
+
} else if (pushed) {
|
|
681
|
+
console.log(`Reconciled sync source ${source.getDebugName()} (store ${this.folder}): pushed ${pushed} files it was missing or held older copies of`);
|
|
602
682
|
}
|
|
603
683
|
}
|
|
604
684
|
|
|
605
|
-
private
|
|
685
|
+
private updateScanIndex(sourceIndex: number, file: ArchiveFileInfo): ScanOutcome {
|
|
606
686
|
// An in-flight scan can outlive its source's removal; its results are dead
|
|
607
687
|
if (!this.isLive(sourceIndex)) return "filtered";
|
|
608
688
|
if (file.path === ROUTING_FILE) {
|
|
@@ -618,21 +698,20 @@ export class BlobStore implements IBucketStore {
|
|
|
618
698
|
let existing = this.mem.get(file.path);
|
|
619
699
|
// The highest write time wins across all sources (ties keep the existing entry)
|
|
620
700
|
if (existing && file.createTime <= existing.writeTime) return "unchanged";
|
|
621
|
-
this.setIndexEntry(file.path, { writeTime: file.createTime, size: file.size,
|
|
701
|
+
this.setIndexEntry(file.path, { writeTime: file.createTime, size: file.size, sourcesListIndex: this.sourcesListIndexOfSlot(sourceIndex) });
|
|
622
702
|
if (file.size === 0) return "tombstone";
|
|
623
703
|
if (existing) return "updated";
|
|
624
704
|
return "new";
|
|
625
705
|
}
|
|
626
706
|
|
|
627
707
|
private async pollChanges(sourceIndex: number): Promise<void> {
|
|
628
|
-
let { source } = this.sources[sourceIndex];
|
|
629
|
-
if (!source.getChangesAfter) return;
|
|
708
|
+
let { source, route } = this.sources[sourceIndex];
|
|
630
709
|
let state = this.sourceStates[sourceIndex];
|
|
631
710
|
let pollStart = Date.now();
|
|
632
|
-
let changes = await source.
|
|
711
|
+
let changes = await source.getChangesAfter2({ time: state.changesAfterTime, routes: route && [route] || undefined });
|
|
633
712
|
let tally = newScanTally();
|
|
634
713
|
for (let file of changes) {
|
|
635
|
-
tally[this.
|
|
714
|
+
tally[this.updateScanIndex(sourceIndex, file)]++;
|
|
636
715
|
}
|
|
637
716
|
// Polls run constantly, so only the ones that actually changed the index get a line
|
|
638
717
|
if (tally.new || tally.updated || tally.tombstone) {
|
|
@@ -649,8 +728,9 @@ export class BlobStore implements IBucketStore {
|
|
|
649
728
|
let state = this.sourceStates[sourceIndex];
|
|
650
729
|
let pending: { key: string; entry: IndexEntry }[] = [];
|
|
651
730
|
let totalBytes = 0;
|
|
731
|
+
let copiedSourcesListIndex = this.sourcesListIndexOfSlot(sourceIndex);
|
|
652
732
|
for (let [key, entry] of this.mem) {
|
|
653
|
-
if (entry.
|
|
733
|
+
if (entry.sourcesListIndex !== copiedSourcesListIndex) continue;
|
|
654
734
|
if (entry.size === 0) continue;
|
|
655
735
|
pending.push({ key, entry });
|
|
656
736
|
totalBytes += entry.size;
|
|
@@ -695,12 +775,11 @@ export class BlobStore implements IBucketStore {
|
|
|
695
775
|
let index = nextIndex++;
|
|
696
776
|
if (index >= pending.length) return;
|
|
697
777
|
let { key, entry } = pending[index];
|
|
698
|
-
let
|
|
699
|
-
if (
|
|
700
|
-
await this.sources[0].source.set(key, result.data, { lastModified: result.writeTime });
|
|
778
|
+
let copied = await copyArchiveFile({ from: source, to: this.sources[0].source, path: key, size: entry.size, writeTime: entry.writeTime, forceSetImmutable: true, noChecks: true });
|
|
779
|
+
if (copied) {
|
|
701
780
|
// Only move the entry's source if it wasn't changed while we copied
|
|
702
781
|
if (this.mem.get(key) === entry) {
|
|
703
|
-
this.setIndexEntry(key, { writeTime:
|
|
782
|
+
this.setIndexEntry(key, { writeTime: copied.writeTime, size: copied.size, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
704
783
|
}
|
|
705
784
|
}
|
|
706
785
|
activity.doneFiles = (activity.doneFiles || 0) + 1;
|
|
@@ -727,7 +806,7 @@ export class BlobStore implements IBucketStore {
|
|
|
727
806
|
}
|
|
728
807
|
}
|
|
729
808
|
|
|
730
|
-
// findInfo and
|
|
809
|
+
// 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
810
|
private async waitForRequiredScans(): Promise<void> {
|
|
732
811
|
await this.sourceStates[0].initialScan.promise;
|
|
733
812
|
}
|
|
@@ -741,7 +820,7 @@ export class BlobStore implements IBucketStore {
|
|
|
741
820
|
if (i === 0 && !state.scanComplete) {
|
|
742
821
|
let info = await source.getInfo(key);
|
|
743
822
|
if (info) {
|
|
744
|
-
this.
|
|
823
|
+
this.updateScanIndex(i, { path: key, createTime: info.writeTime, size: info.size });
|
|
745
824
|
}
|
|
746
825
|
continue;
|
|
747
826
|
}
|
|
@@ -753,12 +832,9 @@ export class BlobStore implements IBucketStore {
|
|
|
753
832
|
}
|
|
754
833
|
|
|
755
834
|
private async getIndexEntry(key: string): Promise<IndexEntry | undefined> {
|
|
835
|
+
// 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
836
|
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
|
-
}
|
|
837
|
+
if (entry) return entry;
|
|
762
838
|
await this.checkMissingKey(key);
|
|
763
839
|
return this.mem.get(key);
|
|
764
840
|
}
|
|
@@ -788,17 +864,19 @@ export class BlobStore implements IBucketStore {
|
|
|
788
864
|
if (!entry) return undefined;
|
|
789
865
|
if (entry.size === 0) return undefined;
|
|
790
866
|
entry.lastAccess = Date.now();
|
|
791
|
-
let
|
|
867
|
+
let holderArchives = await this.getEntryHolder(entry);
|
|
792
868
|
let result: { data: Buffer; writeTime: number; size: number } | undefined;
|
|
793
869
|
let holderError: Error | undefined;
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
870
|
+
if (holderArchives) {
|
|
871
|
+
try {
|
|
872
|
+
result = await holderArchives.get2(key, { range });
|
|
873
|
+
} catch (e) {
|
|
874
|
+
holderError = e as Error;
|
|
875
|
+
}
|
|
798
876
|
}
|
|
799
877
|
if (result) {
|
|
800
878
|
// Ranged reads can't populate a cache (they're partial)
|
|
801
|
-
if (
|
|
879
|
+
if (this.slotForSourcesListIndex(entry.sourcesListIndex) !== 0 && !range) {
|
|
802
880
|
await this.cacheRead(key, result);
|
|
803
881
|
}
|
|
804
882
|
return result;
|
|
@@ -809,8 +887,9 @@ export class BlobStore implements IBucketStore {
|
|
|
809
887
|
return undefined;
|
|
810
888
|
}
|
|
811
889
|
// 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.
|
|
890
|
+
let holderSlot = this.slotForSourcesListIndex(entry.sourcesListIndex);
|
|
812
891
|
for (let i = 0; i < this.sources.length; i++) {
|
|
813
|
-
if (i ===
|
|
892
|
+
if (i === holderSlot || !this.isLive(i)) continue;
|
|
814
893
|
let fallback: { data: Buffer; writeTime: number; size: number } | undefined;
|
|
815
894
|
try {
|
|
816
895
|
fallback = await this.sources[i].source.get2(key);
|
|
@@ -833,8 +912,8 @@ export class BlobStore implements IBucketStore {
|
|
|
833
912
|
|
|
834
913
|
// 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
914
|
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,
|
|
915
|
+
await this.sources[0].source.set(key, result.data, { lastModified: result.writeTime, forceSetImmutable: true, noChecks: true });
|
|
916
|
+
this.setIndexEntry(key, { writeTime: result.writeTime, size: result.data.length, sourcesListIndex: this.sourcesListIndexOfSlot(0) });
|
|
838
917
|
}
|
|
839
918
|
|
|
840
919
|
public async set(key: string, data: Buffer, config?: WriteConfig): Promise<string> {
|
|
@@ -901,15 +980,15 @@ export class BlobStore implements IBucketStore {
|
|
|
901
980
|
// A tombstone stores nothing on our own source - the index entry alone records it
|
|
902
981
|
await this.sources[first].source.del(key);
|
|
903
982
|
} else {
|
|
904
|
-
await this.sources[first].source.set(key, data, { lastModified: writeTime });
|
|
983
|
+
await this.sources[first].source.set(key, data, { lastModified: writeTime, noChecks: true });
|
|
905
984
|
}
|
|
906
|
-
this.setIndexEntry(key, { writeTime, size: data.length,
|
|
985
|
+
this.setIndexEntry(key, { writeTime, size: data.length, sourcesListIndex: this.sourcesListIndexOfSlot(first) });
|
|
907
986
|
if (isRouting) return;
|
|
908
987
|
let route = getRoute(key);
|
|
909
988
|
for (let i of writable) {
|
|
910
989
|
if (!routeContains(this.sources[i].route, route)) continue;
|
|
911
990
|
// 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) => {
|
|
991
|
+
void this.sources[i].source.set(key, data, { lastModified: writeTime, forceSetImmutable: true, noChecks: true }).catch((e: Error) => {
|
|
913
992
|
console.error(`Background write of ${key} to sync source ${this.sources[i].source.getDebugName()} failed: ${e.stack ?? e}`);
|
|
914
993
|
});
|
|
915
994
|
}
|
|
@@ -949,18 +1028,21 @@ export class BlobStore implements IBucketStore {
|
|
|
949
1028
|
return files;
|
|
950
1029
|
}
|
|
951
1030
|
|
|
952
|
-
// All files changed after
|
|
953
|
-
public async
|
|
1031
|
+
// 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.
|
|
1032
|
+
public async getChangesAfter2(config: ChangesAfterConfig): Promise<ArchiveFileInfo[]> {
|
|
954
1033
|
await this.init();
|
|
955
1034
|
await this.waitForRequiredScans();
|
|
1035
|
+
let inRoutes = (key: string) => !config.routes || config.routes.some(route => routeContains(route, getRoute(key)));
|
|
956
1036
|
let files: ArchiveFileInfo[] = [];
|
|
957
1037
|
for (let [key, entry] of this.mem) {
|
|
958
|
-
if (entry.changedAt <= time) continue;
|
|
1038
|
+
if (entry.changedAt <= config.time) continue;
|
|
959
1039
|
if (this.overlay.has(key)) continue;
|
|
1040
|
+
if (!inRoutes(key)) continue;
|
|
960
1041
|
files.push({ path: key, createTime: entry.writeTime, size: entry.size });
|
|
961
1042
|
}
|
|
962
1043
|
for (let [key, overlayEntry] of this.overlay) {
|
|
963
|
-
if (overlayEntry.t <= time) continue;
|
|
1044
|
+
if (overlayEntry.t <= config.time) continue;
|
|
1045
|
+
if (!inRoutes(key)) continue;
|
|
964
1046
|
files.push({ path: key, createTime: overlayEntry.t, size: overlayEntry.data.length });
|
|
965
1047
|
}
|
|
966
1048
|
sort(files, x => x.path);
|
|
@@ -1001,13 +1083,13 @@ export class BlobStore implements IBucketStore {
|
|
|
1001
1083
|
public async appendLargeUpload(id: string, data: Buffer): Promise<void> {
|
|
1002
1084
|
await this.getDiskSource().disk.appendLargeUpload(id, data);
|
|
1003
1085
|
}
|
|
1004
|
-
public async finishLargeUpload(id: string, key: string): Promise<void> {
|
|
1086
|
+
public async finishLargeUpload(id: string, key: string, lastModified?: number): Promise<void> {
|
|
1005
1087
|
let { disk, sourceIndex } = this.getDiskSource();
|
|
1006
|
-
await disk.finishLargeUpload(id, key);
|
|
1088
|
+
await disk.finishLargeUpload(id, key, lastModified);
|
|
1007
1089
|
this.overlay.delete(key);
|
|
1008
1090
|
let info = await disk.getInfo(key);
|
|
1009
1091
|
if (info) {
|
|
1010
|
-
this.setIndexEntry(key, { writeTime: info.writeTime, size: info.size,
|
|
1092
|
+
this.setIndexEntry(key, { writeTime: info.writeTime, size: info.size, sourcesListIndex: this.sourcesListIndexOfSlot(sourceIndex) });
|
|
1011
1093
|
}
|
|
1012
1094
|
}
|
|
1013
1095
|
public async cancelLargeUpload(id: string): Promise<void> {
|
|
@@ -1036,9 +1118,10 @@ export class BlobStore implements IBucketStore {
|
|
|
1036
1118
|
let evictedFiles = 0;
|
|
1037
1119
|
let evictedBytes = 0;
|
|
1038
1120
|
try {
|
|
1121
|
+
let baseSourcesListIndex = this.sourcesListIndexOfSlot(0);
|
|
1039
1122
|
let candidates: { key: string; entry: IndexEntry }[] = [];
|
|
1040
1123
|
for (let [key, entry] of this.mem) {
|
|
1041
|
-
if (entry.
|
|
1124
|
+
if (entry.sourcesListIndex !== baseSourcesListIndex || entry.size === 0 || key === ROUTING_FILE) continue;
|
|
1042
1125
|
candidates.push({ key, entry });
|
|
1043
1126
|
}
|
|
1044
1127
|
sort(candidates, x => x.entry.lastAccess);
|
|
@@ -1061,7 +1144,7 @@ export class BlobStore implements IBucketStore {
|
|
|
1061
1144
|
}
|
|
1062
1145
|
if (holder === undefined) continue;
|
|
1063
1146
|
await this.sources[0].source.del(key);
|
|
1064
|
-
this.setIndexEntry(key, { writeTime: entry.writeTime, size: entry.size,
|
|
1147
|
+
this.setIndexEntry(key, { writeTime: entry.writeTime, size: entry.size, sourcesListIndex: this.sourcesListIndexOfSlot(holder) });
|
|
1065
1148
|
evictedFiles++;
|
|
1066
1149
|
evictedBytes += entry.size;
|
|
1067
1150
|
}
|