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
package/storage/IArchives.d.ts
CHANGED
|
@@ -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;
|
package/storage/IArchives.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
794
|
-
|
|
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}
|
|
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
|
|
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
|
-
|
|
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
|
|