sliftutils 1.7.91 → 1.7.92

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 CHANGED
@@ -2257,6 +2257,7 @@ declare module "sliftutils/storage/IArchives" {
2257
2257
  };
2258
2258
  export declare const STORAGE_WRONG_VALID_WINDOW = "REMOTE_STORAGE_WRONG_VALID_WINDOW_a7c1f04e";
2259
2259
  export declare const STORAGE_WRONG_ROUTE = "REMOTE_STORAGE_WRONG_ROUTE_c94d2e17";
2260
+ export declare const STORAGE_NOT_CONFIGURED = "REMOTE_STORAGE_NOT_CONFIGURED_e51b7d92";
2260
2261
  export declare const FULL_ROUTE: [number, number];
2261
2262
  export declare const VARIABLE_SHARD = "VARIABLE_SHARD_f0234jfah08fgyhfgyssdds83nmp";
2262
2263
  export declare function windowAcceptsWrites(validWindow: [number, number] | undefined): boolean;
@@ -3303,6 +3304,7 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3303
3304
  /** What this store is configured to be. It owns this: the routing config is a file IN the store, so the store reads it, applies it to itself, and re-applies it whenever the file changes - by our own write, or by a peer's copy arriving through synchronization. */
3304
3305
  storeConfig: StoreConfig;
3305
3306
  private appliedRoutingVersion;
