sliftutils 1.7.26 → 1.7.28

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.
@@ -2,10 +2,6 @@
2
2
 
3
3
  This is the reasoning behind the remote storage system (createArchives / storageServer / BlobStore). The code documents the mechanics; this documents the ideas that must survive refactors.
4
4
 
5
- ## Configuration updates happen only on startup
6
-
7
- A client adopts a routing config once, during initialization, and then runs on it. Configs never automatically upgrade mid-run: the 5-minute poll picks up whatever the sources currently hold (so a developer's change spreads), but the version-gated *upgrade* — writing a newer configured version over an older stored one — happens only at startup. This is deliberate: config changes are made by a developer running code, and that developer is present at startup. It also prevents reversion loops — if a server briefly held a newer version and then went away, running clients don't cling to that phantom version; they follow what the surviving sources actually hold, and only a fresh startup with an explicitly newer configured version moves things forward again.
8
-
9
5
  ## Scanning + tombstones + versions make any two sources mergeable
10
6
 
11
7
  Every store fully rescans its sources' metadata (on startup and periodically), and every write — including deletions — is ordered by last-write time. Deletions are tombstones: an empty file IS a missing file, kept as a size-0 index entry (for a week) so the deletion itself propagates and reconciles like any other write. Because everything is a timestamped write and scans are bidirectional (pull what they have, push what they're missing), any two sources can be merged in any order and converge to the same state: newest write time wins, per file. There is no operation whose loss corrupts the system — a failed background write, a missed delete, a source that was down for a day — the next scan reconciles it.
@@ -16,6 +12,16 @@ Each bucket stores its complete routing config (the full redundancy list) inside
16
12
 
17
13
  If the client tries to do a write where the valid state is far enough away or the sharding is wrong, it will re-download the routing config throttled, so it only does this at most once every 30 seconds, and retry the request.
18
14
 
15
+ Storage routing JSON is only written to if we have write access and it's only written to on startup, it doesn't propagate. that way things don't revert without the developer intentionally rerunning it.
16
+
17
+ ## Redundancy, sharding, and deployment
18
+
19
+ For redundancy, we can just have multiple different configurations that will satisfy the same request. The first one is the one that we write to. If that one's down, we don't do writes.
20
+
21
+ However, there's also sharding, where we have different route ranges that values can hash to. Values can explicitly try to write to a specific value via a special like hash override key. This allows us to sometimes make our rights hit a server which has less latency. In the case that the client writer will accept the fact that we might change the key.
22
+
23
+ The valid windows allow us to schedule deployments, so the nodes can switch over gracefully.
24
+
19
25
  ## BulkDatabase2 index + our own disk as just another source
20
26
 
21
27
  Each bucket's store keeps a BulkDatabase2 index of every file (path, write time, size, holding source), served from memory — existence checks and listings are extremely fast, never touching a source. The trick that keeps the index accurate: our own disk is not special-cased, it is simply the first synchronization source. The same scan/reconcile code that synchronizes remote sources also synchronizes the disk with the index, so the index self-heals from the same machinery instead of needing separate consistency logic.
