sliftutils 1.7.85 → 1.7.87

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.
@@ -5,7 +5,7 @@ import { timeInMinute, sort, promiseObj } from "socket-function/src/misc";
5
5
  import { formatNumber, formatTime } from "socket-function/src/formatting/format";
6
6
  import {
7
7
  IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, ChangesAfterConfig, FindConfig, HostedConfig, SourceConfig, assertValidLastModified,
8
- windowAcceptsWrites, SyncActivity, FULL_ROUTE, STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, copyArchiveFile,
8
+ windowAcceptsWrites, windowsAcceptWrites, SyncActivity, FULL_ROUTE, STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, copyArchiveFile,
9
9
  } from "../IArchives";
10
10
  import { ArchivesDisk, applyFindInfoShape } from "../ArchivesDisk";
11
11
  import { ArchivesBackblaze } from "../backblaze";
@@ -166,11 +166,11 @@ export type BlobSourceSpec = {
166
166
  identity: string;
167
167
  // See ArchivesSource.url
168
168
  url: string;
169
- validWindow: [number, number];
169
+ validWindows: [number, number][];
170
170
  route?: [number, number];
171
171
  noFullSync?: boolean;
172
172
  // See ArchivesSource.intermediate
173
- intermediate?: boolean;
173
+ intermediate?: string;
174
174
  // See ArchivesSource.sourceConfig (absent for the base disk source)
175
175
  sourceConfig?: SourceConfig;
176
176
  // Only called for sources that don't match an existing live slot
@@ -470,7 +470,7 @@ export class BlobStore implements IBucketStore {
470
470
  indexSize: this.mem.size,
471
471
  sources: this.sources.map((x, i) => ({
472
472
  debugName: x.source.getDebugName(),
473
- validWindow: x.validWindow,
473
+ validWindows: x.validWindows,
474
474
  route: x.route,
475
475
  noFullSync: x.noFullSync,
476
476
  supportsChangesAfter: this.sourceStates[i].supportsChangesAfter,
@@ -530,14 +530,14 @@ export class BlobStore implements IBucketStore {
530
530
  if (entries) {
531
531
  this.entries = entries;
532
532
  }
533
- let setWindow = (i: number, window: [number, number]) => {
534
- let old = this.sources[i].validWindow;
535
- if (old[0] === window[0] && old[1] === window[1]) return;
536
- console.log(`Valid window changed for ${this.sources[i].source.getDebugName()} (store ${this.folder}): [${old.join(", ")}] -> [${window.join(", ")}]`);
537
- this.sources[i].validWindow = window;
533
+ let setWindows = (i: number, windows: [number, number][]) => {
534
+ let old = this.sources[i].validWindows;
535
+ if (JSON.stringify(old) === JSON.stringify(windows)) return;
536
+ console.log(`Valid windows changed for ${this.sources[i].source.getDebugName()} (store ${this.folder}): ${JSON.stringify(old)} -> ${JSON.stringify(windows)}`);
537
+ this.sources[i].validWindows = windows;
538
538
  };
539
- setWindow(0, specs[0].validWindow);
540
- // Live slots pair with specs by identity, in order (the same endpoint can appear several times, e.g. one entry per window)
539
+ setWindows(0, specs[0].validWindows);
540
+ // Live slots pair with specs by identity. Each endpoint is now ONE spec carrying all its windows, so this is a 1:1 pairing; any leftover duplicate slots from before (an endpoint that used to be split into several slots) go unmatched below and are collapsed away.
541
541
  let liveByIdentity = new Map<string, number[]>();
542
542
  let originalLength = this.sources.length;
543
543
  for (let i = 1; i < originalLength; i++) {
@@ -556,7 +556,7 @@ export class BlobStore implements IBucketStore {
556
556
  let slot = liveByIdentity.get(spec.identity)?.shift();
557
557
  if (slot !== undefined) {
558
558
  matched.add(slot);
559
- setWindow(slot, spec.validWindow);
559
+ setWindows(slot, spec.validWindows);
560
560
  let existing = this.sources[slot];
561
561
  if (JSON.stringify(existing.route) !== JSON.stringify(spec.route)) {
562
562
  console.log(`Route changed for ${existing.source.getDebugName()} (store ${this.folder}): ${JSON.stringify(existing.route)} -> ${JSON.stringify(spec.route)}`);
@@ -567,7 +567,7 @@ export class BlobStore implements IBucketStore {
567
567
  continue;
568
568
  }
569
569
  let source = spec.create();
570
- this.sources.push({ source, url: spec.url, validWindow: spec.validWindow, route: spec.route, noFullSync: spec.noFullSync, intermediate: spec.intermediate, sourceConfig: spec.sourceConfig, identity: spec.identity });
570
+ this.sources.push({ source, url: spec.url, validWindows: spec.validWindows, route: spec.route, noFullSync: spec.noFullSync, intermediate: spec.intermediate, sourceConfig: spec.sourceConfig, identity: spec.identity });
571
571
  this.sourceStates.push(newSourceState());
572
572
  this.sourceFileCounts.push(0);
573
573
  this.sourceByteCounts.push(0);
@@ -580,7 +580,7 @@ export class BlobStore implements IBucketStore {
580
580
  if (!this.isLive(i) || matched.has(i)) continue;
581
581
  this.removeSource(i);
582
582
  }
583
- let deadline = this.sources[0].validWindow[1] - WINDOW_END_FLUSH_MARGIN;
583
+ let deadline = this.baseWriteWindowEnd() - WINDOW_END_FLUSH_MARGIN;
584
584
  let recapped = 0;
585
585
  for (let entry of this.overlay.values()) {
586
586
  if (entry.fastFlushAt <= deadline && entry.slowFlushAt <= deadline) continue;
@@ -921,10 +921,10 @@ export class BlobStore implements IBucketStore {
921
921
  }
922
922
  }
923
923
 
924
- // 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
924
+ // 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. (Dead only once EVERY window has ended - an intermediate normally has just one.)
925
925
  private isDeadIntermediate(sourceIndex: number): boolean {
926
- let { intermediate, validWindow } = this.sources[sourceIndex];
927
- return !!intermediate && validWindow[1] <= Date.now();
926
+ let { intermediate, validWindows } = this.sources[sourceIndex];
927
+ return !!intermediate && validWindows.every(w => w[1] <= Date.now());
928
928
  }
929
929
 
930
930
  // 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.
@@ -987,9 +987,9 @@ export class BlobStore implements IBucketStore {
987
987
 
988
988
  // 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.
989
989
  private async reconcileSource(sourceIndex: number, listing: Map<string, number>): Promise<void> {
990
- let { source, validWindow, route } = this.sources[sourceIndex];
990
+ let { source, validWindows, route } = this.sources[sourceIndex];
991
991
  let state = this.sourceStates[sourceIndex];
992
- let acceptsWrites = windowAcceptsWrites(validWindow);
992
+ let acceptsWrites = windowsAcceptWrites(validWindows);
993
993
  let targetSourcesListIndex = this.sourcesListIndexOfSlot(sourceIndex);
994
994
  let pushed = 0;
995
995
  let failed = 0;
@@ -1253,7 +1253,7 @@ export class BlobStore implements IBucketStore {
1253
1253
  writeDelay = DEFAULT_FAST_WRITE_DELAY;
1254
1254
  }
1255
1255
  // The delay never extends past our own valid window's end (minus the margin, so the writes are on disk before the next window's source takes over - a deploy switchover is just this too, since its remap ends our window). Past that point fast writes write through immediately.
1256
- let deadline = this.sources[0].validWindow[1] - WINDOW_END_FLUSH_MARGIN;
1256
+ let deadline = this.baseWriteWindowEnd() - WINDOW_END_FLUSH_MARGIN;
1257
1257
  if (writeDelay > 0 && Date.now() < deadline) {
1258
1258
  // Own disk + our own storage servers (type-"remote") flush within MAX_REMOTE_FAST_BUFFER; the non-"remote" sources (backblaze) wait the full writeDelay
1259
1259
  let fastFlushAt = Math.min(Date.now() + Math.min(writeDelay, MAX_REMOTE_FAST_BUFFER), deadline);
@@ -1266,11 +1266,21 @@ export class BlobStore implements IBucketStore {
1266
1266
  await this.writeToSources(key, data, writeTime);
1267
1267
  }
1268
1268
 
1269
+ // The end of our own (base disk) write window that CONTAINS now - the deadline fast writes must flush before, so the next window's source has the data on handoff. The LATEST end among covering windows (overlapping windows hand off at the last one). No window contains now (an inert store, or a moment between our windows) -> 0, i.e. flush through immediately.
1270
+ private baseWriteWindowEnd(): number {
1271
+ let now = Date.now();
1272
+ let end = 0;
1273
+ for (let w of this.sources[0].validWindows) {
1274
+ if (w[0] <= now && now < w[1]) end = Math.max(end, w[1]);
1275
+ }
1276
+ return end;
1277
+ }
1278
+
1269
1279
  private getWritableSources(config?: { ignoreWindow?: boolean }): number[] {
1270
1280
  let writable: number[] = [];
1271
1281
  for (let i = 0; i < this.sources.length; i++) {
1272
1282
  if (!this.isLive(i)) continue;
1273
- if (!config?.ignoreWindow && !windowAcceptsWrites(this.sources[i].validWindow)) continue;
1283
+ if (!config?.ignoreWindow && !windowsAcceptWrites(this.sources[i].validWindows)) continue;
1274
1284
  writable.push(i);
1275
1285
  }
1276
1286
  return writable;
@@ -1406,7 +1416,7 @@ export class BlobStore implements IBucketStore {
1406
1416
  for (let i = 0; i < this.sources.length; i++) {
1407
1417
  if (!this.isLive(i)) continue;
1408
1418
  let sourceEntry = this.sources[i];
1409
- if (!windowAcceptsWrites(sourceEntry.validWindow)) continue;
1419
+ if (!windowsAcceptWrites(sourceEntry.validWindows)) continue;
1410
1420
  let source = sourceEntry.source;
1411
1421
  if (!(source instanceof ArchivesBackblaze)) continue;
1412
1422
  void source.del(key).catch((e: Error) => {