3307
+ private appliedRouting;
3306
3308
  init: {
3307
3309
  (): Promise<void>;
3308
3310
  reset(): void;
@@ -3489,6 +3491,8 @@ declare module "sliftutils/storage/remoteStorage/blobStore" {
3489
3491
  * reason it aimed here is that its config was stale.
3490
3492
  */
3491
3493
  private assertWriteTarget;
3494
+ /** Exactly why this store has no configuration entries - which of the three possible reasons it is, with the values that decided it, because "not configured" alone is undiagnosable. */
3495
+ private unconfiguredDetail;
3492
3496
  /**
3493
3497
  * Whether a routing config may be written here. Two rules, and this is the one place either is
3494
3498
  * applied - a config only ever enters the system through a write, so a config that got in is a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliftutils",
3
- "version": "1.7.91",
3
+ "version": "1.7.92",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -163,6 +163,7 @@ export type ArchivesSource = {
163
163
  };
164
164
  export declare const STORAGE_WRONG_VALID_WINDOW = "REMOTE_STORAGE_WRONG_VALID_WINDOW_a7c1f04e";
165
165
  export declare const STORAGE_WRONG_ROUTE = "REMOTE_STORAGE_WRONG_ROUTE_c94d2e17";
166
+ export declare const STORAGE_NOT_CONFIGURED = "REMOTE_STORAGE_NOT_CONFIGURED_e51b7d92";
166
167
  export declare const FULL_ROUTE: [number, number];
167
168
  export declare const VARIABLE_SHARD = "VARIABLE_SHARD_f0234jfah08fgyhfgyssdds83nmp";
168
169
  export declare function windowAcceptsWrites(validWindow: [number, number] | undefined): boolean;
@@ -218,6 +218,9 @@ export const STORAGE_WRONG_VALID_WINDOW = "REMOTE_STORAGE_WRONG_VALID_WINDOW_a7c
218
218
  // Error marker a server includes when a freshly-stamped write's key routes outside the shards this server handles (the client's config disagrees with the server's - clients re-resolve once)
219
219
  export const STORAGE_WRONG_ROUTE = "REMOTE_STORAGE_WRONG_ROUTE_c94d2e17";
220
220
 
221
+ // Error marker a server includes when a write reaches a store whose own routing config has NO entry for it on that server (or that has no routing config at all) - accepting the write would store data nothing scans or reconciles. Clients re-resolve once, exactly like the markers above: the usual cause is the client running a different config than the store.
222
+ export const STORAGE_NOT_CONFIGURED = "REMOTE_STORAGE_NOT_CONFIGURED_e51b7d92";
223
+
221
224
  export const FULL_ROUTE: [number, number] = [0, 1];
222
225
 
223
226
  // A key containing this sentinel doesn't have a fixed shard: setVariableShard picks the (lowest latency, up) write shard, appends "_<value in the shard's route>" directly after the sentinel, and returns the materialized key. getRoute treats that suffix as a complete route override.
@@ -67,6 +67,7 @@ export declare class BlobStore {
67
67
  /** What this store is configured to be. It owns this: the routing config is a file IN the store, so the store reads it, applies it to itself, and re-applies it whenever the file changes - by our own write, or by a peer's copy arriving through synchronization. */
68
68
  storeConfig: StoreConfig;
69
69
  private appliedRoutingVersion;
70
+ private appliedRouting;
70
71
  init: {
71
72
  (): Promise<void>;
72
73
  reset(): void;
@@ -253,6 +254,8 @@ export declare class BlobStore {
253
254
  * reason it aimed here is that its config was stale.
254
255
  */
255
256
  private assertWriteTarget;
257
+ /** Exactly why this store has no configuration entries - which of the three possible reasons it is, with the values that decided it, because "not configured" alone is undiagnosable. */
258
+ private unconfiguredDetail;
256
259
  /**
257
260
  * Whether a routing config may be written here. Two rules, and this is the one place either is
258
261
  * applied - a config only ever enters the system through a write, so a config that got in is a
@@ -3,7 +3,7 @@ import { lazy } from "socket-function/src/caching";
3
3
  import { timeInMinute, sort } from "socket-function/src/misc";
4
4
  import {
5
5
  IArchives, ArchiveFileInfo, ArchivesSource, ArchivesSyncStatus, ChangesAfterConfig, FindConfig, HostedConfig, RemoteConfig, SourceConfig, assertValidLastModified,
6
- windowsAcceptWrites, SyncActivity, FULL_ROUTE, FULL_VALID_WINDOW, STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE,
6
+ windowsAcceptWrites, SyncActivity, FULL_ROUTE, FULL_VALID_WINDOW, STORAGE_WRONG_VALID_WINDOW, STORAGE_WRONG_ROUTE, STORAGE_NOT_CONFIGURED,
7
7
  } from "../IArchives";
8
8
  import { ArchivesDisk, applyFindInfoShape } from "../ArchivesDisk";
9
9
  import { ROUTING_FILE, getRoute, routeContains, routeIntersection, parseRoutingData, assertValidRemoteConfig, getConfigVersion, sourceIdentity, sourcePersistentUrl } from "./remoteConfig";
@@ -122,6 +122,8 @@ export class BlobStore {
122
122
  public storeConfig: StoreConfig;
123
123
  // The routing config this store last applied, so a write of an identical (or older) one is not re-applied
124
124
  private appliedRoutingVersion = -1;
125
+ // ...and the config itself, so a rejection can say exactly what the store IS running instead of guessing
126
+ private appliedRouting: RemoteConfig | undefined;
125
127
 
126
128
  // #region Main interface
127
129
 
@@ -161,9 +163,15 @@ export class BlobStore {
161
163
  if (routing && !entries.length) {
162
164
  console.warn(`The routing config in store ${this.folder} (version ${getConfigVersion(routing)}) does not name this store (${JSON.stringify(this.storeName)}) as one of ours, so it stays a disk-only store`);
163
165
  }
166
+ let previousVersion = this.appliedRoutingVersion;
167
+ let newVersion = routing && getConfigVersion(routing) || -1;
168
+ if (newVersion !== previousVersion) {
169
+ console.log(`Store ${JSON.stringify(this.storeName)} (folder ${this.folder}) adopted routing config version ${previousVersion} -> ${newVersion}: ${entries.length} entr${entries.length === 1 && "y" || "ies"} of ours ${JSON.stringify(entries.map(x => ({ validWindow: x.validWindow, route: x.route || FULL_ROUTE })))}, ${peers.length} peer(s)`);
170
+ }
164
171
  this.storeConfig.update(entries);
165
172
  this.updateSources(this.planSources(peers));
166
- this.appliedRoutingVersion = routing && getConfigVersion(routing) || -1;
173
+ this.appliedRoutingVersion = newVersion;
174
+ this.appliedRouting = routing;
167
175
  if (routing) {
168
176
  this.config?.onRoutingApplied?.(routing);
169
177
  }
@@ -790,8 +798,9 @@ export class BlobStore {
790
798
  */
791
799
  private assertWriteTarget(key: string, route: number, lastModified: number | undefined): void {
792
800
  if (!this.storeConfig.all().length) {
793
- logWrongTargetRejection(`Rejecting write of ${JSON.stringify(key)} (store ${this.folder}): this server has no entry in the bucket's routing config, so nothing would ever scan or reconcile what it stored`);
794
- throw new Error(`${STORAGE_WRONG_VALID_WINDOW} This server is not in the routing config for this bucket, so it cannot accept writes (store ${this.folder}). Re-resolve the currently valid source and retry.`);
801
+ let detail = this.unconfiguredDetail();
802
+ logWrongTargetRejection(`Rejecting write of ${JSON.stringify(key)}: ${detail}`);
803
+ throw new Error(`${STORAGE_NOT_CONFIGURED} Cannot write ${JSON.stringify(key)}: ${detail} Data written to a store with no configuration entry would never be scanned or reconciled, so accepting it would silently lose it. Re-resolve the currently valid source and retry - or, if this store is genuinely meant to take this write, write the bucket's routing config with an entry naming it on this server.`);
795
804
  }
796
805
  if (!lastModified) {
797
806
  this.assertFreshWriteTarget(key, Date.now(), route);
@@ -803,6 +812,21 @@ export class BlobStore {
803
812
  throw new Error(`${STORAGE_WRONG_ROUTE} This store does not handle route ${route} (key ${JSON.stringify(key)}, our routes: ${JSON.stringify(this.storeConfig.all().map(x => x.route || FULL_ROUTE))}, store ${this.folder}). Re-resolve the source for this key and retry.`);
804
813
  }
805
814
 
815
+ /** Exactly why this store has no configuration entries - which of the three possible reasons it is, with the values that decided it, because "not configured" alone is undiagnosable. */
816
+ private unconfiguredDetail(): string {
817
+ let routing = this.appliedRouting;
818
+ let identity = `store ${JSON.stringify(this.storeName)} (folder ${this.folder})`;
819
+ if (!routing) {
820
+ return `${identity} has no routing config at all - nothing has ever written ${JSON.stringify(ROUTING_FILE)} into it.`;
821
+ }
822
+ let objects = routing.sources.filter(x => typeof x !== "string") as SourceConfig[];
823
+ let named = objects.filter(x => x.name === this.storeName);
824
+ if (!named.length) {
825
+ return `${identity} is running routing config version ${getConfigVersion(routing)}, which has no entry named ${JSON.stringify(this.storeName)} (its entries are named ${JSON.stringify(objects.map(x => x.name))}).`;
826
+ }
827
+ return `${identity} is running routing config version ${getConfigVersion(routing)}, whose ${named.length} entr${named.length === 1 && "y" || "ies"} named ${JSON.stringify(this.storeName)} point(s) at ${JSON.stringify(named.map(x => x.url))} - none of which is THIS server, so the config says this store lives elsewhere.`;
828
+ }
829
+
806
830
  /**
807
831
  * Whether a routing config may be written here. Two rules, and this is the one place either is
808
832
  * applied - a config only ever enters the system through a write, so a config that got in is a
@@ -826,7 +850,7 @@ export class BlobStore {
826
850
  let timeValid = this.storeConfig.all().filter(x => writeTime >= x.validWindow[0] && writeTime < x.validWindow[1]);
827
851
  if (!timeValid.length) {
828
852
  logWrongTargetRejection(`Rejecting fresh write of ${JSON.stringify(key)} (store ${this.folder}): writeTime ${writeTime} (${new Date(writeTime).toISOString()}) is outside all our valid windows ${JSON.stringify(this.storeConfig.all().map(x => x.validWindow))} (a switchover moved the write target)`);
829
- throw new Error(`${STORAGE_WRONG_VALID_WINDOW} This store is not a valid write target at ${writeTime} (our valid windows: ${JSON.stringify(this.storeConfig.all().map(x => x.validWindow))}, store ${this.folder}). Re-resolve the currently valid source and retry.`);
853
+ throw new Error(`${STORAGE_WRONG_VALID_WINDOW} Cannot write ${JSON.stringify(key)}: its write time ${writeTime} (${new Date(writeTime).toISOString()}) is outside every valid window of store ${JSON.stringify(this.storeName)} (windows: ${JSON.stringify(this.storeConfig.all().map(x => x.validWindow))}, folder ${this.folder}) - a switchover moved the write target. Re-resolve the currently valid source and retry.`);
830
854
  }
831
855
  if (!timeValid.some(x => routeContains(x.route, route))) {
832
856
  logWrongTargetRejection(`Rejecting fresh write of ${JSON.stringify(key)} (store ${this.folder}): route ${route} is outside our routes ${JSON.stringify(timeValid.map(x => x.route || FULL_ROUTE))} at writeTime ${writeTime} (the client's shard config is stale)`);
@@ -3,7 +3,7 @@ import { delay } from "socket-function/src/batching";
3
3
  import {
4
4
  IArchives, RemoteConfig, RemoteConfigBase, SourceConfig,
5
5
  ArchiveFileInfo, ArchivesConfig, ArchivesSyncStatus, ChangesAfterConfig, DelConfig, FindConfig, GetConfig, GetInfoConfig, MoveFileConfig, SetConfig, SetLargeFileConfig, STORAGE_WRONG_VALID_WINDOW,
6
- STORAGE_WRONG_ROUTE, FULL_ROUTE, VARIABLE_SHARD, LARGE_SET_THRESHOLD, bufferChunkStream,
6
+ STORAGE_WRONG_ROUTE, STORAGE_NOT_CONFIGURED, FULL_ROUTE, VARIABLE_SHARD, LARGE_SET_THRESHOLD, bufferChunkStream,
7
7
  } from "../IArchives";
8
8
  import { copyArchiveFile } from "../archiveHelpers";
9
9
  import {
@@ -451,11 +451,11 @@ export class ArchivesChain implements IArchives {
451
451
  errors.push(message);
452
452
  continue;
453
453
  }
454
- // A wrong-window/route rejection means OUR config is stale, so every source we'd fall back to is judged by the same stale config - refresh it and restart the pass instead
455
- if ((message.includes(STORAGE_WRONG_VALID_WINDOW) || message.includes(STORAGE_WRONG_ROUTE)) && !retriedWrongTarget) {
454
+ // A wrong-window/route/unconfigured rejection means OUR config disagrees with the server's, so every source we'd fall back to is judged by the same stale config - refresh it and restart the pass instead
455
+ if ((message.includes(STORAGE_WRONG_VALID_WINDOW) || message.includes(STORAGE_WRONG_ROUTE) || message.includes(STORAGE_NOT_CONFIGURED)) && !retriedWrongTarget) {
456
456
  retriedWrongTarget = true;
457
457
  wrongTarget = true;
458
- await this.prepareWrongTargetRetry(state, message.includes(STORAGE_WRONG_VALID_WINDOW) && "window" || "route");
458
+ await this.prepareWrongTargetRetry(state, message.includes(STORAGE_WRONG_VALID_WINDOW) && "window" || message.includes(STORAGE_WRONG_ROUTE) && "route" || "unconfigured");
459
459
  break;
460
460
  }
461
461
  if (source.isConnected()) throw e;
@@ -481,6 +481,7 @@ export class ArchivesChain implements IArchives {
481
481
  private async runPrimary<T>(config: { write?: boolean; route?: number; timeout?: SmartTimeout }, run: (archives: IArchives, sourceUrl: string) => Promise<T>): Promise<T> {
482
482
  let retriedWrongWindow = false;
483
483
  let retriedWrongRoute = false;
484
+ let retriedNotConfigured = false;
484
485
  let deadline = Date.now() + PRIMARY_RETRY_TIMEOUT;
485
486
  let attempt = 0;
486
487
  while (true) {
@@ -509,6 +510,11 @@ export class ArchivesChain implements IArchives {
509
510
  await this.prepareWrongTargetRetry(state, "route");
510
511
  continue;
511
512
  }
513
+ if (message.includes(STORAGE_NOT_CONFIGURED) && !retriedNotConfigured) {
514
+ retriedNotConfigured = true;
515
+ await this.prepareWrongTargetRetry(state, "unconfigured");
516
+ continue;
517
+ }
512
518
  if (target && !target.isConnected()) target.noteFailure();
513
519
  if (!config.write && target && (attempt === 1 || attempt % 3 === 0)) {
514
520
  // The reason we try the HTTP request: httpsRequest has better DNS retrying capabilities than our WebSocket server (we have more control over it), so just using it can fix some DNS issues, which can propagate to fix the WebSocket connection. It is still the primary source's own data, so the no-fallback semantics hold - and ArchivesUrl has no setup cost, so making one on the spot is fine.
@@ -570,7 +576,7 @@ export class ArchivesChain implements IArchives {
570
576
  }
571
577
 
572
578
  private lastConfigRefresh = 0;
573
- private async prepareWrongTargetRetry(state: ChainState, kind: "window" | "route"): Promise<void> {
579
+ private async prepareWrongTargetRetry(state: ChainState, kind: "window" | "route" | "unconfigured"): Promise<void> {
574
580
  if (kind === "window") {
575
581
  let now = Date.now();
576
582
  let nearBoundary = state.sources.some(source => source.config.validWindow.some(t => t > 0 && t < Number.MAX_SAFE_INTEGER && Math.abs(t - now) <= WRONG_TARGET_BOUNDARY_WINDOW));
@@ -582,7 +588,8 @@ export class ArchivesChain implements IArchives {
582
588
  }
583
589
  if (Date.now() - this.lastConfigRefresh < CONFIG_REFRESH_THROTTLE) return;
584
590
  this.lastConfigRefresh = Date.now();
585
- console.log(`Write rejected by ${this.getDebugName()} (wrong ${kind}): our config is stale (likely a deploy switchover); refreshing it and retrying`);
591
+ let reason = kind === "window" && "wrong valid window" || kind === "route" && "wrong route" || "the store has no configuration entry on that server";
592
+ console.log(`Write rejected by ${this.getDebugName()} (${reason}): our config disagrees with the server's (likely stale, or a deploy switchover); refreshing it and retrying`);
586
593
  await this.refreshActiveConfig();
587
594
  }
588
595