querysub 0.524.0 → 0.526.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/package.json +2 -2
- package/src/-c-identity/IdentityController.ts +2 -2
- package/src/-d-trust/NetworkTrust2.ts +2 -2
- package/src/-f-node-discovery/LatencyTracking.ts +26 -2
- package/src/-f-node-discovery/TrafficTracking.ts +25 -19
- package/src/-h-path-value-serialize/PathValueSerializer.ts +35 -18
- package/src/0-path-value-core/AuthorityLookup.ts +4 -2
- package/src/0-path-value-core/PathRouter.ts +38 -8
- package/src/0-path-value-core/PathValueCommitter.ts +2 -2
- package/src/0-path-value-core/PathValueController.ts +74 -54
- package/src/0-path-value-core/ValidStateComputer.ts +28 -8
- package/src/1-path-client/RemoteWatcher.ts +59 -2
- package/src/3-path-functions/PathFunctionRunner.ts +4 -1
- package/src/4-deploy/edgeBootstrap.ts +1 -1
- package/src/4-querysub/FunctionRunnerTracking.ts +73 -3
- package/src/4-querysub/QuerysubController.ts +0 -2
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +1 -1
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +169 -18
- package/src/diagnostics/pathAuditer.ts +77 -18
- package/src/diagnostics/periodic.ts +2 -1
- package/src/diagnostics/watchdog.ts +11 -0
- package/src/library-components/LatencyGraph.tsx +43 -19
- package/src/server.ts +7 -0
|
@@ -23,11 +23,18 @@ import { encodeCborx, decodeCborx } from "../misc/cloneHelpers";
|
|
|
23
23
|
import { debugGetAllCallFactories } from "socket-function/src/nodeCache";
|
|
24
24
|
import { delay } from "socket-function/src/batching";
|
|
25
25
|
import { isDefined } from "../misc";
|
|
26
|
+
import { measureBlock } from "socket-function/src/profiling/measure";
|
|
26
27
|
export { pathValueCommitter };
|
|
27
28
|
|
|
28
29
|
let pathValueSendCount = 0;
|
|
29
30
|
export function getPathValueSendCount() { return pathValueSendCount; }
|
|
30
31
|
|
|
32
|
+
// Debug hook, called with every batch of values sendData receives, before any filtering or ingest — so tests can observe exactly when values arrive, without worrying about values being ignored or batched downstream.
|
|
33
|
+
let debugOnSendData: ((values: PathValue[], sourceNodeId: string) => void) | undefined;
|
|
34
|
+
export function setDebugOnSendData(callback: typeof debugOnSendData) {
|
|
35
|
+
debugOnSendData = callback;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
// ONLY returns non-canGCValues that are valid
|
|
32
39
|
export type AuditSnapshotEntry = {
|
|
33
40
|
path: string;
|
|
@@ -53,7 +60,7 @@ export class PathValueControllerBase {
|
|
|
53
60
|
let { pathValues, nodeId } = config;
|
|
54
61
|
|
|
55
62
|
pathValueSendCount += pathValues.length;
|
|
56
|
-
let serializedValues = await pathValueSerializer.serialize(pathValues, { compress: Querysub.COMPRESS_NETWORK });
|
|
63
|
+
let serializedValues = await measureBlock(() => pathValueSerializer.serialize(pathValues, { compress: Querysub.COMPRESS_NETWORK }), "createValues|serialize");
|
|
57
64
|
if (isDebugLogEnabled()) {
|
|
58
65
|
for (let value of pathValues) {
|
|
59
66
|
auditLog("SEND CREATED VALUE", {
|
|
@@ -111,11 +118,11 @@ export class PathValueControllerBase {
|
|
|
111
118
|
let changes = config.pathValues;
|
|
112
119
|
let { nodeId, initialTriggers } = config;
|
|
113
120
|
pathValueSendCount += changes.length;
|
|
114
|
-
recordPathValuesSent(changes.length);
|
|
115
|
-
let buffers = await pathValueSerializer.serialize(changes, {
|
|
121
|
+
recordPathValuesSent({ count: changes.length, nodeId });
|
|
122
|
+
let buffers = await measureBlock(() => pathValueSerializer.serialize(changes, {
|
|
116
123
|
noLocks: !config.keepLocks,
|
|
117
124
|
compress: getCompressNetwork(),
|
|
118
|
-
});
|
|
125
|
+
}), "sendValues|serialize");
|
|
119
126
|
this.logSendValues({ nodeId, pathValues: changes, initialTriggers, reason: config.reason });
|
|
120
127
|
return await PathValueController.nodes[nodeId].sendData({
|
|
121
128
|
valueBuffers: buffers,
|
|
@@ -137,7 +144,7 @@ export class PathValueControllerBase {
|
|
|
137
144
|
|
|
138
145
|
}): Promise<void | "refused"> {
|
|
139
146
|
let callerId = SocketFunction.getCaller().nodeId;
|
|
140
|
-
|
|
147
|
+
const { initialCreation, valueBuffers } = config;
|
|
141
148
|
|
|
142
149
|
let slowdown = getSlowdown();
|
|
143
150
|
if (slowdown) {
|
|
@@ -146,28 +153,34 @@ export class PathValueControllerBase {
|
|
|
146
153
|
|
|
147
154
|
let values: PathValue[] = [];
|
|
148
155
|
if (valueBuffers) {
|
|
149
|
-
values = await pathValueSerializer.deserialize(valueBuffers);
|
|
156
|
+
values = await measureBlock(() => pathValueSerializer.deserialize(valueBuffers), "sendData|deserialize");
|
|
150
157
|
ActionsHistory.OnRead(values);
|
|
151
|
-
recordPathValuesReceived(values.length);
|
|
158
|
+
recordPathValuesReceived({ count: values.length, nodeId: callerId });
|
|
159
|
+
}
|
|
160
|
+
if (debugOnSendData) {
|
|
161
|
+
debugOnSendData(values, callerId);
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
if (initialCreation) {
|
|
155
|
-
let
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
let
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
let rejected = measureBlock(() => {
|
|
166
|
+
let sourceNodeId = debugNodeId(callerId);
|
|
167
|
+
let anyRejected = false;
|
|
168
|
+
let threshold = Date.now() - MAX_CHANGE_AGE;
|
|
169
|
+
for (let value of values) {
|
|
170
|
+
let pastThreshold = threshold - value.time.time;
|
|
171
|
+
if (pastThreshold > 0) {
|
|
172
|
+
anyRejected = true;
|
|
173
|
+
console.error(`Rejecting values as one is too old. It likely got caught in the pipeline too long and now can't be committed without trying to undone history which can already been committed to disk.`, {
|
|
174
|
+
path: value.path,
|
|
175
|
+
timeId: value.time.time,
|
|
176
|
+
sourceNodeId,
|
|
177
|
+
sourceNodeThreadId: decodeNodeId(sourceNodeId, getDomain())?.threadId,
|
|
178
|
+
totalValueCount: values.length,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
169
181
|
}
|
|
170
|
-
|
|
182
|
+
return anyRejected;
|
|
183
|
+
}, "sendData|ageCheck");
|
|
171
184
|
if (rejected) {
|
|
172
185
|
return "refused";
|
|
173
186
|
}
|
|
@@ -189,7 +202,7 @@ export class PathValueControllerBase {
|
|
|
189
202
|
}
|
|
190
203
|
|
|
191
204
|
// Note which values are genuinely new to us BEFORE ingesting. We only re-share new values with the other authorities below; if we already had a value we already shared it when we first received it, so re-sharing is redundant and can clobber a newer value with an older copy.
|
|
192
|
-
let valuesNewToUs = config.initialCreation && values.filter(value => !authorityStorage.getValueExactMaybeRejected(value.path, value.time)) || [];
|
|
205
|
+
let valuesNewToUs = measureBlock(() => config.initialCreation && values.filter(value => !authorityStorage.getValueExactMaybeRejected(value.path, value.time)) || [], "sendData|newToUsCheck");
|
|
193
206
|
|
|
194
207
|
try {
|
|
195
208
|
let initialTriggers = config.initialTriggers || { values: new Set(), parentPaths: new Set() };
|
|
@@ -205,7 +218,8 @@ export class PathValueControllerBase {
|
|
|
205
218
|
|
|
206
219
|
if (config.initialCreation && valuesNewToUs.length > 0) {
|
|
207
220
|
// Always shared the latest. If we don't have it, maybe something with ingestion delayed, we should still share it so we don't lose the value. If we haven't even ingested it, then it is our latest version of the value.
|
|
208
|
-
valuesNewToUs = valuesNewToUs.map(value => authorityStorage.getValueExactMaybeRejected(value.path, value.time) || value);
|
|
221
|
+
valuesNewToUs = measureBlock(() => valuesNewToUs.map(value => authorityStorage.getValueExactMaybeRejected(value.path, value.time) || value), "sendData|newToUsRemap");
|
|
222
|
+
// Not measured: this waits on network sends to the other authorities.
|
|
209
223
|
await PathValueControllerBase.authorityShareValues({ pathValues: valuesNewToUs });
|
|
210
224
|
}
|
|
211
225
|
}
|
|
@@ -242,13 +256,13 @@ export class PathValueControllerBase {
|
|
|
242
256
|
auditLog("WATCH PARENT PATH", { path: value, sourceNodeId });
|
|
243
257
|
}
|
|
244
258
|
}
|
|
245
|
-
pathWatcher.watchPath({
|
|
259
|
+
measureBlock(() => pathWatcher.watchPath({
|
|
246
260
|
nodeId: callerId,
|
|
247
261
|
paths: config.paths,
|
|
248
262
|
parentPaths: config.parentPaths,
|
|
249
263
|
initialTrigger: true,
|
|
250
264
|
fullHistory: config.fullHistory,
|
|
251
|
-
});
|
|
265
|
+
}), "watchLatest|watchPath");
|
|
252
266
|
}
|
|
253
267
|
public async unwatchLatest(config: WatchConfig) {
|
|
254
268
|
let callerId = SocketFunction.getCaller().nodeId;
|
|
@@ -261,7 +275,7 @@ export class PathValueControllerBase {
|
|
|
261
275
|
auditLog("UNWATCHING PARENT PATH", { path: value, sourceNodeId });
|
|
262
276
|
}
|
|
263
277
|
}
|
|
264
|
-
pathWatcher.unwatchPath({ paths: config.paths, parentPaths: config.parentPaths, callback: callerId, reason: "PathValueController.unwatchLatest" });
|
|
278
|
+
measureBlock(() => pathWatcher.unwatchPath({ paths: config.paths, parentPaths: config.parentPaths, callback: callerId, reason: "PathValueController.unwatchLatest" }), "unwatchLatest|unwatchPath");
|
|
265
279
|
}
|
|
266
280
|
|
|
267
281
|
public static async getInitialValues(config: {
|
|
@@ -281,11 +295,11 @@ export class PathValueControllerBase {
|
|
|
281
295
|
startTime: number;
|
|
282
296
|
endTime: number;
|
|
283
297
|
}): Promise<Buffer[]> {
|
|
284
|
-
let values = authorityStorage.getAllValues(config);
|
|
285
|
-
let buffers = await pathValueSerializer.serialize(values, {
|
|
298
|
+
let values = measureBlock(() => authorityStorage.getAllValues(config), "getInitialValues|getAllValues");
|
|
299
|
+
let buffers = await measureBlock(() => pathValueSerializer.serialize(values, {
|
|
286
300
|
noLocks: true,
|
|
287
301
|
compress: getCompressNetwork(),
|
|
288
|
-
});
|
|
302
|
+
}), "getInitialValues|serialize");
|
|
289
303
|
return buffers;
|
|
290
304
|
}
|
|
291
305
|
|
|
@@ -304,23 +318,25 @@ export class PathValueControllerBase {
|
|
|
304
318
|
let { spec } = config;
|
|
305
319
|
let { compareTime } = await import("./pathValueCore");
|
|
306
320
|
|
|
307
|
-
|
|
321
|
+
return measureBlock(() => {
|
|
322
|
+
let allValues = authorityStorage.getAllValues({ spec, startTime: 0, endTime: Number.MAX_SAFE_INTEGER });
|
|
308
323
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
324
|
+
let pathToLatest = new Map<string, Time>();
|
|
325
|
+
for (let value of allValues) {
|
|
326
|
+
if (!value.valid) continue;
|
|
327
|
+
if (value.canGCValue) continue;
|
|
313
328
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
329
|
+
let existing = pathToLatest.get(value.path);
|
|
330
|
+
if (!existing || compareTime(value.time, existing) > 0) {
|
|
331
|
+
pathToLatest.set(value.path, value.time);
|
|
332
|
+
}
|
|
317
333
|
}
|
|
318
|
-
}
|
|
319
334
|
|
|
320
|
-
|
|
335
|
+
let entries: AuditSnapshotEntry[] = Array.from(pathToLatest.entries()).map(([path, time]) => ({ path, time }));
|
|
321
336
|
|
|
322
|
-
|
|
323
|
-
|
|
337
|
+
let encoded = encodeCborx(entries);
|
|
338
|
+
return LZ4.compress(encoded);
|
|
339
|
+
}, "getAuditSnapshot|local");
|
|
324
340
|
}
|
|
325
341
|
|
|
326
342
|
|
|
@@ -333,19 +349,22 @@ export class PathValueControllerBase {
|
|
|
333
349
|
}
|
|
334
350
|
|
|
335
351
|
public async getValuesByPathAndTime(entries: AuditSnapshotEntry[]): Promise<Buffer[]> {
|
|
336
|
-
let values
|
|
337
|
-
|
|
338
|
-
let
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
352
|
+
let values = measureBlock(() => {
|
|
353
|
+
let matched: PathValue[] = [];
|
|
354
|
+
for (let entry of entries) {
|
|
355
|
+
let value = authorityStorage.getValueExactMaybeRejected(entry.path, entry.time);
|
|
356
|
+
if (!value) continue;
|
|
357
|
+
if (!value.valid) continue;
|
|
358
|
+
if (value.isTransparent) continue;
|
|
359
|
+
if (value.canGCValue) continue;
|
|
360
|
+
matched.push(value);
|
|
361
|
+
}
|
|
362
|
+
return matched;
|
|
363
|
+
}, "getValuesByPathAndTime|gather");
|
|
364
|
+
let buffers = await measureBlock(() => pathValueSerializer.serialize(values, {
|
|
346
365
|
noLocks: true,
|
|
347
366
|
compress: getCompressNetwork(),
|
|
348
|
-
});
|
|
367
|
+
}), "getValuesByPathAndTime|serialize");
|
|
349
368
|
return buffers;
|
|
350
369
|
}
|
|
351
370
|
}
|
|
@@ -367,6 +386,7 @@ export const PathValueController = SocketFunction.register(
|
|
|
367
386
|
hooks: [requiresNetworkTrustHook],
|
|
368
387
|
}),
|
|
369
388
|
{
|
|
370
|
-
|
|
389
|
+
// Most of these functions spend their time waiting on batching or remote calls, so auto-measuring them is misleading — instead the local work inside them is wrapped in individual measureBlocks.
|
|
390
|
+
noFunctionMeasure: true,
|
|
371
391
|
}
|
|
372
392
|
);
|
|
@@ -2,7 +2,7 @@ import { keyByArray, binarySearchIndex } from "socket-function/src/misc";
|
|
|
2
2
|
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
3
3
|
import { isNode } from "typesafecss";
|
|
4
4
|
import { isDiskAudit } from "../config";
|
|
5
|
-
import { getParentPathStr, getPathFromStr } from "../path";
|
|
5
|
+
import { getParentPathStr, getPathFromStr, getPathStr4 } from "../path";
|
|
6
6
|
import { auditLog } from "./auditLogs";
|
|
7
7
|
import { PathRouter } from "./PathRouter";
|
|
8
8
|
import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue, MAX_CHANGE_AGE, createMissingEpochValue, Time, MISSING_TRANSACTION_PART_TIMEOUT, isOurPrediction, DEFER_LOCK_WINDOW } from "./pathValueCore";
|
|
@@ -21,8 +21,9 @@ class ValidStateComputer {
|
|
|
21
21
|
parentSyncs: { parentPath: string; sourceNodeId: string }[];
|
|
22
22
|
initialTriggers: { values: Set<string>; parentPaths: Set<string> };
|
|
23
23
|
doNotArchive?: boolean;
|
|
24
|
+
forceValidStates?: boolean;
|
|
24
25
|
}) {
|
|
25
|
-
let { pathValues, parentSyncs, doNotArchive } = config;
|
|
26
|
+
let { pathValues, parentSyncs, doNotArchive, forceValidStates } = config;
|
|
26
27
|
let initialTriggers = { ...config.initialTriggers, initialTriggerNonHistoryWatchers: new Set<string>() };
|
|
27
28
|
|
|
28
29
|
// TODO: We might want to add back optimizations for "no watches and no locks"?
|
|
@@ -30,6 +31,13 @@ class ValidStateComputer {
|
|
|
30
31
|
|
|
31
32
|
let now = Date.now();
|
|
32
33
|
|
|
34
|
+
let forcedValidStates = new Map<string, boolean>();
|
|
35
|
+
if (forceValidStates) {
|
|
36
|
+
for (let value of pathValues) {
|
|
37
|
+
forcedValidStates.set(getForcedValidStateKey(value), !!value.valid);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
33
41
|
authorityStorage.addParentSyncs(parentSyncs);
|
|
34
42
|
|
|
35
43
|
// Dedup by (path, time): if multiple values for the same (path, time) arrive in this batch,
|
|
@@ -216,6 +224,7 @@ class ValidStateComputer {
|
|
|
216
224
|
Array.from(dependenciesChanged),
|
|
217
225
|
now,
|
|
218
226
|
initialPrevValidStates,
|
|
227
|
+
forcedValidStates,
|
|
219
228
|
);
|
|
220
229
|
let validStateChanged = result.changed;
|
|
221
230
|
for (let d of result.deferred) {
|
|
@@ -319,6 +328,7 @@ class ValidStateComputer {
|
|
|
319
328
|
valuePaths: PathValue[],
|
|
320
329
|
now: number,
|
|
321
330
|
prevValidStates: Map<PathValue, boolean | undefined>,
|
|
331
|
+
forcedValidStates?: Map<string, boolean>,
|
|
322
332
|
): { changed: PathValue[]; deferred: PathValue[] } {
|
|
323
333
|
let changed: PathValue[] = [];
|
|
324
334
|
let deferred: PathValue[] = [];
|
|
@@ -404,6 +414,11 @@ class ValidStateComputer {
|
|
|
404
414
|
|
|
405
415
|
for (let pathValue of valueGroup) {
|
|
406
416
|
let prevValidState = prevValidStates.get(pathValue);
|
|
417
|
+
let newValid: boolean = valid;
|
|
418
|
+
let forcedValid = forcedValidStates?.get(getForcedValidStateKey(pathValue));
|
|
419
|
+
if (forcedValid !== undefined) {
|
|
420
|
+
newValid = forcedValid;
|
|
421
|
+
}
|
|
407
422
|
/* We used to log these values until 2026 June 28th 6:30 am
|
|
408
423
|
if (valid && prevValidState === false) {
|
|
409
424
|
console.info(`Accepting value that was previously rejected`, {
|
|
@@ -428,31 +443,32 @@ class ValidStateComputer {
|
|
|
428
443
|
}
|
|
429
444
|
*/
|
|
430
445
|
// IMPORTANT! This means if it didn't previously exist and it's presently rejected, we count that as a change, as it this is going from undefined to false. This is actually very useful, as a lot of places want to know if a write is rejected, so they can display it in the UI for the developer.
|
|
431
|
-
if (
|
|
446
|
+
if (newValid === prevValidState && pathValue.valid === newValid) continue;
|
|
432
447
|
|
|
433
448
|
// NOTE: We might remove this logging later as it's pretty heavy, but right now we still have bugs somewhere here.
|
|
434
449
|
{
|
|
435
|
-
console.info(`Changed valid state ${JSON.stringify(prevValidState)} to ${JSON.stringify(
|
|
450
|
+
console.info(`Changed valid state ${JSON.stringify(prevValidState)} to ${JSON.stringify(newValid)} (and path is ${JSON.stringify(pathValue.valid)})`, {
|
|
436
451
|
path: pathValue.path,
|
|
437
452
|
timeId: pathValue.time.time,
|
|
438
453
|
timeIdFull: pathValue.time,
|
|
439
|
-
valid,
|
|
454
|
+
valid: newValid,
|
|
455
|
+
forced: forcedValid !== undefined,
|
|
440
456
|
prevValidState,
|
|
441
457
|
});
|
|
442
458
|
}
|
|
443
459
|
|
|
444
460
|
changed.push(pathValue);
|
|
445
461
|
|
|
446
|
-
// NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it.
|
|
462
|
+
// NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it. Anyone who sets the valid state anywhere else will be absolutely fired.
|
|
447
463
|
// @ts-expect-error
|
|
448
|
-
pathValue.valid =
|
|
464
|
+
pathValue.valid = newValid;
|
|
449
465
|
|
|
450
466
|
// HACK: There are times when we might be evaluating the same path value multiple times at once (ex, multiple initial syncs at once). In which case we might not equal the stored path value, so we need to forcefully update the valid state of it.
|
|
451
467
|
let storedPathValue = authorityStorage.getValueExactMaybeRejected(pathValue.path, pathValue.time);
|
|
452
468
|
// I think this is fine. I think it happens if we receive it multiple times due to sharding. It's a bit inefficient, but... SHOULD be fine.
|
|
453
469
|
if (storedPathValue && pathValue !== storedPathValue) {
|
|
454
470
|
// @ts-expect-error
|
|
455
|
-
storedPathValue.valid =
|
|
471
|
+
storedPathValue.valid = newValid;
|
|
456
472
|
}
|
|
457
473
|
}
|
|
458
474
|
}
|
|
@@ -541,3 +557,7 @@ class ValidStateComputer {
|
|
|
541
557
|
}
|
|
542
558
|
}
|
|
543
559
|
export const validStateComputer = new ValidStateComputer();
|
|
560
|
+
|
|
561
|
+
function getForcedValidStateKey(pathValue: { time: Time; path: string }): string {
|
|
562
|
+
return getPathStr4(pathValue.path, String(pathValue.time.time), String(pathValue.time.version), String(pathValue.time.creatorId));
|
|
563
|
+
}
|
|
@@ -6,7 +6,9 @@ import { isNode, sort, timeInMinute, timeInSecond } from "socket-function/src/mi
|
|
|
6
6
|
import { measureFnc, measureBlock } from "socket-function/src/profiling/measure";
|
|
7
7
|
import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
8
8
|
import { PathValueController } from "../0-path-value-core/PathValueController";
|
|
9
|
-
import { AuthoritySpec, PathRouter } from "../0-path-value-core/PathRouter";
|
|
9
|
+
import { AuthoritySpec, PathRouter, areAuthoritySpecsEquivalent } from "../0-path-value-core/PathRouter";
|
|
10
|
+
import { getNodeLatencyMedian } from "../-f-node-discovery/LatencyTracking";
|
|
11
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
10
12
|
import { WatchConfig, authorityStorage } from "../0-path-value-core/pathValueCore";
|
|
11
13
|
import { pathWatcher } from "../0-path-value-core/PathWatcher";
|
|
12
14
|
import { ActionsHistory } from "../diagnostics/ActionsHistory";
|
|
@@ -29,9 +31,15 @@ setImmediate(() => import("../4-querysub/Querysub"));
|
|
|
29
31
|
const STOP_KEYS_DOUBLE_SENDS = true;
|
|
30
32
|
|
|
31
33
|
// How close to an authority's scheduled shutdown we start treating it like a disconnect (rehoming all the paths we watch on it).
|
|
32
|
-
const SHUTDOWN_REHOME_WINDOW = timeInMinute *
|
|
34
|
+
const SHUTDOWN_REHOME_WINDOW = timeInMinute * 3;
|
|
33
35
|
const SHUTDOWN_REHOME_POLL_INTERVAL = timeInSecond * 30;
|
|
34
36
|
|
|
37
|
+
// Rehome the watches on an authority when an equivalent authority has this factor lower latency. At 2 the router's full-confidence cutoff factor (also 2) then reliably routes everything to the better node on the rewatch.
|
|
38
|
+
const LATENCY_REHOME_FACTOR = 2;
|
|
39
|
+
// Both latencies must be medians over this many samples — with fewer we don't trust either number enough to move watches over it.
|
|
40
|
+
const LATENCY_REHOME_HISTORY_COUNT = 10;
|
|
41
|
+
const LATENCY_REHOME_POLL_INTERVAL = timeInMinute;
|
|
42
|
+
|
|
35
43
|
// NOTE: If a parent watch is broken up between multiple nodes, we generally watch everything on all those nodes and then filter when we receive the data. This isn't efficient, and we should probably change it. However, in practice, it's probably fine, as it's unlikely for the watches to be broken up to a much finer granularity than the network (it would require very strange partially overlapping sharding cases which no reasonable sharding setup would ever satisfy).
|
|
36
44
|
export class RemoteWatcher {
|
|
37
45
|
public static DEBUG = false;
|
|
@@ -152,6 +160,40 @@ export class RemoteWatcher {
|
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
}
|
|
163
|
+
|
|
164
|
+
private latencyRehomeLoop = runInfinitePoll(LATENCY_REHOME_POLL_INTERVAL, () => this.rehomeHighLatencyAuthorities());
|
|
165
|
+
private rehomeHighLatencyAuthorities() {
|
|
166
|
+
let authorityIds = new Set<string>(this.remoteWatchPaths.values());
|
|
167
|
+
for (let watchObj of this.remoteWatchParents2.values()) {
|
|
168
|
+
for (let range of watchObj.ranges) {
|
|
169
|
+
authorityIds.add(range.authorityId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Also guards getTopologySync — if we watch anything remotely, the authority lookup has synced.
|
|
173
|
+
if (authorityIds.size === 0) return;
|
|
174
|
+
let topology = authorityLookup.getTopologySync();
|
|
175
|
+
for (let authorityId of authorityIds) {
|
|
176
|
+
if (isOwnNodeId(authorityId)) continue;
|
|
177
|
+
let entry = topology.find(candidate => candidate.nodeId === authorityId);
|
|
178
|
+
if (!entry) continue;
|
|
179
|
+
let current = getNodeLatencyMedian({ nodeId: authorityId, historyCount: LATENCY_REHOME_HISTORY_COUNT });
|
|
180
|
+
if (!current || current.historyUsed < LATENCY_REHOME_HISTORY_COUNT) continue;
|
|
181
|
+
for (let candidate of topology) {
|
|
182
|
+
if (candidate.nodeId === authorityId) continue;
|
|
183
|
+
if (isOwnNodeId(candidate.nodeId)) continue;
|
|
184
|
+
if (!candidate.isReady) continue;
|
|
185
|
+
if (authorityLookup.nodeIsShuttingDownSoon(candidate.nodeId)) continue;
|
|
186
|
+
if (!areAuthoritySpecsEquivalent(entry.authoritySpec, candidate.authoritySpec)) continue;
|
|
187
|
+
let candidateLatency = getNodeLatencyMedian({ nodeId: candidate.nodeId, historyCount: LATENCY_REHOME_HISTORY_COUNT });
|
|
188
|
+
if (!candidateLatency || candidateLatency.historyUsed < LATENCY_REHOME_HISTORY_COUNT) continue;
|
|
189
|
+
if (candidateLatency.latency * LATENCY_REHOME_FACTOR > current.latency) continue;
|
|
190
|
+
console.log(yellow(`Authority ${authorityId} has high latency (${formatTime(current.latency)}), and the equivalent authority ${candidate.nodeId} is much faster (${formatTime(candidateLatency.latency)}), so we are rehoming all paths watched on it`));
|
|
191
|
+
logErrors(this.refreshAllWatches(authorityId));
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
155
197
|
private async tryToReconnectNow() {
|
|
156
198
|
if (!this.disconnectedPaths.size && !this.disconnectedParents.size) return;
|
|
157
199
|
|
|
@@ -656,6 +698,19 @@ export class RemoteWatcher {
|
|
|
656
698
|
return Array.from(new Set(this.remoteWatchPaths.values()));
|
|
657
699
|
}
|
|
658
700
|
|
|
701
|
+
public getWatchedPathCountsPerNodeId(): Map<string, number> {
|
|
702
|
+
let counts = new Map<string, number>();
|
|
703
|
+
for (let nodeId of this.remoteWatchPaths.values()) {
|
|
704
|
+
counts.set(nodeId, (counts.get(nodeId) || 0) + 1);
|
|
705
|
+
}
|
|
706
|
+
for (let watchObj of this.remoteWatchParents2.values()) {
|
|
707
|
+
for (let range of watchObj.ranges) {
|
|
708
|
+
counts.set(range.authorityId, (counts.get(range.authorityId) || 0) + 1);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return counts;
|
|
712
|
+
}
|
|
713
|
+
|
|
659
714
|
|
|
660
715
|
|
|
661
716
|
public async refreshAllWatches(authorityNodeId: string) {
|
|
@@ -696,6 +751,8 @@ export class RemoteWatcher {
|
|
|
696
751
|
}
|
|
697
752
|
let paths = Array.from(pathsToWatch);
|
|
698
753
|
|
|
754
|
+
console.log(yellow(`Refreshing all watches on ${authorityNodeId}: ${pathsToWatch.size} paths, ${parentPathsToRewatch.size} parent paths (${parentRemotePathsToUnwatch.size} remote parent ranges)`));
|
|
755
|
+
|
|
699
756
|
logErrors(RemoteWatcher.REMOTE_UNWATCH_FUNCTION({
|
|
700
757
|
paths,
|
|
701
758
|
parentPaths: Array.from(parentRemotePathsToUnwatch)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { batchFunction, delay, runInfinitePoll } from "socket-function/src/batching";
|
|
2
|
-
import { recordFunctionExecuted } from "../-f-node-discovery/TrafficTracking";
|
|
2
|
+
import { recordFunctionExecuted, recordQuerysubCall } from "../-f-node-discovery/TrafficTracking";
|
|
3
3
|
import { cache, lazy } from "socket-function/src/caching";
|
|
4
4
|
import { blue, magenta, yellow } from "socket-function/src/formatting/logColors";
|
|
5
5
|
import { timeInHour, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
@@ -56,6 +56,9 @@ setImmediate(() => {
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
export function commitCall(call: CallSpec) {
|
|
59
|
+
// Counted here (not in QuerysubController.addCall) so trusted nodes' direct writes are counted too — both the
|
|
60
|
+
// remote addCall path and the direct-write path funnel through here.
|
|
61
|
+
recordQuerysubCall();
|
|
59
62
|
if (!call.network) {
|
|
60
63
|
throw new Error(`Call has no network, so it would never run (no FunctionRunner would pick it up). Call: ${debugCallSpec(call)}`);
|
|
61
64
|
}
|
|
@@ -357,7 +357,7 @@ async function edgeNodeFunction(config: {
|
|
|
357
357
|
// Probes ALL the given nodes (even private / non-live ones), writing latencies to globalThis.EDGE_NODE_STATS (so the edge node dropdown can show them), and returns a latency-weighted random pick among `pickableHosts` (undefined if none respond).
|
|
358
358
|
async function probeAndPick(probeNodes: EdgeNodeConfig[], pickableHosts: Set<string>): Promise<EdgeNodeConfig | undefined> {
|
|
359
359
|
// Having this much lower latency than another node means we pick it 100% of the time (the weight scales linearly, from 1 at the best latency down to 0 at the best latency + this)
|
|
360
|
-
const LATENCY_WEIGHT_WINDOW =
|
|
360
|
+
const LATENCY_WEIGHT_WINDOW = 300;
|
|
361
361
|
// How many probes are in flight at once. The first batch fires simultaneously, so anything that responds more than LATENCY_WEIGHT_WINDOW after the first response has a latency at least that much higher than it, and so would never have been picked anyway.
|
|
362
362
|
const PROBE_BATCH_SIZE = 5;
|
|
363
363
|
|
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
8
8
|
import { timeInMinute, timeInSecond, sort } from "socket-function/src/misc";
|
|
9
9
|
import { lazy } from "socket-function/src/caching";
|
|
10
|
-
import { delay, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
10
|
+
import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
11
11
|
import { isClient } from "../config2";
|
|
12
12
|
import { t } from "../2-proxy/schema2";
|
|
13
13
|
import { createLocalSchema } from "./schemaHelpers";
|
|
14
14
|
import { Querysub } from "./Querysub";
|
|
15
15
|
import { getAllNodeIds, getOwnNodeId, watchDeltaNodeIds } from "../-f-node-discovery/NodeDiscovery";
|
|
16
|
-
import { getNodeLatencyInfo } from "../-f-node-discovery/LatencyTracking";
|
|
16
|
+
import { getNodeLatencyInfo, getNodeLatencyMedian } from "../-f-node-discovery/LatencyTracking";
|
|
17
17
|
import { FunctionRunnerInfoController, FunctionStatsSummary } from "../3-path-functions/PathFunctionRunner";
|
|
18
18
|
import { URLParam } from "../library-components/URLParam";
|
|
19
19
|
import { logErrors, timeoutToUndefined, timeoutToUndefinedSilent } from "../errors";
|
|
@@ -37,6 +37,12 @@ const CALL_TIME_WEIGHT_BASE = 1000;
|
|
|
37
37
|
// If every runner on a network will be shut down within this window, the network is dying and we switch off of it.
|
|
38
38
|
const NETWORK_DYING_WINDOW = timeInMinute * 2;
|
|
39
39
|
|
|
40
|
+
// Once selected we stick with a network — only switching when another network's best node has this factor lower latency than our network's best node (comparing medians, both with full history, so we never switch on noisy data).
|
|
41
|
+
const NETWORK_SWITCH_LATENCY_FACTOR = 2;
|
|
42
|
+
const NETWORK_SWITCH_HISTORY_COUNT = 10;
|
|
43
|
+
// The latency data is already collected by LatencyTracking, so checking is cheap and can run often.
|
|
44
|
+
const NETWORK_SWITCH_POLL_INTERVAL = timeInMinute;
|
|
45
|
+
|
|
40
46
|
// The forced network is a URL parameter, so it survives reloads and is shareable / obvious in the URL.
|
|
41
47
|
export const forcedNetworkURL = new URLParam("network", "");
|
|
42
48
|
|
|
@@ -306,11 +312,75 @@ export function getAutoSelectedNetwork(): string | undefined {
|
|
|
306
312
|
return candidates[0]?.network;
|
|
307
313
|
}
|
|
308
314
|
|
|
315
|
+
// The lowest full-history median latency among the network's runners (the best node is what we compare networks by).
|
|
316
|
+
function getNetworkBestNodeLatency(network: string): number | undefined {
|
|
317
|
+
let index = getFunctionRunnerIndex();
|
|
318
|
+
if (!index) return undefined;
|
|
319
|
+
let best: number | undefined;
|
|
320
|
+
for (let node of index.nodes) {
|
|
321
|
+
if (!node.networks.includes(network)) continue;
|
|
322
|
+
let median = getNodeLatencyMedian({ nodeId: node.nodeId, historyCount: NETWORK_SWITCH_HISTORY_COUNT });
|
|
323
|
+
if (!median || median.historyUsed < NETWORK_SWITCH_HISTORY_COUNT) continue;
|
|
324
|
+
if (best === undefined || median.latency < best) {
|
|
325
|
+
best = median.latency;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return best;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let stickyNetwork: string | undefined;
|
|
332
|
+
function checkNetworkSwitch() {
|
|
333
|
+
if (!stickyNetwork) return;
|
|
334
|
+
let currentLatency = getNetworkBestNodeLatency(stickyNetwork);
|
|
335
|
+
if (currentLatency === undefined) return;
|
|
336
|
+
|
|
337
|
+
let candidates = getNetworkSelectionInfos().filter(x => !x.dying && x.network !== stickyNetwork);
|
|
338
|
+
if (isPublic()) {
|
|
339
|
+
candidates = candidates.filter(x => x.isPublic);
|
|
340
|
+
}
|
|
341
|
+
let upLongEnough = candidates.filter(x => x.upLongEnough);
|
|
342
|
+
if (upLongEnough.length > 0) {
|
|
343
|
+
candidates = upLongEnough;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
let bestNetwork: string | undefined;
|
|
347
|
+
let bestLatency: number | undefined;
|
|
348
|
+
for (let candidate of candidates) {
|
|
349
|
+
let latency = getNetworkBestNodeLatency(candidate.network);
|
|
350
|
+
if (latency === undefined) continue;
|
|
351
|
+
if (bestLatency === undefined || latency < bestLatency) {
|
|
352
|
+
bestLatency = latency;
|
|
353
|
+
bestNetwork = candidate.network;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (bestNetwork === undefined || bestLatency === undefined) return;
|
|
357
|
+
if (bestLatency * NETWORK_SWITCH_LATENCY_FACTOR > currentLatency) return;
|
|
358
|
+
|
|
359
|
+
console.log(yellow(`Switching the selected function network from ${stickyNetwork} (best node latency ${formatTime(currentLatency)}) to ${bestNetwork} (best node latency ${formatTime(bestLatency)}), so all new function calls use the faster network`));
|
|
360
|
+
stickyNetwork = bestNetwork;
|
|
361
|
+
}
|
|
362
|
+
const startNetworkSwitchPoll = lazy(() => {
|
|
363
|
+
logErrors(runInfinitePoll(NETWORK_SWITCH_POLL_INTERVAL, checkNetworkSwitch));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Sticky: the score-based pick only initializes (or replaces a dead/dying selection); after that only checkNetworkSwitch changes it, so calls don't bounce between networks on small score changes.
|
|
367
|
+
function getStickyNetwork(): string | undefined {
|
|
368
|
+
void startNetworkSwitchPoll();
|
|
369
|
+
if (stickyNetwork) {
|
|
370
|
+
let info = getNetworkSelectionInfos().find(x => x.network === stickyNetwork);
|
|
371
|
+
if (info && !info.dying && (!isPublic() || info.isPublic)) {
|
|
372
|
+
return stickyNetwork;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
stickyNetwork = getAutoSelectedNetwork();
|
|
376
|
+
return stickyNetwork;
|
|
377
|
+
}
|
|
378
|
+
|
|
309
379
|
/** Safe to call from any context. Returns the network new function calls should be put on. Throws if no network is available (calls without a network can never run), unless noThrow is set. */
|
|
310
380
|
export function getSelectedNetwork(): string;
|
|
311
381
|
export function getSelectedNetwork(config: { noThrow: boolean }): string | undefined;
|
|
312
382
|
export function getSelectedNetwork(config?: { noThrow?: boolean }): string | undefined {
|
|
313
|
-
let network = forcedNetworkURL.value ||
|
|
383
|
+
let network = forcedNetworkURL.value || getStickyNetwork();
|
|
314
384
|
if (!network && !config?.noThrow) {
|
|
315
385
|
throw new Error(`No function runner discovered during discovery, so this call cannot be given a network and would never run (we keep polling for function runners, every ${formatTime(POLL_INTERVAL)} when tracking, every ${formatTime(INDEX_POLL_INTERVAL)} for the client index)`);
|
|
316
386
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
2
|
import { cache, lazy } from "socket-function/src/caching";
|
|
3
|
-
import { recordQuerysubCall } from "../-f-node-discovery/TrafficTracking";
|
|
4
3
|
import { appendToPathStr, getPathDepth, getPathStr1, getPathStr3 } from "../path";
|
|
5
4
|
import { FunctionMetadata } from "../3-path-functions/syncSchema";
|
|
6
5
|
import { RemoteWatcher, remoteWatcher } from "../1-path-client/RemoteWatcher";
|
|
@@ -535,7 +534,6 @@ export class QuerysubControllerBase {
|
|
|
535
534
|
|
|
536
535
|
// NOTE: Calls are going to be temporary and random. Any user can use any call ID, so technically you could clobber other users' call IDs, or your our. There wouldn't really be any benefit. Nothing would really happen if you do that, so I don't believe these need to be kept secret. I think if you know someone else's call ID you might be able to read that data, but also it's securely random, so you're not going to be able to guess the call ID.
|
|
537
536
|
public async addCall(call: CallSpec) {
|
|
538
|
-
recordQuerysubCall();
|
|
539
537
|
if (isBootstrapOnly()) throw new Error(`Cannot add calls to bootstrap only server`);
|
|
540
538
|
if (Querysub.DEBUG_CALLS) {
|
|
541
539
|
console.log(`[Querysub] addCall @${debugTime(call.runAtTime)}: ${call.DomainName}.${call.ModuleId}.${call.FunctionId}`);
|
|
@@ -182,11 +182,11 @@ export class IndexedLogs<T> {
|
|
|
182
182
|
};
|
|
183
183
|
let writeBuffers = async (buffers: Buffer[]) => {
|
|
184
184
|
if (Date.now() > endTime) {
|
|
185
|
+
await newStreamer();
|
|
185
186
|
let timeBlockObj = this.getTimeBlock(Date.now());
|
|
186
187
|
startTime = timeBlockObj.startTime;
|
|
187
188
|
endTime = timeBlockObj.endTime;
|
|
188
189
|
path = new TimeFileTree(this.getLocalLogs()).getNewPendingPath(timeBlockObj);
|
|
189
|
-
await newStreamer();
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
let maxSize = this.config.maxSingleFileData || MAX_SINGLE_FILE_DATA;
|