sliftutils 1.7.55 → 1.7.57
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 +6 -3
- package/package.json +1 -1
- package/storage/IArchives.d.ts +1 -0
- package/storage/IArchives.ts +4 -1
- package/storage/backblaze.ts +11 -7
- package/storage/dist/IArchives.ts.cache +4 -3
- package/storage/dist/backblaze.ts.cache +15 -8
- package/storage/remoteStorage/blobStore.d.ts +2 -0
- package/storage/remoteStorage/blobStore.ts +28 -9
- package/storage/remoteStorage/createArchives.d.ts +1 -1
- package/storage/remoteStorage/createArchives.ts +9 -4
- package/storage/remoteStorage/dist/blobStore.ts.cache +33 -14
- package/storage/remoteStorage/dist/createArchives.ts.cache +14 -7
- package/storage/remoteStorage/dist/sourceWrapper.ts.cache +27 -13
- package/storage/remoteStorage/dist/storageController.ts.cache +2 -2
- package/storage/remoteStorage/dist/storageServerState.ts.cache +5 -3
- package/storage/remoteStorage/sourceWrapper.d.ts +2 -2
- package/storage/remoteStorage/sourceWrapper.ts +22 -10
- package/storage/remoteStorage/storageServerState.ts +2 -0
|
@@ -126,6 +126,8 @@ export type BlobSourceSpec = {
|
|
|
126
126
|
validWindow: [number, number];
|
|
127
127
|
route?: [number, number];
|
|
128
128
|
noFullSync?: boolean;
|
|
129
|
+
// See ArchivesSource.intermediate
|
|
130
|
+
intermediate?: boolean;
|
|
129
131
|
// Only called for sources that don't match an existing live slot
|
|
130
132
|
create: () => IArchives;
|
|
131
133
|
};
|
|
@@ -138,7 +140,7 @@ function newScanTally(): ScanTally {
|
|
|
138
140
|
}
|
|
139
141
|
function formatScanTally(tally: ScanTally, total: number): string {
|
|
140
142
|
let pct = (n: number) => `${Math.round(n / Math.max(total, 1) * 1000) / 10}%`;
|
|
141
|
-
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
|
|
143
|
+
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)})`;
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
function newSourceState(): SourceState {
|
|
@@ -359,7 +361,7 @@ export class BlobStore implements IBucketStore {
|
|
|
359
361
|
continue;
|
|
360
362
|
}
|
|
361
363
|
let source = spec.create();
|
|
362
|
-
this.sources.push({ source, url: spec.url, validWindow: spec.validWindow, route: spec.route, noFullSync: spec.noFullSync, identity: spec.identity });
|
|
364
|
+
this.sources.push({ source, url: spec.url, validWindow: spec.validWindow, route: spec.route, noFullSync: spec.noFullSync, intermediate: spec.intermediate, identity: spec.identity });
|
|
363
365
|
this.sourceStates.push(newSourceState());
|
|
364
366
|
this.sourceFileCounts.push(0);
|
|
365
367
|
this.sourceByteCounts.push(0);
|
|
@@ -525,14 +527,23 @@ export class BlobStore implements IBucketStore {
|
|
|
525
527
|
let state = this.sourceStates[sourceIndex];
|
|
526
528
|
// Read live for every pass, not captured - updateSources can change it while loops run
|
|
527
529
|
let noFullSync = () => this.sources[sourceIndex].noFullSync;
|
|
530
|
+
let intermediate = this.sources[sourceIndex].intermediate;
|
|
528
531
|
let listing: Map<string, number> | undefined;
|
|
529
532
|
while (!this.stopped.stop && !state.stopped.stop) {
|
|
533
|
+
if (this.isDeadIntermediate(sourceIndex)) {
|
|
534
|
+
console.log(`Not scanning sync source ${source.getDebugName()} (store ${this.folder}): it is an intermediate whose window already ended`);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
530
537
|
try {
|
|
531
538
|
let config = await source.getConfig();
|
|
532
539
|
state.supportsChangesAfter = !!config.supportsChangesAfter;
|
|
533
540
|
listing = await this.scanSource(sourceIndex);
|
|
534
541
|
break;
|
|
535
542
|
} catch (e) {
|
|
543
|
+
if (intermediate) {
|
|
544
|
+
console.error(`Initial scan of intermediate sync source ${source.getDebugName()} (store ${this.folder}) failed; not retrying (intermediates are temporary switchover ports): ${(e as Error).stack ?? e}`);
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
536
547
|
console.error(`Initial scan of sync source ${source.getDebugName()} failed, retrying:`, e);
|
|
537
548
|
await delay(SCAN_RETRY_DELAY);
|
|
538
549
|
}
|
|
@@ -540,6 +551,8 @@ export class BlobStore implements IBucketStore {
|
|
|
540
551
|
state.scanComplete = true;
|
|
541
552
|
state.initialScan.resolve(undefined);
|
|
542
553
|
if (this.stopped.stop || state.stopped.stop) return;
|
|
554
|
+
// An intermediate that never produced a listing is dead or dying - polling and copying from it would only log errors
|
|
555
|
+
if (intermediate && !listing) return;
|
|
543
556
|
if (listing) {
|
|
544
557
|
await this.reconcileSource(sourceIndex, listing);
|
|
545
558
|
}
|
|
@@ -552,16 +565,19 @@ export class BlobStore implements IBucketStore {
|
|
|
552
565
|
}
|
|
553
566
|
if (state.supportsChangesAfter) {
|
|
554
567
|
runInfinitePoll(CHANGES_POLL_INTERVAL, async () => {
|
|
568
|
+
if (this.isDeadIntermediate(sourceIndex)) return;
|
|
555
569
|
await this.pollChanges(sourceIndex);
|
|
556
570
|
if (!noFullSync()) await this.copySourceFiles(sourceIndex);
|
|
557
571
|
}, state.stopped);
|
|
558
572
|
// 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)
|
|
559
573
|
runInfinitePoll(FULL_RESCAN_INTERVAL, async () => {
|
|
574
|
+
if (this.isDeadIntermediate(sourceIndex)) return;
|
|
560
575
|
let files = await source.findInfo("");
|
|
561
576
|
await this.reconcileSource(sourceIndex, new Map(files.map(x => [x.path, x.createTime])));
|
|
562
577
|
}, state.stopped);
|
|
563
578
|
} else {
|
|
564
579
|
runInfinitePoll(FULL_RESCAN_UNINDEXED_INTERVAL, async () => {
|
|
580
|
+
if (this.isDeadIntermediate(sourceIndex)) return;
|
|
565
581
|
let rescan = await this.scanSource(sourceIndex);
|
|
566
582
|
await this.reconcileSource(sourceIndex, rescan);
|
|
567
583
|
if (!noFullSync()) await this.copySourceFiles(sourceIndex);
|
|
@@ -569,9 +585,15 @@ export class BlobStore implements IBucketStore {
|
|
|
569
585
|
}
|
|
570
586
|
}
|
|
571
587
|
|
|
588
|
+
// An intermediate is a deploy switchover's temporary alternate port: once its window is past, the port is gone for good, so scanning it (or retrying a failed scan) can never succeed - it would just log errors forever
|
|
589
|
+
private isDeadIntermediate(sourceIndex: number): boolean {
|
|
590
|
+
let { intermediate, validWindow } = this.sources[sourceIndex];
|
|
591
|
+
return !!intermediate && validWindow[1] <= Date.now();
|
|
592
|
+
}
|
|
593
|
+
|
|
572
594
|
// Full metadata scan (size, writeTime, path) of one source, applied to the index. Returns the source's listing (path -> write time), which reconcileSource uses for the push direction.
|
|
573
595
|
private async scanSource(sourceIndex: number): Promise<Map<string, number>> {
|
|
574
|
-
let { source,
|
|
596
|
+
let { source, route } = this.sources[sourceIndex];
|
|
575
597
|
let state = this.sourceStates[sourceIndex];
|
|
576
598
|
let scanStart = Date.now();
|
|
577
599
|
let activity: SyncActivity = { type: "metadataScan", sourceDebugName: source.getDebugName(), startTime: scanStart };
|
|
@@ -614,9 +636,8 @@ export class BlobStore implements IBucketStore {
|
|
|
614
636
|
removedFromIndex++;
|
|
615
637
|
continue;
|
|
616
638
|
}
|
|
617
|
-
// Counted only when the source SHOULD hold the entry (its
|
|
639
|
+
// Counted only when the source SHOULD hold the entry (its route matches) - these are what the reconcile pass pushes to it (which also ignores the valid window: synchronization moves existing values, the window only routes fresh writes)
|
|
618
640
|
if (entry.size === 0 || key === ROUTING_FILE) continue;
|
|
619
|
-
if (entry.writeTime < validWindow[0] || entry.writeTime > validWindow[1]) continue;
|
|
620
641
|
if (!routeContains(route, getRoute(key))) continue;
|
|
621
642
|
missingOnSource++;
|
|
622
643
|
}
|
|
@@ -689,10 +710,8 @@ export class BlobStore implements IBucketStore {
|
|
|
689
710
|
// The routing config is NEVER pulled from other sources - it only ever arrives as an explicit, version-validated write, and is only ever read off our own disk. Route and valid-window filters can't possibly apply to it either: it is the file DEFINING them, so filtering it would mean certain sources could never have their routing config updated, ever.
|
|
690
711
|
if (sourceIndex !== 0) return "filtered";
|
|
691
712
|
} else {
|
|
692
|
-
|
|
693
|
-
let
|
|
694
|
-
if (file.createTime < windowStart || file.createTime > windowEnd) return "filtered";
|
|
695
|
-
// A partially-overlapping shard's listing includes keys outside our route; ignore them
|
|
713
|
+
// The valid window is deliberately NOT applied here: it decides where WRITES route, but a scan is us asking a source what it already holds - existing values synchronize regardless of the window (the same reasoning that lets synchronization ignore the immutable flag). Only the route filters: a partially-overlapping shard's listing legitimately includes keys that aren't ours.
|
|
714
|
+
let { route } = this.sources[sourceIndex];
|
|
696
715
|
if (!routeContains(route, getRoute(file.path))) return "filtered";
|
|
697
716
|
}
|
|
698
717
|
let existing = this.mem.get(file.path);
|
|
@@ -86,7 +86,7 @@ export declare class ArchivesChain implements IArchives {
|
|
|
86
86
|
getNextData(): Promise<Buffer | undefined>;
|
|
87
87
|
}): Promise<void>;
|
|
88
88
|
getURL(path: string): Promise<string>;
|
|
89
|
-
/** Every URL that could serve this path
|
|
89
|
+
/** Every URL that could serve this path: public sources matching both the path's route and the current valid window. The first is the write node's (first matching source in config order, see runWrite - the one guaranteed current); the rest are ranked fastest-first by measured latency. Empty when none qualify. */
|
|
90
90
|
getURLs(path: string): Promise<string[]>;
|
|
91
91
|
dispose(): void;
|
|
92
92
|
}
|
|
@@ -541,6 +541,8 @@ export class ArchivesChain implements IArchives {
|
|
|
541
541
|
public async waitingForAccess(): Promise<{ link: string; machineId: string; ip: string } | undefined> {
|
|
542
542
|
let state = await this.getState();
|
|
543
543
|
for (let source of state.sources) {
|
|
544
|
+
// A source whose window has passed is never read or written again, so its access state is irrelevant - and asking a dead intermediate would just hang or throw. Future windows DO matter: access should be granted before their window starts.
|
|
545
|
+
if (source.config.validWindow[1] <= Date.now()) continue;
|
|
544
546
|
if (source.api instanceof ArchivesRemote) {
|
|
545
547
|
let waiting = await source.api.waitingForAccess();
|
|
546
548
|
if (waiting) return waiting;
|
|
@@ -800,19 +802,22 @@ export class ArchivesChain implements IArchives {
|
|
|
800
802
|
return urls[0];
|
|
801
803
|
}
|
|
802
804
|
|
|
803
|
-
/** Every URL that could serve this path
|
|
805
|
+
/** Every URL that could serve this path: public sources matching both the path's route and the current valid window. The first is the write node's (first matching source in config order, see runWrite - the one guaranteed current); the rest are ranked fastest-first by measured latency. Empty when none qualify. */
|
|
804
806
|
public async getURLs(path: string): Promise<string[]> {
|
|
805
807
|
let state = await this.getState();
|
|
806
808
|
let route = getRoute(path);
|
|
807
|
-
let
|
|
809
|
+
let candidates: { source: SourceWrapper; provider: IArchives }[] = [];
|
|
808
810
|
for (let source of state.sources) {
|
|
809
811
|
if (!(source.config.public ?? true)) continue;
|
|
810
812
|
if (!routeContains(source.config.route, route)) continue;
|
|
813
|
+
if (!configWindowCurrent(source.config)) continue;
|
|
811
814
|
let provider = source.url || source.api;
|
|
812
815
|
if (!provider) continue;
|
|
813
|
-
|
|
816
|
+
candidates.push({ source, provider });
|
|
814
817
|
}
|
|
815
|
-
let
|
|
818
|
+
let rest = candidates.slice(1);
|
|
819
|
+
sort(rest, x => x.source.getLatency());
|
|
820
|
+
let urls = await Promise.all([...candidates.slice(0, 1), ...rest].map(x => x.provider.getURL(path)));
|
|
816
821
|
return [...new Set(urls)];
|
|
817
822
|
}
|
|
818
823
|
|