@@ -8,10 +8,10 @@ import { BlobStore, IBucketStore, DEFAULT_FAST_WRITE_DELAY, WINDOW_END_FLUSH_MAR
8
8
  import {
9
9
  RemoteConfig, HostedConfig, BackblazeConfig, IArchives, ArchivesSource, ArchiveFileInfo, ArchivesConfig,
10
10
  ArchivesSyncStatus,
11
- WRITE_PAST_WINDOW_GRACE, STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
11
+ STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, FULL_ROUTE,
12
12
  } from "../IArchives";
13
13
  import { ROUTING_FILE, parseRoutingData, parseHostedUrl, buildFileUrl, getConfigVersion, getRoute, routeContains, routeIntersection } from "./remoteConfig";
14
- import { applyDeployRemap, getTakeoverAltPort, onTakeoverEvent } from "./deployTakeover";
14
+ import { applyDeployRemap, getTakeoverAltPort, getOwnWindowEndClip, onTakeoverEvent } from "./deployTakeover";
15
15
  import { createApiArchives } from "./createArchives";
16
16
  import type { IStorage } from "../IStorage";
17
17
  import type { AccessRequest, TrustRecord } from "./storageController";
@@ -445,9 +445,36 @@ function computeStorePlan(account: string, bucketName: string, routing: RemoteCo
445
445
  // Being removed from the config entirely is a valid window of NOTHING ([0, 0]): fast
446
446
  // writes flush immediately (nothing may sit in memory on a node that's been cut out),
447
447
  // while the disk data and index stay served.
448
+ // Internally every self entry is the SAME store - one process listening on one or more ports
449
+ // (a takeover's alternate-port middle window included). So the disk window merges all
450
+ // contiguous own windows: an options boundary or the port split must never force a pointless
451
+ // internal flush between two windows that are both us. Only the DYING process of a takeover
452
+ // clips its end - from the write handoff on, the data belongs to the successor process.
453
+ let diskWindow: [number, number] = [0, 0];
454
+ if (self) {
455
+ let [start, end] = self.validWindow;
456
+ let merged = true;
457
+ while (merged) {
458
+ merged = false;
459
+ for (let entry of selfEntries) {
460
+ let [entryStart, entryEnd] = entry.validWindow;
461
+ if (entryStart > end || entryEnd < start) continue;
462
+ if (entryStart < start || entryEnd > end) {
463
+ start = Math.min(start, entryStart);
464
+ end = Math.max(end, entryEnd);
465
+ merged = true;
466
+ }
467
+ }
468
+ }
469
+ let clip = getOwnWindowEndClip();
470
+ if (clip !== undefined && clip < end) {
471
+ end = clip;
472
+ }
473
+ diskWindow = [start, end];
474
+ }
448
475
  let ownIndexes = new Set(selfIndexes);
449
476
  let sourceSpecs: StorePlan["sourceSpecs"] = [{
450
- validWindow: self && self.validWindow || [0, 0],
477
+ validWindow: diskWindow,
451
478
  }];
452
479
  if (selfIndex !== -1) {
453
480
  for (let i = selfIndex + 1; i < effective.sources.length; i++) {
@@ -669,9 +696,9 @@ export async function writeBucketFile(account: string, bucketName: string, fileP
669
696
  // target from a stale view - it must re-resolve and retry at the correct source.
670
697
  let route = getRoute(filePath);
671
698
  if (!config?.lastModified && loaded.selfEntries.length) {
672
- let timeValid = loaded.selfEntries.filter(x => writeTime >= x.validWindow[0] - WRITE_PAST_WINDOW_GRACE && writeTime <= x.validWindow[1] + WRITE_PAST_WINDOW_GRACE);
699
+ let timeValid = loaded.selfEntries.filter(x => writeTime >= x.validWindow[0] && writeTime < x.validWindow[1]);
673
700
  if (!timeValid.length) {
674
- logWrongTargetRejection(`Rejecting fresh write of ${JSON.stringify(filePath)} to bucket ${account}/${bucketName}: writeTime ${writeTime} (${new Date(writeTime).toISOString()}) is outside all our valid windows ${JSON.stringify(loaded.selfEntries.map(x => x.validWindow))} even with the ${WRITE_PAST_WINDOW_GRACE}ms grace (a switchover moved the write target)`);
701
+ logWrongTargetRejection(`Rejecting fresh write of ${JSON.stringify(filePath)} to bucket ${account}/${bucketName}: writeTime ${writeTime} (${new Date(writeTime).toISOString()}) is outside all our valid windows ${JSON.stringify(loaded.selfEntries.map(x => x.validWindow))} (a switchover moved the write target)`);
675
702
  throw new Error(`${STORAGE_WRONG_VALID_WINDOW} This server is not a valid write target at ${writeTime} for bucket ${account}/${bucketName} (our valid windows: ${JSON.stringify(loaded.selfEntries.map(x => x.validWindow))}). Re-resolve the currently valid source and retry.`);
676
703
  }
677
704
  if (!timeValid.some(x => routeContains(x.route, route))) {