querysub 0.406.0 → 0.408.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/audit-disk-values.js +7 -0
- package/bin/deploy-prefixes.js +7 -0
- package/package.json +5 -3
- package/src/-a-archives/archiveCache.ts +12 -9
- package/src/-a-auth/certs.ts +1 -1
- package/src/-c-identity/IdentityController.ts +9 -1
- package/src/-f-node-discovery/NodeDiscovery.ts +63 -10
- package/src/0-path-value-core/AuthorityLookup.ts +14 -4
- package/src/0-path-value-core/PathRouter.ts +247 -117
- package/src/0-path-value-core/PathRouterRouteOverride.ts +1 -1
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +4 -2
- package/src/0-path-value-core/PathValueCommitter.ts +68 -31
- package/src/0-path-value-core/PathValueController.ts +77 -8
- package/src/0-path-value-core/PathWatcher.ts +46 -4
- package/src/0-path-value-core/ShardPrefixes.ts +6 -0
- package/src/0-path-value-core/ValidStateComputer.ts +20 -8
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +18 -55
- package/src/0-path-value-core/pathValueArchives.ts +19 -8
- package/src/0-path-value-core/pathValueCore.ts +75 -27
- package/src/0-path-value-core/startupAuthority.ts +9 -9
- package/src/1-path-client/RemoteWatcher.ts +217 -178
- package/src/1-path-client/pathValueClientWatcher.ts +6 -11
- package/src/2-proxy/pathValueProxy.ts +2 -3
- package/src/3-path-functions/PathFunctionRunner.ts +3 -1
- package/src/3-path-functions/syncSchema.ts +6 -2
- package/src/4-deploy/deployGetFunctionsInner.ts +1 -1
- package/src/4-deploy/deployPrefixes.ts +14 -0
- package/src/4-deploy/edgeNodes.ts +1 -1
- package/src/4-querysub/Querysub.ts +17 -5
- package/src/4-querysub/QuerysubController.ts +21 -10
- package/src/4-querysub/predictionQueue.tsx +3 -0
- package/src/4-querysub/querysubPrediction.ts +27 -20
- package/src/5-diagnostics/nodeMetadata.ts +17 -0
- package/src/diagnostics/NodeConnectionsPage.tsx +167 -0
- package/src/diagnostics/NodeViewer.tsx +11 -15
- package/src/diagnostics/PathDistributionInfo.tsx +102 -0
- package/src/diagnostics/SyncTestPage.tsx +19 -8
- package/src/diagnostics/auditDiskValues.ts +221 -0
- package/src/diagnostics/auditDiskValuesEntry.ts +43 -0
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +5 -1
- package/src/diagnostics/logs/TimeRangeSelector.tsx +3 -3
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +2 -0
- package/src/diagnostics/managementPages.tsx +10 -1
- package/src/diagnostics/misc-pages/ArchiveViewer.tsx +3 -2
- package/src/diagnostics/pathAuditer.ts +21 -0
- package/src/path.ts +9 -2
- package/src/rangeMath.ts +41 -0
- package/tempnotes.txt +5 -58
- package/test.ts +13 -295
- package/src/diagnostics/benchmark.ts +0 -139
- package/src/diagnostics/runSaturationTest.ts +0 -416
- package/src/diagnostics/satSchema.ts +0 -64
- package/src/test/mongoSatTest.tsx +0 -55
- package/src/test/satTest.ts +0 -193
- package/src/test/test.tsx +0 -552
|
@@ -9,6 +9,7 @@ import { unique } from "../misc";
|
|
|
9
9
|
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
10
10
|
import { getRoutingOverride, hasPrefixHash } from "./PathRouterRouteOverride";
|
|
11
11
|
import { sha256 } from "js-sha256";
|
|
12
|
+
import { rangesOverlap, removeRange } from "../rangeMath";
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
// Cases
|
|
@@ -36,23 +37,35 @@ export type AuthoritySpec = {
|
|
|
36
37
|
excludeDefault?: boolean;
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
export function debugSpec(spec: AuthoritySpec) {
|
|
41
|
+
return {
|
|
42
|
+
info: `${spec.routeStart}-${spec.routeEnd} (${spec.prefixes.length} prefixes${spec.excludeDefault ? " excluding default" : ""})`,
|
|
43
|
+
spec: { do: { not: { expand: { spec } } } },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getMatchingPrefix(spec: AuthoritySpec, path: string): string | undefined {
|
|
48
|
+
let longestPrefix: string | undefined;
|
|
49
|
+
for (let prefix of spec.prefixes) {
|
|
50
|
+
if (path.startsWith(prefix) && prefix !== path) {
|
|
51
|
+
if (!longestPrefix || longestPrefix.length < prefix.length) {
|
|
52
|
+
longestPrefix = prefix;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return longestPrefix;
|
|
57
|
+
}
|
|
58
|
+
|
|
39
59
|
|
|
40
60
|
export class PathRouter {
|
|
41
61
|
|
|
42
62
|
public static async waitUntilReady() {
|
|
43
63
|
await authorityLookup.startSyncing();
|
|
44
64
|
}
|
|
45
|
-
|
|
46
|
-
- The topology is really only used for the initial sync, which will use matchesAuthoritySpec, which gets the full routing value, AND, for disk storage.
|
|
47
|
-
*/
|
|
48
|
-
@measureFnc
|
|
49
|
-
public static getRouteChildKey(path: string): number {
|
|
50
|
-
let key = getLastPathPart(path);
|
|
51
|
-
return this.getSingleKeyRoute(key);
|
|
52
|
-
}
|
|
65
|
+
|
|
53
66
|
// NOTE: For non-prefix values, breaking up by routes on the file system becomes complicated, and so we just all non-prefix values in the same file. However, in memory, in some places, we need route values for every single path, such as for FunctionRunner, so it can distribute the function running evenly, without overlap.
|
|
54
67
|
@measureFnc
|
|
55
|
-
|
|
68
|
+
public static getRouteFull(config: {
|
|
56
69
|
path: string;
|
|
57
70
|
spec: AuthoritySpec;
|
|
58
71
|
}): number {
|
|
@@ -61,22 +74,38 @@ export class PathRouter {
|
|
|
61
74
|
path = hack_stripPackedPath(path);
|
|
62
75
|
let override = getRoutingOverride(path);
|
|
63
76
|
if (override) {
|
|
64
|
-
if (!hasPrefixHash({ spec, prefixHash: override.prefixHash })) return -1;
|
|
77
|
+
if (spec.excludeDefault && !hasPrefixHash({ spec, prefixHash: override.prefixHash })) return -1;
|
|
78
|
+
if (override.route < spec.routeStart || override.route >= spec.routeEnd) return -1;
|
|
65
79
|
return override.route;
|
|
66
80
|
}
|
|
67
81
|
|
|
68
|
-
let prefix = spec
|
|
82
|
+
let prefix = getMatchingPrefix(spec, path);
|
|
69
83
|
if (prefix) {
|
|
70
84
|
let key = getPathIndex(path, getPathDepth(prefix));
|
|
71
85
|
if (key === undefined) {
|
|
86
|
+
require("debugbreak")(2);
|
|
87
|
+
debugger;
|
|
72
88
|
throw new Error(`Impossible, hash index ${getPathDepth(prefix)} is out of range for path ${path}, but it matched the prefix ${prefix}`);
|
|
73
89
|
}
|
|
74
|
-
|
|
90
|
+
let route = this.getSingleKeyRoute(key);
|
|
91
|
+
if (route < spec.routeStart || route >= spec.routeEnd) return -1;
|
|
92
|
+
return route;
|
|
75
93
|
}
|
|
76
94
|
if (spec.excludeDefault) return -1;
|
|
77
|
-
let
|
|
78
|
-
if (
|
|
79
|
-
return
|
|
95
|
+
let route = this.getSingleKeyRoute(path);
|
|
96
|
+
if (route < spec.routeStart || route >= spec.routeEnd) return -1;
|
|
97
|
+
return route;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Mostly for debugging
|
|
101
|
+
@measureFnc
|
|
102
|
+
public static getAllRoutes(path: string): number[] {
|
|
103
|
+
let routes: number[] = [];
|
|
104
|
+
for (let authority of authorityLookup.getTopologySync()) {
|
|
105
|
+
let route = this.getRouteFull({ path, spec: authority.authoritySpec });
|
|
106
|
+
routes.push(route);
|
|
107
|
+
}
|
|
108
|
+
return routes;
|
|
80
109
|
}
|
|
81
110
|
|
|
82
111
|
private static lastKeyRoute = {
|
|
@@ -86,6 +115,12 @@ export class PathRouter {
|
|
|
86
115
|
// Takes a key which is a part of a path. Mostly used PathRouterRouteOverride, or other PathRouter helpers.
|
|
87
116
|
public static getSingleKeyRoute(key: string): number {
|
|
88
117
|
if (key && this.lastKeyRoute.key === key) return this.lastKeyRoute.route;
|
|
118
|
+
let override = getRoutingOverride(key);
|
|
119
|
+
if (override) {
|
|
120
|
+
this.lastKeyRoute.key = key;
|
|
121
|
+
this.lastKeyRoute.route = override.route;
|
|
122
|
+
return override.route;
|
|
123
|
+
}
|
|
89
124
|
let hash = fastHash(key);
|
|
90
125
|
let route = hash % (1000 * 1000 * 1000) / (1000 * 1000 * 1000);
|
|
91
126
|
this.lastKeyRoute.key = key;
|
|
@@ -104,6 +139,9 @@ export class PathRouter {
|
|
|
104
139
|
private static getPrefixHash(prefix: string): string {
|
|
105
140
|
return Buffer.from(sha256(prefix), "hex").toString("base64").slice(0, 6);
|
|
106
141
|
}
|
|
142
|
+
private static isPrefixHash(hash: string): boolean {
|
|
143
|
+
return hash.length === 6 && /^[a-zA-Z0-9]+$/.test(hash);
|
|
144
|
+
}
|
|
107
145
|
private static encodeIdentifier(config: { prefixes: string[]; rangeStart: number; rangeEnd: number } | "remaining"): string {
|
|
108
146
|
if (config === "remaining") return "P!REMAINING";
|
|
109
147
|
let { prefixes, rangeStart, rangeEnd } = config;
|
|
@@ -125,7 +163,7 @@ export class PathRouter {
|
|
|
125
163
|
return {
|
|
126
164
|
rangeStart: parseFloat(parts[1]),
|
|
127
165
|
rangeEnd: parseFloat(parts[2]),
|
|
128
|
-
prefixHashes: parts.slice(3),
|
|
166
|
+
prefixHashes: parts.slice(3).filter(this.isPrefixHash),
|
|
129
167
|
};
|
|
130
168
|
}
|
|
131
169
|
|
|
@@ -137,26 +175,19 @@ export class PathRouter {
|
|
|
137
175
|
// NOTE: The file size limit is 1024 bytes. But we also have our folder, etc, so we want to add enough buffer
|
|
138
176
|
// - Shorter hashes means we can store more, but there's a point when the collisions make it less useful.
|
|
139
177
|
const MAX_PREFIXES_PER_FILE = 50;
|
|
140
|
-
const PREFIX_COVER_FRACTION = 0.
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
178
|
+
const PREFIX_COVER_FRACTION = 0.99;
|
|
179
|
+
const TARGET_VALUES_PER_SHARD_GROUP = 10 * 1000 * 1000;
|
|
180
|
+
const TARGET_SHARD_SIZE = 50 * 1000;
|
|
181
|
+
const MIN_SHARD_FILE_COUNT = 10;
|
|
182
|
+
const SHARD_THRESHOLD = 1000;
|
|
145
183
|
|
|
146
184
|
let prefixes = ourSpec.prefixes.slice();
|
|
147
185
|
sort(prefixes, x => x.length);
|
|
148
|
-
function getPrefix(path: string): string | undefined {
|
|
149
|
-
for (let prefix of prefixes) {
|
|
150
|
-
if (path.startsWith(prefix) && prefix !== path) return prefix;
|
|
151
|
-
}
|
|
152
|
-
return undefined;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
186
|
|
|
156
187
|
// NOTE: If there are few enough path values for a prefix, we don't even need to calculate the routing hash.
|
|
157
188
|
let byPrefix = new Map<string | undefined, PathValue[]>();
|
|
158
189
|
for (let value of values) {
|
|
159
|
-
let prefix =
|
|
190
|
+
let prefix = getMatchingPrefix(ourSpec, value.path);
|
|
160
191
|
let values = byPrefix.get(prefix);
|
|
161
192
|
if (!values) {
|
|
162
193
|
values = [];
|
|
@@ -194,7 +225,7 @@ export class PathRouter {
|
|
|
194
225
|
}
|
|
195
226
|
let last = groups[groups.length - 1];
|
|
196
227
|
if (
|
|
197
|
-
last.count > 0 && last.count + prefixGroup.values.length >
|
|
228
|
+
last.count > 0 && last.count + prefixGroup.values.length > TARGET_VALUES_PER_SHARD_GROUP
|
|
198
229
|
|| last.prefixes.length >= MAX_PREFIXES_PER_FILE
|
|
199
230
|
) {
|
|
200
231
|
groups.push({
|
|
@@ -210,42 +241,44 @@ export class PathRouter {
|
|
|
210
241
|
prefixLeft -= prefixGroup.values.length;
|
|
211
242
|
}
|
|
212
243
|
|
|
213
|
-
|
|
214
244
|
let finalFiles = new Map<string, PathValue[]>();
|
|
215
245
|
for (let group of groups) {
|
|
216
|
-
if (group.
|
|
217
|
-
// Split by routing hash
|
|
218
|
-
let values = group.values.flat();
|
|
219
|
-
let splitCount = Math.ceil(values.length / TARGET_VALUES_PER_FILE);
|
|
220
|
-
let byRouteGroup = new Map<number, PathValue[]>();
|
|
221
|
-
let prefix = group.prefixes[0];
|
|
222
|
-
let hashIndex = getPathDepth(prefix);
|
|
223
|
-
for (let value of values) {
|
|
224
|
-
let key = getPathIndex(value.path, hashIndex);
|
|
225
|
-
if (key === undefined) {
|
|
226
|
-
throw new Error(`Impossible, hash index ${hashIndex} is out of range for path ${value.path}, but it matched the prefix ${prefix}`);
|
|
227
|
-
}
|
|
228
|
-
let route = this.getSingleKeyRoute(key);
|
|
229
|
-
let routeIndex = Math.floor(route * splitCount);
|
|
230
|
-
let routeValues = byRouteGroup.get(routeIndex);
|
|
231
|
-
if (!routeValues) {
|
|
232
|
-
routeValues = [];
|
|
233
|
-
byRouteGroup.set(routeIndex, routeValues);
|
|
234
|
-
}
|
|
235
|
-
routeValues.push(value);
|
|
236
|
-
}
|
|
237
|
-
for (let [routeIndex, routeValues] of byRouteGroup) {
|
|
238
|
-
let rangeStart = routeIndex / splitCount;
|
|
239
|
-
let rangeEnd = (routeIndex + 1) / splitCount;
|
|
240
|
-
let identifier = this.encodeIdentifier({ prefixes: [prefix], rangeStart, rangeEnd });
|
|
241
|
-
finalFiles.set(identifier, routeValues);
|
|
242
|
-
}
|
|
243
|
-
} else {
|
|
246
|
+
if (group.count < SHARD_THRESHOLD) {
|
|
244
247
|
let identifier = this.encodeIdentifier({ prefixes: group.prefixes, rangeStart: 0, rangeEnd: 1 });
|
|
245
248
|
finalFiles.set(identifier, group.values.flat());
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// Split by routing hash
|
|
252
|
+
let values = group.values.flat();
|
|
253
|
+
let splitCount = Math.max(MIN_SHARD_FILE_COUNT, Math.ceil(values.length / TARGET_SHARD_SIZE));
|
|
254
|
+
let byRouteGroup = new Map<number, PathValue[]>();
|
|
255
|
+
for (let value of values) {
|
|
256
|
+
let route = this.getRouteFull({
|
|
257
|
+
path: value.path,
|
|
258
|
+
spec: {
|
|
259
|
+
nodeId: "",
|
|
260
|
+
prefixes: group.prefixes,
|
|
261
|
+
routeStart: 0,
|
|
262
|
+
routeEnd: 1,
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
let routeIndex = Math.floor(route * splitCount);
|
|
266
|
+
let routeValues = byRouteGroup.get(routeIndex);
|
|
267
|
+
if (!routeValues) {
|
|
268
|
+
routeValues = [];
|
|
269
|
+
byRouteGroup.set(routeIndex, routeValues);
|
|
270
|
+
}
|
|
271
|
+
routeValues.push(value);
|
|
272
|
+
}
|
|
273
|
+
for (let [routeIndex, routeValues] of byRouteGroup) {
|
|
274
|
+
let rangeStart = routeIndex / splitCount;
|
|
275
|
+
let rangeEnd = (routeIndex + 1) / splitCount;
|
|
276
|
+
let identifier = this.encodeIdentifier({ prefixes: group.prefixes, rangeStart, rangeEnd });
|
|
277
|
+
finalFiles.set(identifier, routeValues);
|
|
246
278
|
}
|
|
247
279
|
}
|
|
248
280
|
|
|
281
|
+
// NOTE: There could be a huge number of prefixes and we can't pack them all into one file because of the prefix limit, so this will write any remaining values.
|
|
249
282
|
if (remainingValues.length > 0) {
|
|
250
283
|
let identifier = this.encodeIdentifier("remaining");
|
|
251
284
|
finalFiles.set(identifier, remainingValues.flat());
|
|
@@ -268,6 +301,46 @@ export class PathRouter {
|
|
|
268
301
|
return decodeObj.rangeStart < authority.routeEnd && decodeObj.rangeEnd > authority.routeStart;
|
|
269
302
|
}
|
|
270
303
|
|
|
304
|
+
@measureFnc
|
|
305
|
+
public static overlapsAuthority(authority1: AuthoritySpec, authority2: AuthoritySpec): boolean {
|
|
306
|
+
// TODO: This becomes complicated because of exclude default, although I feel like there has to be a way to simplify it? Eh... whatever.
|
|
307
|
+
|
|
308
|
+
// Normalize it so if only one excludes default, it's always going to be the second one.
|
|
309
|
+
if (authority1.excludeDefault && !authority2.excludeDefault) return this.overlapsAuthority(authority2, authority1);
|
|
310
|
+
|
|
311
|
+
let doRangesOverlap = rangesOverlap({ start: authority1.routeStart, end: authority1.routeEnd }, { start: authority2.routeStart, end: authority2.routeEnd });
|
|
312
|
+
|
|
313
|
+
// If their prefixes are identical, then it's purely a range check
|
|
314
|
+
if (authority1.prefixes.length === authority2.prefixes.length && authority1.prefixes.every(x => authority2.prefixes.includes(x))) {
|
|
315
|
+
return doRangesOverlap;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If they have any prefixes which are identical and the ranges overlap, then they overlap.
|
|
319
|
+
if (doRangesOverlap) {
|
|
320
|
+
if (authority1.prefixes.some(x => authority2.prefixes.includes(x))) {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// If any of their prefixes are under the prefix and match it, then that's a match.
|
|
325
|
+
if (authority1.prefixes.some(x => this.matchesAuthoritySpec(authority2, x))) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
if (authority2.prefixes.some(x => this.matchesAuthoritySpec(authority1, x))) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (authority1.excludeDefault && authority2.excludeDefault) {
|
|
332
|
+
// No shared prefixes, and none of them are nested under each other, and we don't include defaults, so neither match.
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
// If their prefixes are entirely unrelated, it means they're going to hash differently, so they do overlap.
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
@measureFnc
|
|
339
|
+
public static getAllOverlappingAuthorities(authority: AuthoritySpec): AuthoritySpec[] {
|
|
340
|
+
let allAuthorities = authorityLookup.getTopologySync();
|
|
341
|
+
return allAuthorities.filter(x => this.overlapsAuthority(authority, x.authoritySpec)).map(x => x.authoritySpec);
|
|
342
|
+
}
|
|
343
|
+
|
|
271
344
|
|
|
272
345
|
|
|
273
346
|
public static isLocalPath(path: string): boolean {
|
|
@@ -283,11 +356,12 @@ export class PathRouter {
|
|
|
283
356
|
|
|
284
357
|
|
|
285
358
|
// NOTE: This might return overlapping specs. Presently, this is only used when we're loading our initial data, so it's fine. However, if we use this for another purpose in the future, it might cause problems. So we might need to implement a different function for that theoretical future purpose.
|
|
359
|
+
// NOTE: Only takes remote nodes as presently this is just used during startup.
|
|
286
360
|
@measureFnc
|
|
287
361
|
public static getAuthoritySources(config: {
|
|
288
362
|
target: AuthoritySpec;
|
|
289
363
|
preferredNodeIds?: string[];
|
|
290
|
-
}):
|
|
364
|
+
}): AuthoritySpec[] {
|
|
291
365
|
let { target } = config;
|
|
292
366
|
let allSources = authorityLookup.getTopologySync();
|
|
293
367
|
allSources = allSources.filter(x => !isOwnNodeId(x.nodeId));
|
|
@@ -368,41 +442,21 @@ export class PathRouter {
|
|
|
368
442
|
for (let source of group) {
|
|
369
443
|
let s = source.routeStart;
|
|
370
444
|
let e = source.routeEnd;
|
|
371
|
-
|
|
372
|
-
for (let
|
|
373
|
-
let missingRange = missingRanges[i];
|
|
374
|
-
if (s >= missingRange.end || e <= missingRange.start) continue;
|
|
375
|
-
let startTaken = Math.max(missingRange.start, s);
|
|
376
|
-
let endTaken = Math.min(missingRange.end, e);
|
|
377
|
-
// NOTE: It's not ideal that we might have to fragment one node ID between multiple requests. However, in practice, there shouldn't be much fragmentation here. The ranges that our nodes are breaking down by should be consistent, so there's actually no overlap or subsets.
|
|
445
|
+
let { removedRanges } = removeRange(missingRanges, { start: s, end: e });
|
|
446
|
+
for (let removedRange of removedRanges) {
|
|
378
447
|
usedParts.push({
|
|
379
448
|
nodeId: source.nodeId,
|
|
380
|
-
routeStart:
|
|
381
|
-
routeEnd:
|
|
449
|
+
routeStart: removedRange.start,
|
|
450
|
+
routeEnd: removedRange.end,
|
|
382
451
|
prefixes: target.prefixes,
|
|
383
452
|
excludeDefault: target.excludeDefault,
|
|
384
|
-
useFullPathHash,
|
|
385
453
|
});
|
|
386
|
-
missingRanges.splice(i, 1);
|
|
387
|
-
// Add back the parts we didn't overlap
|
|
388
|
-
if (missingRange.start < s) {
|
|
389
|
-
missingRanges.push({
|
|
390
|
-
start: missingRange.start,
|
|
391
|
-
end: s,
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
if (missingRange.end > e) {
|
|
395
|
-
missingRanges.push({
|
|
396
|
-
start: e,
|
|
397
|
-
end: missingRange.end,
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
454
|
}
|
|
401
455
|
if (missingRanges.length === 0) break;
|
|
402
456
|
}
|
|
403
457
|
if (missingRanges.length === 0) {
|
|
404
458
|
if (!hashesSameAsTarget(group[0])) {
|
|
405
|
-
console.warn(`
|
|
459
|
+
console.warn(`Could only match a cohesive group that doesn't hash the same way we do. Expanding our range to be the full range of the group. This will be slower than it needs to, and might cause issues that result in only partial synchronization. This should be fixable with a deploy.`, config.target);
|
|
406
460
|
}
|
|
407
461
|
return usedParts;
|
|
408
462
|
}
|
|
@@ -414,67 +468,141 @@ export class PathRouter {
|
|
|
414
468
|
}
|
|
415
469
|
|
|
416
470
|
|
|
471
|
+
// NOTE: The returned nodes are guaranteed to hash in the same way (either all child key hashing for the path children, or all path hashing). This is required, otherwise it would mean if you take a single child path, It might have two different routing values depending on which node it matches, which means even if those ranges don't overlap, different routing values mean it could match two ranges, which is impossible and would break things (and it would also mean there would be values that wouldn't match anything, which I guess is even worse).
|
|
417
472
|
@measureFnc
|
|
418
473
|
public static getChildReadNodes(path: string, config?: {
|
|
419
474
|
preferredNodeIds?: string[];
|
|
475
|
+
onlyOwnNodes?: boolean;
|
|
420
476
|
}): {
|
|
421
|
-
// By default we hash the key directly under the path. However, If that is not how our nodes were sharded, as in they didn't use it as a prefix, then they will be sharded by hashing the full path, and so when we receive the data, we need to keep this in mind and filter the data based on the full path hash.
|
|
422
|
-
useFullPathHash?: boolean;
|
|
423
|
-
|
|
424
477
|
// NOTE: If at all possible, we will cover all ranges. Node of the returned nodes will be redundant.
|
|
425
478
|
// - Sorted by range.start
|
|
426
479
|
nodes: {
|
|
427
480
|
nodeId: string;
|
|
481
|
+
authoritySpec: AuthoritySpec;
|
|
428
482
|
// The range of hashes this node owns, for the child keys of path
|
|
429
483
|
// (If the node doesn't restrict the range, it will just be { start: 0, end: 1 })
|
|
430
484
|
range: { start: number; end: number };
|
|
431
485
|
}[];
|
|
432
486
|
} {
|
|
433
|
-
|
|
434
|
-
return { nodes: [{ nodeId: getOwnNodeId(), range: { start: 0, end: 1 } }] };
|
|
435
|
-
}
|
|
487
|
+
let preferredNodeIds = new Set(config?.preferredNodeIds ?? []);
|
|
436
488
|
|
|
437
489
|
// If a prefix is a parent of path, then it is the same as matching just the path directly
|
|
438
490
|
// (If our prefix directly equals one of the other matches, then it's more complicated, As then, the child keys of path are what is hashed, and so all the children will have different routes, so we might match multiple nodes. The same thing if we're matching the remaining case, in which case it's a full path hash, so the child key matters, and again, different routes).
|
|
439
491
|
// - 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.
|
|
440
|
-
let allSources = authorityLookup.getTopologySync();
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
492
|
+
let allSources = config?.onlyOwnNodes ? [{ nodeId: getOwnNodeId(), authoritySpec: authorityLookup.getOurSpec() }] : authorityLookup.getTopologySync();
|
|
493
|
+
// Prefer our own node
|
|
494
|
+
sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
// Direct prefixes always take priority, as almost everything is under a prefix anyways...
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
// Direct prefix. This happens for things like calls and functions, it requires more advanced routing as it means we're going to route between multiple servers, but... it is important
|
|
501
|
+
let hasPrefix = allSources.filter(x => x.authoritySpec.prefixes.some(y => y === path)).map(x => x.authoritySpec);
|
|
502
|
+
if (hasPrefix.length > 0) {
|
|
503
|
+
shuffle(hasPrefix, Math.random());
|
|
504
|
+
sort(hasPrefix, x => preferredNodeIds.has(x.nodeId) ? -1 : 1);
|
|
505
|
+
|
|
506
|
+
let missingRanges: { start: number; end: number }[] = [{
|
|
507
|
+
start: 0,
|
|
508
|
+
end: 1,
|
|
509
|
+
}];
|
|
510
|
+
let usedParts: {
|
|
511
|
+
nodeId: string;
|
|
512
|
+
authoritySpec: AuthoritySpec;
|
|
513
|
+
range: { start: number; end: number };
|
|
514
|
+
}[] = [];
|
|
515
|
+
for (let source of hasPrefix) {
|
|
516
|
+
let s = source.routeStart;
|
|
517
|
+
let e = source.routeEnd;
|
|
518
|
+
let { removedRanges } = removeRange(missingRanges, { start: s, end: e });
|
|
519
|
+
// NOTE: It's not ideal that we might have to fragment one node ID between multiple requests. However, in practice, there shouldn't be much fragmentation here. The ranges that our nodes are breaking down by should be consistent, so there's actually no overlap or subsets.
|
|
520
|
+
for (let removedRange of removedRanges) {
|
|
521
|
+
usedParts.push({
|
|
522
|
+
nodeId: source.nodeId,
|
|
523
|
+
range: removedRange,
|
|
524
|
+
authoritySpec: source,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
if (missingRanges.length === 0) break;
|
|
528
|
+
}
|
|
529
|
+
if (missingRanges.length === 0) {
|
|
530
|
+
return { nodes: usedParts };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let nestedMatches = allSources.filter(x => {
|
|
535
|
+
// There's nested prefixes, so if we match any prefix explicitly, we can't just take one of the previous prefixes because that isn't how the hashing will work.
|
|
536
|
+
// - This happens if it's a direct match, but one of the shards is down, in which case we can't get a full match.
|
|
537
|
+
if (x.authoritySpec.prefixes.some(y => y === path)) return false;
|
|
538
|
+
|
|
539
|
+
// If our path, which we're going to read the children of, is the child of another path, then it means in that other path, the child key will be known to us constant, and so we're going to match exactly one authority.
|
|
540
|
+
return (
|
|
541
|
+
x.authoritySpec.prefixes.some(y => path.startsWith(y) && y !== path)
|
|
542
|
+
&& this.matchesAuthoritySpec(x.authoritySpec, path)
|
|
543
|
+
);
|
|
544
|
+
});
|
|
446
545
|
if (nestedMatches.length > 0) {
|
|
447
546
|
shuffle(nestedMatches, Math.random());
|
|
448
|
-
let preferredNodeIds = new Set(config?.preferredNodeIds ?? []);
|
|
449
547
|
sort(nestedMatches, x => preferredNodeIds.has(x.nodeId) ? -1 : 1);
|
|
548
|
+
sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
|
|
450
549
|
return {
|
|
451
|
-
|
|
550
|
+
// 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.
|
|
551
|
+
nodes: nestedMatches.slice(0, 1).map(x => ({
|
|
452
552
|
nodeId: x.nodeId,
|
|
453
|
-
|
|
553
|
+
authoritySpec: x.authoritySpec,
|
|
454
554
|
range: { start: 0, end: 1 },
|
|
455
555
|
})),
|
|
456
556
|
};
|
|
457
557
|
}
|
|
458
558
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
prefixes: [path],
|
|
463
|
-
routeStart: 0,
|
|
464
|
-
routeEnd: 1,
|
|
465
|
-
excludeDefault: true,
|
|
466
|
-
},
|
|
467
|
-
preferredNodeIds: config?.preferredNodeIds,
|
|
559
|
+
// If we are not under any prefixes of it, then it will be a full path hash
|
|
560
|
+
let fullPathMatches = allSources.filter(x => {
|
|
561
|
+
return !x.authoritySpec.prefixes.some(y => path.startsWith(y) && y !== path) && !x.authoritySpec.excludeDefault;
|
|
468
562
|
});
|
|
469
|
-
|
|
563
|
+
// 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.
|
|
564
|
+
if (fullPathMatches.length > 0) {
|
|
565
|
+
shuffle(fullPathMatches, Math.random());
|
|
566
|
+
sort(fullPathMatches, x => preferredNodeIds.has(x.nodeId) ? -1 : 1);
|
|
567
|
+
sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
|
|
568
|
+
let missingRanges: { start: number; end: number }[] = [{
|
|
569
|
+
start: 0,
|
|
570
|
+
end: 1,
|
|
571
|
+
}];
|
|
572
|
+
let usedParts: {
|
|
573
|
+
nodeId: string;
|
|
574
|
+
authoritySpec: AuthoritySpec;
|
|
575
|
+
range: { start: number; end: number };
|
|
576
|
+
}[] = [];
|
|
577
|
+
for (let source of fullPathMatches) {
|
|
578
|
+
let s = source.authoritySpec.routeStart;
|
|
579
|
+
let e = source.authoritySpec.routeEnd;
|
|
580
|
+
let { removedRanges } = removeRange(missingRanges, { start: s, end: e });
|
|
581
|
+
// NOTE: It's not ideal that we might have to fragment one node ID between multiple requests. However, in practice, there shouldn't be much fragmentation here. The ranges that our nodes are breaking down by should be consistent, so there's actually no overlap or subsets.
|
|
582
|
+
for (let removedRange of removedRanges) {
|
|
583
|
+
usedParts.push({
|
|
584
|
+
nodeId: source.nodeId,
|
|
585
|
+
authoritySpec: source.authoritySpec,
|
|
586
|
+
range: removedRange,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
if (missingRanges.length === 0) break;
|
|
590
|
+
}
|
|
591
|
+
if (missingRanges.length === 0) {
|
|
592
|
+
return { nodes: usedParts };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
470
595
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
596
|
+
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
if (!config?.onlyOwnNodes) {
|
|
600
|
+
// TODO: We could maybe match a partial match. However, even that is suspect. The site being partially broken is almost worse than it being completely broken. We should just get ALL the shards running again...
|
|
601
|
+
|
|
602
|
+
// 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.
|
|
603
|
+
console.error(`Want to sync a prefix which is not under an existing prefix, nor equal to a prefix. 1) The servers are down. 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 });
|
|
604
|
+
}
|
|
605
|
+
return { nodes: [] };
|
|
478
606
|
}
|
|
479
607
|
|
|
480
608
|
|
|
@@ -538,3 +666,5 @@ export class PathRouter {
|
|
|
538
666
|
|
|
539
667
|
}
|
|
540
668
|
|
|
669
|
+
|
|
670
|
+
(globalThis as any).PathRouter = PathRouter;
|
|
@@ -22,7 +22,7 @@ let keySpecialIdentifier = (
|
|
|
22
22
|
export function createRoutingOverrideKey(config: {
|
|
23
23
|
originalKey: string;
|
|
24
24
|
routeKey: string;
|
|
25
|
-
// This is
|
|
25
|
+
// 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.
|
|
26
26
|
remappedPrefix: string;
|
|
27
27
|
}) {
|
|
28
28
|
let { originalKey, routeKey, remappedPrefix } = config;
|
|
@@ -55,13 +55,15 @@ export function getEmptyAuthoritySpec(): AuthoritySpec {
|
|
|
55
55
|
routeStart: -1,
|
|
56
56
|
routeEnd: -1,
|
|
57
57
|
prefixes: [],
|
|
58
|
+
excludeDefault: true,
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
|
-
export function getAllAuthoritySpec(): AuthoritySpec {
|
|
61
|
+
export async function getAllAuthoritySpec(): Promise<AuthoritySpec> {
|
|
62
|
+
let prefixes = await getShardPrefixes();
|
|
61
63
|
return {
|
|
62
64
|
nodeId: "",
|
|
63
65
|
routeStart: 0,
|
|
64
66
|
routeEnd: 1,
|
|
65
|
-
prefixes:
|
|
67
|
+
prefixes: prefixes,
|
|
66
68
|
};
|
|
67
69
|
}
|