querysub 0.508.0 → 0.509.0
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/bin/function-public.js +2 -0
- package/bin/server.js +0 -2
- package/package.json +3 -4
- package/spec.txt +5 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
- package/src/-g-core-values/NodeCapabilities.ts +4 -0
- package/src/-g-core-values/scheduledShutdown.ts +48 -0
- package/src/0-path-value-core/AuthorityLookup.ts +50 -9
- package/src/0-path-value-core/PathRouter.ts +10 -31
- package/src/0-path-value-core/PathRouterRouteOverride.ts +14 -98
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +1 -4
- package/src/0-path-value-core/PathValueCommitter.ts +1 -6
- package/src/0-path-value-core/PathValueController.ts +5 -7
- package/src/0-path-value-core/PathWatcher.ts +8 -11
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +8 -22
- package/src/0-path-value-core/pathValueCore.ts +4 -5
- package/src/1-path-client/RemoteWatcher.ts +47 -34
- package/src/2-proxy/archiveMoveHarness.ts +2 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +11 -10
- package/src/3-path-functions/PathFunctionRunner.ts +165 -24
- package/src/3-path-functions/functionReplay.ts +2 -0
- package/src/3-path-functions/syncSchema.ts +0 -2
- package/src/4-deploy/edgeBootstrap.ts +10 -0
- package/src/4-deploy/edgeClientWatcher.tsx +4 -6
- package/src/4-deploy/edgeNodes.ts +52 -12
- package/src/4-querysub/FunctionRunnerTracking.ts +290 -0
- package/src/4-querysub/Querysub.ts +11 -16
- package/src/4-querysub/QuerysubController.ts +13 -11
- package/src/4-querysub/permissions.ts +3 -0
- package/src/4-querysub/querysubPrediction.ts +2 -2
- package/src/config.ts +1 -3
- package/src/deployManager/components/ServiceDetailPage.tsx +127 -73
- package/src/deployManager/components/ServicesListPage.tsx +3 -3
- package/src/deployManager/machineApplyMainCode.ts +139 -153
- package/src/deployManager/machineController.ts +12 -61
- package/src/deployManager/machineSchema.ts +84 -3
- package/src/deployManager/setupMachineMain.ts +2 -1
- package/src/diagnostics/grossStats/GrossStatsController.ts +3 -2
- package/src/diagnostics/logs/errorTickets/autoFixer.ts +3 -2
- package/src/diagnostics/logs/logGitHashes.ts +1 -1
- package/src/diagnostics/managementPages.tsx +2 -0
- package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +36 -28
- package/src/diagnostics/misc-pages/FunctionCaptureAnalyzePage.tsx +1 -1
- package/src/library-components/NetworkSelector.tsx +77 -0
- package/src/misc.ts +19 -0
- package/src/user-implementation/userData.ts +1 -1
- package/src/zipThreaded.ts +3 -2
- package/bin/server-dev.js +0 -11
package/bin/function-public.js
CHANGED
package/bin/server.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.509.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
"deploy": "./bin/deploy.js",
|
|
32
32
|
"deploy-prefixes": "./bin/deploy-prefixes.js",
|
|
33
33
|
"server": "./bin/server.js",
|
|
34
|
-
"server-dev": "./bin/server-dev.js",
|
|
35
34
|
"server-public": "./bin/server-public.js",
|
|
36
35
|
"function": "./bin/function.js",
|
|
37
36
|
"function-public": "./bin/function-public.js",
|
|
@@ -71,10 +70,10 @@
|
|
|
71
70
|
"pako": "^2.1.0",
|
|
72
71
|
"peggy": "^5.0.6",
|
|
73
72
|
"sliftutils": "^1.7.5",
|
|
74
|
-
"socket-function": "^1.2.
|
|
73
|
+
"socket-function": "^1.2.5",
|
|
75
74
|
"terser": "^5.31.0",
|
|
76
75
|
"typenode": "^6.6.1",
|
|
77
|
-
"typesafecss": "
|
|
76
|
+
"typesafecss": "^0.32.0",
|
|
78
77
|
"yaml": "^2.5.0",
|
|
79
78
|
"yargs": "^15.3.1"
|
|
80
79
|
},
|
package/spec.txt
CHANGED
|
@@ -24,7 +24,7 @@ import { logDisk } from "../diagnostics/logs/diskLogger";
|
|
|
24
24
|
import { MaybePromise } from "socket-function/src/types";
|
|
25
25
|
import { getPathStr2 } from "../path";
|
|
26
26
|
|
|
27
|
-
let HEARTBEAT_INTERVAL = timeInMinute *
|
|
27
|
+
let HEARTBEAT_INTERVAL = timeInMinute * 5;
|
|
28
28
|
// Interval which we check other heartbeats
|
|
29
29
|
let CHECK_INTERVAL = HEARTBEAT_INTERVAL;
|
|
30
30
|
// If the heartbeat is older than thing, it fails the dead check
|
|
@@ -24,6 +24,7 @@ import { getPathStr2 } from "../path";
|
|
|
24
24
|
import { PromiseObj } from "../promise";
|
|
25
25
|
import { getTrueTimeOffset } from "socket-function/time/trueTimeShim";
|
|
26
26
|
import { getGitRefLive } from "../4-deploy/git";
|
|
27
|
+
import { getScheduledShutdownTime } from "./scheduledShutdown";
|
|
27
28
|
setImmediate(() => {
|
|
28
29
|
import("../diagnostics/MachineThreadInfo");
|
|
29
30
|
});
|
|
@@ -139,6 +140,8 @@ export type NodeMetadata = {
|
|
|
139
140
|
exposedControllers: string[];
|
|
140
141
|
trueTimeOffset: number;
|
|
141
142
|
gitRef: string;
|
|
143
|
+
// Set when the machine apply process has told us we will be shut down (due to a scheduled switchover)
|
|
144
|
+
scheduledShutdownTime?: number;
|
|
142
145
|
};
|
|
143
146
|
|
|
144
147
|
class NodeCapabilitiesControllerBase {
|
|
@@ -153,6 +156,7 @@ class NodeCapabilitiesControllerBase {
|
|
|
153
156
|
exposedControllers: Array.from(SocketFunction.exposedClasses),
|
|
154
157
|
trueTimeOffset: getTrueTimeOffset(),
|
|
155
158
|
gitRef: await getGitRefLive(),
|
|
159
|
+
scheduledShutdownTime: getScheduledShutdownTime(),
|
|
156
160
|
};
|
|
157
161
|
}
|
|
158
162
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Tracks when OUR node is scheduled to shut down (told to us by the machine apply process when a service switchover is scheduled). Other systems watch this to gracefully wind down ahead of the shutdown (deregistering as an edge node, rehoming watched paths, etc). The time is also exposed in our NodeCapabilities metadata, so other nodes can stop using us before we shutdown.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const IMMINENT_SHUTDOWN_WARNING_TIME = 60_000;
|
|
6
|
+
|
|
7
|
+
let scheduledShutdownTime: number | undefined;
|
|
8
|
+
let watchers = new Set<(time: number) => void>();
|
|
9
|
+
let imminentLogTimer: ReturnType<typeof setTimeout> | undefined;
|
|
10
|
+
|
|
11
|
+
export function getScheduledShutdownTime(): number | undefined {
|
|
12
|
+
return scheduledShutdownTime;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function setScheduledShutdownTime(time: number) {
|
|
16
|
+
console.log(`Our node is scheduled to shut down at ${new Date(time).toLocaleString()} (in ${Math.round((time - Date.now()) / 1000)}s) due to a service switchover`);
|
|
17
|
+
scheduledShutdownTime = time;
|
|
18
|
+
|
|
19
|
+
if (imminentLogTimer) {
|
|
20
|
+
clearTimeout(imminentLogTimer);
|
|
21
|
+
imminentLogTimer = undefined;
|
|
22
|
+
}
|
|
23
|
+
// setTimeout overflows (fires immediately) past ~24.8 days, and switchovers can be scheduled weeks out, so re-arm until we're within range.
|
|
24
|
+
const MAX_TIMEOUT_DELAY = 2 ** 31 - 1;
|
|
25
|
+
const armImminentLog = () => {
|
|
26
|
+
let delay = time - Date.now() - IMMINENT_SHUTDOWN_WARNING_TIME;
|
|
27
|
+
if (delay > MAX_TIMEOUT_DELAY) {
|
|
28
|
+
imminentLogTimer = setTimeout(armImminentLog, MAX_TIMEOUT_DELAY);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
imminentLogTimer = setTimeout(() => {
|
|
32
|
+
console.log(`We are about to be shut down due to a service switchover, at ${new Date(time).toLocaleString()}`);
|
|
33
|
+
}, Math.max(0, delay));
|
|
34
|
+
};
|
|
35
|
+
armImminentLog();
|
|
36
|
+
|
|
37
|
+
for (let watcher of watchers) {
|
|
38
|
+
watcher(time);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Calls the callback whenever our scheduled shutdown time is set or changed (immediately if it is already set). */
|
|
43
|
+
export function watchScheduledShutdown(callback: (time: number) => void) {
|
|
44
|
+
watchers.add(callback);
|
|
45
|
+
if (scheduledShutdownTime !== undefined) {
|
|
46
|
+
callback(scheduledShutdownTime);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -11,21 +11,35 @@ import { IdentityController_getCurrentReconnectNodeIdAssert } from "../-c-identi
|
|
|
11
11
|
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
12
12
|
import { isClient } from "../config2";
|
|
13
13
|
import { getPathStr1 } from "../path";
|
|
14
|
-
import { timeoutToError } from "../errors";
|
|
14
|
+
import { logErrors, timeoutToError } from "../errors";
|
|
15
15
|
import { AuthoritySpec, parsePrefixMatcher } from "./PathRouter";
|
|
16
|
+
|
|
16
17
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
17
18
|
import { getAllAuthoritySpec, getEmptyAuthoritySpec } from "./PathRouterServerAuthoritySpec";
|
|
19
|
+
import { getScheduledShutdownTime, watchScheduledShutdown } from "../-g-core-values/scheduledShutdown";
|
|
18
20
|
|
|
19
21
|
setImmediate(() => import("../3-path-functions/syncSchema"));
|
|
20
22
|
|
|
21
23
|
let NETWORK_POLL_INTERVAL = timeInMinute * 1;
|
|
22
24
|
let CALL_TIMEOUT = timeInSecond * 5;
|
|
23
25
|
|
|
26
|
+
// Every node's per-node poll is created together at discovery, so without jitter all of their
|
|
27
|
+
// networkSyncPaths calls fire in lockstep each NETWORK_POLL_INTERVAL — and when many nodes are
|
|
28
|
+
// dead their CALL_TIMEOUT waits pile up into one spike. Each node instead drifts an extra wait
|
|
29
|
+
// by its own random velocity (wrapping at this window), which spreads the calls out and keeps
|
|
30
|
+
// them from re-aligning over time. Kept small so no single sync is delayed by much.
|
|
31
|
+
const SYNC_JITTER_WINDOW = timeInSecond * 10;
|
|
32
|
+
|
|
33
|
+
// Nodes this close to their scheduled shutdown are avoided as sources (only used if nothing else can satisfy the request).
|
|
34
|
+
const SHUTDOWN_AVOID_WINDOW = timeInMinute * 10;
|
|
35
|
+
|
|
24
36
|
export type AuthorityEntry = {
|
|
25
37
|
nodeId: string;
|
|
26
38
|
authoritySpec: AuthoritySpec;
|
|
27
39
|
entryReceivedTime: number;
|
|
28
40
|
isReady: boolean;
|
|
41
|
+
// This can be far in the future (a switchover scheduled weeks out), so routing doesn't avoid the node immediately — only once within SHUTDOWN_AVOID_WINDOW (see nodeIsShuttingDownSoon).
|
|
42
|
+
scheduledShutdownTime?: number;
|
|
29
43
|
};
|
|
30
44
|
type AuthorityTopology = {
|
|
31
45
|
// nodeId =>
|
|
@@ -127,15 +141,21 @@ class AuthorityLookup {
|
|
|
127
141
|
});
|
|
128
142
|
await Promise.all(promises);
|
|
129
143
|
this.didInitialSync = true;
|
|
144
|
+
|
|
145
|
+
// Push our scheduled shutdown time to the other nodes right away, so they can start avoiding us (instead of waiting up to a full poll interval).
|
|
146
|
+
watchScheduledShutdown(() => {
|
|
147
|
+
logErrors(this.syncAllNow());
|
|
148
|
+
});
|
|
130
149
|
});
|
|
131
150
|
|
|
132
151
|
|
|
133
|
-
private updatePaths(nodeId: string, spec: AuthoritySpec, isReady: boolean) {
|
|
152
|
+
private updatePaths(nodeId: string, spec: AuthoritySpec, isReady: boolean, scheduledShutdownTime?: number) {
|
|
134
153
|
let entry: AuthorityEntry = {
|
|
135
154
|
nodeId,
|
|
136
155
|
authoritySpec: spec,
|
|
137
156
|
entryReceivedTime: Date.now(),
|
|
138
157
|
isReady,
|
|
158
|
+
scheduledShutdownTime,
|
|
139
159
|
};
|
|
140
160
|
let prevEntry = this.topology.nodes.get(nodeId);
|
|
141
161
|
// Try to preserve === property where possible, so we can use it as a cache key
|
|
@@ -145,22 +165,36 @@ class AuthorityLookup {
|
|
|
145
165
|
this.topology.nodes.set(nodeId, entry);
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
public
|
|
168
|
+
public getScheduledShutdownTime(nodeId: string): number | undefined {
|
|
169
|
+
return this.topology.nodes.get(nodeId)?.scheduledShutdownTime;
|
|
170
|
+
}
|
|
171
|
+
public nodeIsShuttingDownSoon(nodeId: string): boolean {
|
|
172
|
+
let time = this.getScheduledShutdownTime(nodeId);
|
|
173
|
+
return time !== undefined && time - Date.now() < SHUTDOWN_AVOID_WINDOW;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public async networkSyncPaths(config: {
|
|
149
177
|
spec: AuthoritySpec;
|
|
150
178
|
isReady: boolean;
|
|
179
|
+
scheduledShutdownTime?: number;
|
|
180
|
+
}): Promise<{
|
|
181
|
+
spec: AuthoritySpec;
|
|
182
|
+
isReady: boolean;
|
|
183
|
+
scheduledShutdownTime?: number;
|
|
151
184
|
}> {
|
|
152
185
|
let nodeId = IdentityController_getCurrentReconnectNodeIdAssert();
|
|
153
|
-
this.updatePaths(nodeId,
|
|
186
|
+
this.updatePaths(nodeId, config.spec, config.isReady, config.scheduledShutdownTime);
|
|
154
187
|
console.info(`Received network sync, returning our data`, {
|
|
155
188
|
nodeId,
|
|
156
|
-
otherSpec,
|
|
157
|
-
isReady,
|
|
189
|
+
otherSpec: config.spec,
|
|
190
|
+
isReady: config.isReady,
|
|
158
191
|
ourSpec: this.ourSpec,
|
|
159
192
|
ourIsReady: this.ourIsReady,
|
|
160
193
|
});
|
|
161
194
|
return {
|
|
162
195
|
spec: this.ourSpec,
|
|
163
196
|
isReady: this.ourIsReady,
|
|
197
|
+
scheduledShutdownTime: getScheduledShutdownTime(),
|
|
164
198
|
};
|
|
165
199
|
}
|
|
166
200
|
|
|
@@ -169,9 +203,13 @@ class AuthorityLookup {
|
|
|
169
203
|
|
|
170
204
|
private connectTo = cache((nodeId: string) => {
|
|
171
205
|
let stopObj = { stop: false };
|
|
206
|
+
let syncJitter = Math.random() * NETWORK_POLL_INTERVAL;
|
|
207
|
+
let syncJitterVelocity = Math.random() * SYNC_JITTER_WINDOW;
|
|
172
208
|
let promise = runInfinitePollCallAtStart(NETWORK_POLL_INTERVAL, async () => {
|
|
173
209
|
// NOTE: We keep calling even on disconnect, until it is removed from node discovery, so we handle reconnections. AND if it is removed from node discovery and reconnects, that's fine, it will tell us about the rediscovery as if was a new node, and we will setup it again.
|
|
174
210
|
await this.syncNodeSerial(nodeId)();
|
|
211
|
+
syncJitter = (syncJitter + syncJitterVelocity) % SYNC_JITTER_WINDOW;
|
|
212
|
+
await delay(syncJitter);
|
|
175
213
|
}, stopObj);
|
|
176
214
|
|
|
177
215
|
const onNodeRemoved = () => {
|
|
@@ -219,14 +257,17 @@ class AuthorityLookup {
|
|
|
219
257
|
prefixes: prefixes.map(p => parsePrefixMatcher(p)),
|
|
220
258
|
routeStart: 0,
|
|
221
259
|
routeEnd: 1,
|
|
222
|
-
networks: "all",
|
|
223
260
|
}, true);
|
|
224
261
|
return;
|
|
225
262
|
}
|
|
226
263
|
|
|
227
264
|
let otherPaths = await timeoutToError(
|
|
228
265
|
CALL_TIMEOUT,
|
|
229
|
-
AuthorityLookupController.nodes[nodeId].networkSyncPaths(
|
|
266
|
+
AuthorityLookupController.nodes[nodeId].networkSyncPaths({
|
|
267
|
+
spec: this.ourSpec,
|
|
268
|
+
isReady: this.ourIsReady,
|
|
269
|
+
scheduledShutdownTime: getScheduledShutdownTime(),
|
|
270
|
+
}),
|
|
230
271
|
() => new Error(`Timeout calling networkSyncPaths for ${nodeId}`)
|
|
231
272
|
);
|
|
232
273
|
console.info(`Finished network sync`, {
|
|
@@ -236,7 +277,7 @@ class AuthorityLookup {
|
|
|
236
277
|
ourSpec: this.ourSpec,
|
|
237
278
|
ourIsReady: this.ourIsReady,
|
|
238
279
|
});
|
|
239
|
-
this.updatePaths(nodeId, otherPaths.spec, otherPaths.isReady);
|
|
280
|
+
this.updatePaths(nodeId, otherPaths.spec, otherPaths.isReady, otherPaths.scheduledShutdownTime);
|
|
240
281
|
this.disconnectWatch(nodeId);
|
|
241
282
|
}
|
|
242
283
|
|
|
@@ -8,7 +8,6 @@ import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
|
8
8
|
import { unique } from "../misc";
|
|
9
9
|
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
10
10
|
import { getRoutingOverride, hasPrefixHash } from "./PathRouterRouteOverride";
|
|
11
|
-
import { DEFAULT_NETWORK } from "../config";
|
|
12
11
|
import { sha256 } from "js-sha256";
|
|
13
12
|
import { rangesOverlap, removeRange } from "../rangeMath";
|
|
14
13
|
import { decodeParentFilter } from "./hackedPackedPathParentFiltering";
|
|
@@ -38,23 +37,8 @@ export type AuthoritySpec = {
|
|
|
38
37
|
prefixes: PrefixMatcher[];
|
|
39
38
|
// - Make sure to set this if you just want the prefix values, otherwise you will get all that that prefixes don't match (the prefixes exclude prefix match but where route does not match).
|
|
40
39
|
excludeDefault?: boolean;
|
|
41
|
-
// The networks this authority satisfies. Unset is equivalent to ["default"]. Paths only route to non-default networks if their routing override key explicitly asks for that network. "all" is ONLY for client proxy specs (the querysub server the client talks to proxies to the real authorities, which do their own network routing), and is not valid for real authorities.
|
|
42
|
-
networks?: string[] | "all";
|
|
43
40
|
};
|
|
44
41
|
|
|
45
|
-
function getSpecNetworkList(networks: string[] | undefined): string[] {
|
|
46
|
-
return networks && networks.length > 0 && networks || [DEFAULT_NETWORK];
|
|
47
|
-
}
|
|
48
|
-
export function specSatisfiesNetwork(spec: AuthoritySpec, network: string | undefined): boolean {
|
|
49
|
-
if (spec.networks === "all") return true;
|
|
50
|
-
return getSpecNetworkList(spec.networks).includes(network || DEFAULT_NETWORK);
|
|
51
|
-
}
|
|
52
|
-
export function networksOverlap(spec1: AuthoritySpec, spec2: AuthoritySpec): boolean {
|
|
53
|
-
if (spec1.networks === "all" || spec2.networks === "all") return true;
|
|
54
|
-
let networks2 = getSpecNetworkList(spec2.networks);
|
|
55
|
-
return getSpecNetworkList(spec1.networks).some(x => networks2.includes(x));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
42
|
// If you want to match just a simple path, use parsePrefixMatcher. It won't match ===, so get the parent if you want that (and then filter the result).
|
|
59
43
|
export type PrefixMatcher = {
|
|
60
44
|
prefix: string;
|
|
@@ -158,8 +142,7 @@ export class PathRouter {
|
|
|
158
142
|
let { path, spec } = config;
|
|
159
143
|
path = hack_stripPackedPath(path);
|
|
160
144
|
let override = getRoutingOverride(path);
|
|
161
|
-
if (
|
|
162
|
-
if (override && override.route !== undefined) {
|
|
145
|
+
if (override) {
|
|
163
146
|
if (spec.excludeDefault && !hasPrefixHash({ spec, prefixHash: override.prefixHash })) return -1;
|
|
164
147
|
if (override.route < spec.routeStart || override.route >= spec.routeEnd) return -1;
|
|
165
148
|
return override.route;
|
|
@@ -202,7 +185,7 @@ export class PathRouter {
|
|
|
202
185
|
public static getSingleKeyRoute(key: string): number {
|
|
203
186
|
if (key && this.lastKeyRoute.key === key) return this.lastKeyRoute.route;
|
|
204
187
|
let override = getRoutingOverride(key);
|
|
205
|
-
if (override
|
|
188
|
+
if (override) {
|
|
206
189
|
this.lastKeyRoute.key = key;
|
|
207
190
|
this.lastKeyRoute.route = override.route;
|
|
208
191
|
return override.route;
|
|
@@ -348,7 +331,6 @@ export class PathRouter {
|
|
|
348
331
|
prefixes: group.prefixes,
|
|
349
332
|
routeStart: 0,
|
|
350
333
|
routeEnd: 1,
|
|
351
|
-
networks: ourSpec.networks,
|
|
352
334
|
}
|
|
353
335
|
});
|
|
354
336
|
let routeIndex = Math.floor(route * splitCount);
|
|
@@ -392,7 +374,6 @@ export class PathRouter {
|
|
|
392
374
|
|
|
393
375
|
@measureFnc
|
|
394
376
|
public static overlapsAuthority(authority1: AuthoritySpec, authority2: AuthoritySpec): boolean {
|
|
395
|
-
if (!networksOverlap(authority1, authority2)) return false;
|
|
396
377
|
// TODO: This becomes complicated because of exclude default, although I feel like there has to be a way to simplify it? Eh... whatever.
|
|
397
378
|
|
|
398
379
|
let doRangesOverlap = rangesOverlap({ start: authority1.routeStart, end: authority1.routeEnd }, { start: authority2.routeStart, end: authority2.routeEnd });
|
|
@@ -457,7 +438,6 @@ export class PathRouter {
|
|
|
457
438
|
let { target } = config;
|
|
458
439
|
let allSources = authorityLookup.getTopologySync();
|
|
459
440
|
allSources = allSources.filter(x => !isOwnNodeId(x.nodeId));
|
|
460
|
-
allSources = allSources.filter(x => networksOverlap(x.authoritySpec, target));
|
|
461
441
|
// THIS is normal during initial server startup
|
|
462
442
|
if (!allSources.length) {
|
|
463
443
|
return [];
|
|
@@ -517,7 +497,7 @@ export class PathRouter {
|
|
|
517
497
|
let preferredNodeIds = new Set(config?.preferredNodeIds ?? []);
|
|
518
498
|
for (let group of cohesiveGroups) {
|
|
519
499
|
shuffle(group, Math.random());
|
|
520
|
-
sort(group, x => preferredNodeIds.has(x.nodeId)
|
|
500
|
+
sort(group, x => (authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 2 || 0) + (preferredNodeIds.has(x.nodeId) && 0 || 1));
|
|
521
501
|
let targetStart = target.routeStart;
|
|
522
502
|
let targetEnd = target.routeEnd;
|
|
523
503
|
// If the group doesn't hash the same way we do, then we need to expand our range to be the full range of the group, so we don't miss anything.
|
|
@@ -577,12 +557,10 @@ export class PathRouter {
|
|
|
577
557
|
range: { start: number; end: number };
|
|
578
558
|
}[];
|
|
579
559
|
} {
|
|
580
|
-
let
|
|
581
|
-
let parentRange = parentFilter || {
|
|
560
|
+
let parentRange = decodeParentFilter(path) || {
|
|
582
561
|
start: 0,
|
|
583
562
|
end: 1,
|
|
584
563
|
};
|
|
585
|
-
let parentNetwork = parentFilter?.network;
|
|
586
564
|
path = hack_stripPackedPath(path);
|
|
587
565
|
let preferredNodeIds = new Set(config?.preferredNodeIds ?? []);
|
|
588
566
|
|
|
@@ -591,7 +569,6 @@ export class PathRouter {
|
|
|
591
569
|
// - The different route case is how the FuntionRunner works, and without it large databases couldn't run functions. However, most applications won't directly use it.
|
|
592
570
|
// NOTE: The only own nodes flag is actually so we can access this before the topology finishes synchronizing. Because we want to be able to call get child read nodes from path value core for some really basic stuff. It also is what the caller wants, but that's not a good enough reason to add this check. The reason we have this check is because without it, the topology won't have finished synchronizing and startup won't work.
|
|
593
571
|
let allSources = config?.onlyOwnNodes ? [{ nodeId: getOwnNodeId(), authoritySpec: authorityLookup.getOurSpec() }] : authorityLookup.getTopologySync();
|
|
594
|
-
allSources = allSources.filter(x => specSatisfiesNetwork(x.authoritySpec, parentNetwork));
|
|
595
572
|
// Prefer our own node
|
|
596
573
|
sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
|
|
597
574
|
|
|
@@ -603,7 +580,7 @@ export class PathRouter {
|
|
|
603
580
|
let hasPrefix = allSources.filter(x => x.authoritySpec.prefixes.some(y => isPrefixParent(y, path))).map(x => x.authoritySpec);
|
|
604
581
|
if (hasPrefix.length > 0) {
|
|
605
582
|
shuffle(hasPrefix, Math.random());
|
|
606
|
-
sort(hasPrefix, x => preferredNodeIds.has(x.nodeId)
|
|
583
|
+
sort(hasPrefix, x => (authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 2 || 0) + (preferredNodeIds.has(x.nodeId) && 0 || 1));
|
|
607
584
|
|
|
608
585
|
let missingRanges: { start: number; end: number }[] = [{
|
|
609
586
|
...parentRange
|
|
@@ -645,7 +622,7 @@ export class PathRouter {
|
|
|
645
622
|
});
|
|
646
623
|
if (nestedMatches.length > 0) {
|
|
647
624
|
shuffle(nestedMatches, Math.random());
|
|
648
|
-
sort(nestedMatches, x => preferredNodeIds.has(x.nodeId)
|
|
625
|
+
sort(nestedMatches, x => (authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 2 || 0) + (preferredNodeIds.has(x.nodeId) && 0 || 1));
|
|
649
626
|
sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
|
|
650
627
|
return {
|
|
651
628
|
// Only need to take the first match. Our path is picked by the prefix, and the prefix only hashes the direct child, and we're more deeply nested than that, which means... the route for all of our children will be identical, so this node matches all of our children.
|
|
@@ -664,7 +641,7 @@ export class PathRouter {
|
|
|
664
641
|
// Same as prefix matches. Not preferred, and not preferred over being under a prefix, but required for some root data, or data with no prefixes.
|
|
665
642
|
if (fullPathMatches.length > 0) {
|
|
666
643
|
shuffle(fullPathMatches, Math.random());
|
|
667
|
-
sort(fullPathMatches, x => preferredNodeIds.has(x.nodeId)
|
|
644
|
+
sort(fullPathMatches, x => (authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 2 || 0) + (preferredNodeIds.has(x.nodeId) && 0 || 1));
|
|
668
645
|
sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
|
|
669
646
|
let missingRanges: { start: number; end: number }[] = [{
|
|
670
647
|
...parentRange
|
|
@@ -697,7 +674,7 @@ export class PathRouter {
|
|
|
697
674
|
if (!config?.onlyOwnNodes) {
|
|
698
675
|
|
|
699
676
|
// NOTE: We *could* actually synchronize it even if it doesn't have a prefix shard as we can fall back to just the full path sharding. However, it becomes very complicated if we want a specific range, and then it becomes complicated if it then switches to prefix hashing (With the nodes that were using the full path hashing slowly going away). AND... key synchronization IS slow, so it's good to discourage it in general.
|
|
700
|
-
console.error(`Want to sync a prefix which is not under an existing prefix, nor equal to a prefix. 1) The servers are down. 1.5) The servers are in an inconsistent state (one shard range added a parent path, the other didn't). 2) Don't access the .keys() 3) call addRoutingPrefixForDeploy to add a route/parent route explicitly (as is done in PathFunctionRunner.ts).
|
|
677
|
+
console.error(`Want to sync a prefix which is not under an existing prefix, nor equal to a prefix. 1) The servers are down. 1.5) The servers are in an inconsistent state (one shard range added a parent path, the other didn't). 2) Don't access the .keys() 3) call addRoutingPrefixForDeploy to add a route/parent route explicitly (as is done in PathFunctionRunner.ts). Path: ${JSON.stringify(path)}`, { path, allSources: allSources.map(x => x.authoritySpec) });
|
|
701
678
|
}
|
|
702
679
|
return { nodes: [] };
|
|
703
680
|
}
|
|
@@ -712,6 +689,8 @@ export class PathRouter {
|
|
|
712
689
|
public static getReadyAuthority(path: string): AuthorityEntry | undefined {
|
|
713
690
|
let candidates = authorityLookup.getTopologySync();
|
|
714
691
|
shuffle(candidates, Math.random());
|
|
692
|
+
// Only use nodes that are about to shut down if nothing else matches
|
|
693
|
+
sort(candidates, x => authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 1 || 0);
|
|
715
694
|
for (let candidate of candidates) {
|
|
716
695
|
if (!candidate.isReady) continue;
|
|
717
696
|
if (this.matchesAuthoritySpec(candidate.authoritySpec, path)) {
|
|
@@ -2,7 +2,6 @@ import { cacheLimited } from "socket-function/src/caching";
|
|
|
2
2
|
import { AuthoritySpec, PathRouter } from "./PathRouter";
|
|
3
3
|
import { sha256 } from "js-sha256";
|
|
4
4
|
import { getPathFromStr } from "../path";
|
|
5
|
-
import { DEFAULT_NETWORK } from "../config";
|
|
6
5
|
|
|
7
6
|
function getPrefixHash(prefix: string): string {
|
|
8
7
|
return Buffer.from(sha256(prefix), "hex").toString("base64").slice(0, 12);
|
|
@@ -17,102 +16,21 @@ let keySpecialIdentifier = (
|
|
|
17
16
|
+ String.fromCharCode(0xF4D7)
|
|
18
17
|
);
|
|
19
18
|
|
|
20
|
-
export function assertValidNetwork(network: string) {
|
|
21
|
-
if (!/^[a-z]+$/.test(network)) {
|
|
22
|
-
throw new Error(`Networks must be lowercase letters (a-z) only, was ${JSON.stringify(network)}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// The route slot is either a route number ("0.12345"), a network ("dev"), or both ("dev:0.12345"). Networks are strictly lowercase letters and routes are strictly numeric, so the forms are unambiguous.
|
|
27
|
-
function encodeRouteSlot(config: { route?: number; network?: string }): string {
|
|
28
|
-
let { route, network } = config;
|
|
29
|
-
let routeStr = route !== undefined && route.toString().slice(0, 7) || "";
|
|
30
|
-
if (network && routeStr) return `${network}:${routeStr}`;
|
|
31
|
-
if (network) return network;
|
|
32
|
-
return routeStr;
|
|
33
|
-
}
|
|
34
|
-
function decodeRouteSlot(slot: string): { route?: number; network?: string } | undefined {
|
|
35
|
-
if (!slot) return undefined;
|
|
36
|
-
let network: string | undefined;
|
|
37
|
-
let routeStr = slot;
|
|
38
|
-
let colonIndex = slot.indexOf(":");
|
|
39
|
-
if (colonIndex >= 0) {
|
|
40
|
-
network = slot.slice(0, colonIndex);
|
|
41
|
-
routeStr = slot.slice(colonIndex + 1);
|
|
42
|
-
} else if (/^[a-z]+$/.test(slot)) {
|
|
43
|
-
return { network: slot };
|
|
44
|
-
}
|
|
45
|
-
if (network && !/^[a-z]+$/.test(network)) return undefined;
|
|
46
|
-
let route = parseFloat(routeStr);
|
|
47
|
-
if (isNaN(route)) return undefined;
|
|
48
|
-
return { route, network };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function createRoutingOverrideKeyBase(config: {
|
|
52
|
-
originalKey: string;
|
|
53
|
-
// Hashed to get the route, if route is not directly provided
|
|
54
|
-
routeKey?: string;
|
|
55
|
-
route?: number;
|
|
56
|
-
network?: string;
|
|
57
|
-
// This is the prefix it has the equivalent of. We need this, so if something excludes default, it doesn't automatically get every routing overridden value. Required whenever a route (or routeKey) is set.
|
|
58
|
-
remappedPrefix?: string;
|
|
59
|
-
}) {
|
|
60
|
-
let { originalKey, routeKey, route, network, remappedPrefix } = config;
|
|
61
|
-
if (network) {
|
|
62
|
-
assertValidNetwork(network);
|
|
63
|
-
}
|
|
64
|
-
if (route === undefined && routeKey !== undefined) {
|
|
65
|
-
route = PathRouter.getSingleKeyRoute(routeKey);
|
|
66
|
-
}
|
|
67
|
-
if (route === undefined && !network) {
|
|
68
|
-
throw new Error(`createRoutingOverrideKeyBase requires at least one of route, routeKey, or network`);
|
|
69
|
-
}
|
|
70
|
-
if (route !== undefined && !remappedPrefix) {
|
|
71
|
-
throw new Error(`createRoutingOverrideKeyBase requires remappedPrefix when a route is set, so excludeDefault authorities can match it correctly`);
|
|
72
|
-
}
|
|
73
|
-
let prefixHash = remappedPrefix && getPrefixHash(remappedPrefix) || "";
|
|
74
|
-
return keySpecialIdentifier + "!" + prefixHash + "!" + encodeRouteSlot({ route, network }) + "!" + originalKey;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
19
|
export function createRoutingOverrideKey(config: {
|
|
78
20
|
originalKey: string;
|
|
79
21
|
routeKey: string;
|
|
22
|
+
// This is the prefix it has the equivalent of. We need this, so if something excludes default, it doesn't automatically get every routing overridden value.
|
|
80
23
|
remappedPrefix: string;
|
|
81
|
-
network?: string;
|
|
82
24
|
}) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
/** Creates a key that routes to a specific network, but otherwise routes normally (via prefix / full path hashing). */
|
|
87
|
-
export function createNetworkKey(config: {
|
|
88
|
-
originalKey: string;
|
|
89
|
-
network: string;
|
|
90
|
-
}) {
|
|
91
|
-
return createRoutingOverrideKeyBase({ originalKey: config.originalKey, network: config.network });
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Rewrites the routing override part inside key (which must contain one) to be on the given network. */
|
|
95
|
-
export function setRoutingOverrideKeyNetwork(key: string, network: string): string {
|
|
96
|
-
assertValidNetwork(network);
|
|
97
|
-
if (!key.startsWith(keySpecialIdentifier)) {
|
|
98
|
-
return createNetworkKey({ originalKey: key, network });
|
|
99
|
-
}
|
|
100
|
-
let parts = key.split("!");
|
|
101
|
-
if (parts.length < 4) {
|
|
102
|
-
return createNetworkKey({ originalKey: key, network });
|
|
103
|
-
}
|
|
104
|
-
let slot = decodeRouteSlot(parts[2]) || {};
|
|
105
|
-
parts[2] = encodeRouteSlot({ route: slot.route, network });
|
|
106
|
-
return parts.join("!");
|
|
25
|
+
let { originalKey, routeKey, remappedPrefix } = config;
|
|
26
|
+
let route = PathRouter.getSingleKeyRoute(routeKey);
|
|
27
|
+
return keySpecialIdentifier + "!" + getPrefixHash(remappedPrefix) + "!" + route.toString().slice(0, 7) + "!" + originalKey;
|
|
107
28
|
}
|
|
108
29
|
|
|
109
|
-
export
|
|
30
|
+
export function getRoutingOverride(path: string): {
|
|
31
|
+
route: number;
|
|
110
32
|
prefixHash: string;
|
|
111
|
-
|
|
112
|
-
network?: string;
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
export function getRoutingOverride(path: string): RoutingOverride | undefined {
|
|
33
|
+
} | undefined {
|
|
116
34
|
if (!path.includes(keySpecialIdentifier)) return undefined;
|
|
117
35
|
let parts = getPathFromStr(path);
|
|
118
36
|
for (let part of parts) {
|
|
@@ -121,24 +39,22 @@ export function getRoutingOverride(path: string): RoutingOverride | undefined {
|
|
|
121
39
|
}
|
|
122
40
|
return undefined;
|
|
123
41
|
}
|
|
124
|
-
export function getRoutingOverridePart(part: string):
|
|
42
|
+
export function getRoutingOverridePart(part: string): {
|
|
43
|
+
prefixHash: string;
|
|
44
|
+
route: number;
|
|
45
|
+
} | undefined {
|
|
125
46
|
if (!part.startsWith(keySpecialIdentifier)) return undefined;
|
|
126
47
|
let parts = part.split("!");
|
|
127
48
|
if (parts.length < 4) return undefined;
|
|
128
49
|
let prefixHash = parts[1];
|
|
129
|
-
let
|
|
130
|
-
if (
|
|
50
|
+
let route = parseFloat(parts[2]);
|
|
51
|
+
if (isNaN(route)) return undefined;
|
|
131
52
|
return {
|
|
132
53
|
prefixHash,
|
|
133
|
-
route
|
|
134
|
-
network: slot.network,
|
|
54
|
+
route,
|
|
135
55
|
};
|
|
136
56
|
}
|
|
137
57
|
|
|
138
|
-
export function getPathNetwork(path: string): string {
|
|
139
|
-
return getRoutingOverride(path)?.network || DEFAULT_NETWORK;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
58
|
export const hasPrefixHash = cacheLimited(1000 * 10,
|
|
143
59
|
(config: { spec: AuthoritySpec, prefixHash: string }) => {
|
|
144
60
|
let { spec, prefixHash } = config;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import { AuthoritySpec, parsePrefixMatcher } from "./PathRouter";
|
|
3
3
|
import { getPathDepth } from "../path";
|
|
4
|
-
import { getAuthorityRange, getAuthorityExcludeDefault, getAuthorityPrefix
|
|
4
|
+
import { getAuthorityRange, getAuthorityExcludeDefault, getAuthorityPrefix } from "../config";
|
|
5
5
|
import { getShardPrefixes } from "./ShardPrefixes";
|
|
6
6
|
import { getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
7
7
|
|
|
@@ -22,7 +22,6 @@ export async function getOurAuthoritySpec(defaultToAll?: boolean): Promise<Autho
|
|
|
22
22
|
routeStart: 0,
|
|
23
23
|
routeEnd: 1,
|
|
24
24
|
prefixes: prefixMatchers,
|
|
25
|
-
networks: getNetworks(),
|
|
26
25
|
};
|
|
27
26
|
}
|
|
28
27
|
return undefined;
|
|
@@ -47,7 +46,6 @@ export async function getOurAuthoritySpec(defaultToAll?: boolean): Promise<Autho
|
|
|
47
46
|
routeEnd: rangeEnd,
|
|
48
47
|
prefixes: usePrefixes,
|
|
49
48
|
excludeDefault: excludeDefault || undefined,
|
|
50
|
-
networks: getNetworks(),
|
|
51
49
|
};
|
|
52
50
|
}
|
|
53
51
|
|
|
@@ -69,6 +67,5 @@ export async function getAllAuthoritySpec(): Promise<AuthoritySpec> {
|
|
|
69
67
|
routeStart: 0,
|
|
70
68
|
routeEnd: 1,
|
|
71
69
|
prefixes: prefixMatchers,
|
|
72
|
-
networks: getNetworks(),
|
|
73
70
|
};
|
|
74
71
|
}
|
|
@@ -20,8 +20,6 @@ import { debugNodeId } from "../-c-identity/IdentityController";
|
|
|
20
20
|
import { decodeNodeId } from "sliftutils/misc/https/certs";
|
|
21
21
|
import { getDomain } from "../config";
|
|
22
22
|
import { decodeParentFilter, encodeParentFilter } from "./hackedPackedPathParentFiltering";
|
|
23
|
-
import { getPathNetwork } from "./PathRouterRouteOverride";
|
|
24
|
-
import { DEFAULT_NETWORK } from "../config";
|
|
25
23
|
import { deepCloneCborx } from "../misc/cloneHelpers";
|
|
26
24
|
import { removeRange } from "../rangeMath";
|
|
27
25
|
import { registerShutdownHandler } from "../diagnostics/periodic";
|
|
@@ -207,9 +205,8 @@ class PathValueCommitter {
|
|
|
207
205
|
parentSyncs: [],
|
|
208
206
|
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
209
207
|
});
|
|
210
|
-
console.error(`There are no authorities for path ${pathValue.path}
|
|
208
|
+
console.error(`There are no authorities for path ${pathValue.path}. The write will be lost.`, {
|
|
211
209
|
path: pathValue.path,
|
|
212
|
-
network: getPathNetwork(pathValue.path),
|
|
213
210
|
timeId: pathValue.time.time,
|
|
214
211
|
source: pathValue.source,
|
|
215
212
|
otherAuthorities,
|
|
@@ -352,7 +349,6 @@ class PathValueCommitter {
|
|
|
352
349
|
remoteWatcher.getExistingWatchRemoteNodeId(path);
|
|
353
350
|
console.warn(`Ignoring value from wrong authority. Should have been ${debugNodeId(watchingAuthorityId)}, but was received from ${debugNodeId(batch.sourceNodeId)}.`, {
|
|
354
351
|
path,
|
|
355
|
-
network: getPathNetwork(path),
|
|
356
352
|
type,
|
|
357
353
|
timeId: value?.time.time,
|
|
358
354
|
source: value?.source,
|
|
@@ -394,7 +390,6 @@ class PathValueCommitter {
|
|
|
394
390
|
|
|
395
391
|
console.warn(`Ignoring parent path which we aren't watching. From ${debugNodeId(batch.sourceNodeId)}.`, {
|
|
396
392
|
parentPath,
|
|
397
|
-
network: decodeParentFilter(parentPath)?.network || DEFAULT_NETWORK,
|
|
398
393
|
sourceNodeId: debugNodeId(batch.sourceNodeId),
|
|
399
394
|
sourceNodeThreadId: decodeNodeId(batch.sourceNodeId, getDomain())?.threadId,
|
|
400
395
|
});
|