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
|
@@ -20,6 +20,7 @@ import { getFunctionRunnerIndex, FunctionRunnerNodeInfo } from "../../4-querysub
|
|
|
20
20
|
import { formatTime, formatNumber } from "socket-function/src/formatting/format";
|
|
21
21
|
import { LatencyGraph, LatencyGraphNode, LatencyGraphLink, LatencyGraphLabelLine } from "../../library-components/LatencyGraph";
|
|
22
22
|
import { URLParam } from "../../library-components/URLParam";
|
|
23
|
+
import { mainResets } from "../../library-components/urlResetGroups";
|
|
23
24
|
|
|
24
25
|
const ID_CHARS = 8;
|
|
25
26
|
// Green means querysub, blue means path value, purple means function runner.
|
|
@@ -31,10 +32,11 @@ const PROBE_TIMEOUT_MS = 5000;
|
|
|
31
32
|
const RANGE_BAR_WIDTH_PX = 360;
|
|
32
33
|
const RANGE_BAR_HEIGHT_PX = 14;
|
|
33
34
|
const LATENCY_GRAPH_HEIGHT_PX = 720;
|
|
35
|
+
const MACHINE_LATENCY_WIDTH_PX = 235;
|
|
34
36
|
|
|
35
37
|
// Clicking a machine node in the graph (or a row) sorts that machine's nodes to the top of the table.
|
|
36
|
-
const selectedMachineParam = new URLParam("rtSelectedMachine", "");
|
|
37
|
-
const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute * 60);
|
|
38
|
+
const selectedMachineParam = new URLParam("rtSelectedMachine", "", { reset: [mainResets] });
|
|
39
|
+
const trafficWindowParam = new URLParam<number>("rtTrafficWindow", timeInMinute * 60, { reset: [mainResets] });
|
|
38
40
|
|
|
39
41
|
// The per-node table columns, in order. `color` tints the function/path-value/querysub columns to match the graph;
|
|
40
42
|
// `perMachine` columns (the IP and machine id) only print on the first row of each machine group.
|
|
@@ -227,6 +229,43 @@ function machineIdOf(nodeId: string): string {
|
|
|
227
229
|
return parts?.machineId || nodeId;
|
|
228
230
|
}
|
|
229
231
|
|
|
232
|
+
function threadLabel(nodeId: string): string {
|
|
233
|
+
let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
|
|
234
|
+
return `${(parts?.threadId || "?").slice(0, ID_CHARS)}:${parts?.port ?? "?"}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// min · median · max of one source node's latencies to a machine's threads (plus a tooltip naming the farthest
|
|
238
|
+
// thread), and the node's total path values sent/received to that machine (summed — the per-thread split isn't interesting).
|
|
239
|
+
function machineLatencyCell(config: {
|
|
240
|
+
sourceLatencies: { [nodeId: string]: number } | undefined;
|
|
241
|
+
threads: string[];
|
|
242
|
+
traffic: TrafficStats | undefined;
|
|
243
|
+
}): { text: string; tooltip: string } {
|
|
244
|
+
let { sourceLatencies, threads, traffic } = config;
|
|
245
|
+
if (!sourceLatencies) return { text: "", tooltip: "" };
|
|
246
|
+
let entries: { thread: string; ms: number }[] = [];
|
|
247
|
+
for (let thread of threads) {
|
|
248
|
+
let ms = sourceLatencies[thread];
|
|
249
|
+
if (Number.isFinite(ms)) entries.push({ thread, ms });
|
|
250
|
+
}
|
|
251
|
+
if (!entries.length) return { text: "", tooltip: "" };
|
|
252
|
+
sort(entries, entry => entry.ms);
|
|
253
|
+
let farthest = entries[entries.length - 1];
|
|
254
|
+
let text = `${formatTime(entries[0].ms)} · ${formatTime(entries[Math.floor(entries.length / 2)].ms)} · ${formatTime(farthest.ms)}`;
|
|
255
|
+
let pvSent = 0;
|
|
256
|
+
let pvReceived = 0;
|
|
257
|
+
for (let thread of threads) {
|
|
258
|
+
let data = traffic?.perNode?.[thread];
|
|
259
|
+
if (!data) continue;
|
|
260
|
+
pvSent += data.pathValuesSent || 0;
|
|
261
|
+
pvReceived += data.pathValuesReceived || 0;
|
|
262
|
+
}
|
|
263
|
+
if (pvSent || pvReceived) {
|
|
264
|
+
text += ` ↑${formatNumber(pvSent)}/s ↓${formatNumber(pvReceived)}/s values`;
|
|
265
|
+
}
|
|
266
|
+
return { text, tooltip: `${text}\nfarthest: ${threadLabel(farthest.thread)} (${formatTime(farthest.ms)})` };
|
|
267
|
+
}
|
|
268
|
+
|
|
230
269
|
// Summed traffic metrics across a set of nodes (a machine's threads).
|
|
231
270
|
function sumTraffic(threads: string[], trafficMaps: Map<string, TrafficStats>) {
|
|
232
271
|
let dataSent = 0;
|
|
@@ -313,7 +352,7 @@ function buildNodeRow(config: {
|
|
|
313
352
|
if (runner) {
|
|
314
353
|
if (runner.networks.length) fnParts.push(runner.networks.join(" "));
|
|
315
354
|
for (let shard of runner.shards) {
|
|
316
|
-
fnParts.push(
|
|
355
|
+
fnParts.push(`${shard.shardRange.startFraction.toFixed(3)}-${shard.shardRange.endFraction.toFixed(3)}`);
|
|
317
356
|
}
|
|
318
357
|
fnParts.push(`${formatNumber(traffic?.functionsExecuted ?? 0)}/s calls`);
|
|
319
358
|
if (!runner.isPublic) fnParts.push("PRIVATE");
|
|
@@ -322,7 +361,7 @@ function buildNodeRow(config: {
|
|
|
322
361
|
let pvParts: string[] = [];
|
|
323
362
|
let spec = info?.spec;
|
|
324
363
|
if (spec && spec.routeStart >= 0 && spec.routeEnd >= 0) {
|
|
325
|
-
pvParts.push(
|
|
364
|
+
pvParts.push(`${spec.routeStart.toFixed(3)}-${spec.routeEnd.toFixed(3)}`);
|
|
326
365
|
}
|
|
327
366
|
if (traffic && (traffic.pathValuesSent || traffic.pathValuesReceived)) {
|
|
328
367
|
pvParts.push(`↑${formatNumber(traffic.pathValuesSent)}/s ↓${formatNumber(traffic.pathValuesReceived)}/s values`);
|
|
@@ -344,19 +383,30 @@ function buildNodeRow(config: {
|
|
|
344
383
|
};
|
|
345
384
|
}
|
|
346
385
|
|
|
347
|
-
class NodeInfoTable extends qreact.Component<{
|
|
386
|
+
class NodeInfoTable extends qreact.Component<{
|
|
387
|
+
rows: NodeRow[];
|
|
388
|
+
selectedMachine: string;
|
|
389
|
+
machineColumns: { id: string; label: string }[];
|
|
390
|
+
nodeMachineLatency: Map<string, { [machineId: string]: { text: string; tooltip: string } }>;
|
|
391
|
+
}> {
|
|
348
392
|
render() {
|
|
349
393
|
let rows = this.props.rows;
|
|
350
394
|
let selected = this.props.selectedMachine;
|
|
395
|
+
let machineColumns = this.props.machineColumns;
|
|
396
|
+
let nodeMachineLatency = this.props.nodeMachineLatency;
|
|
351
397
|
return <div className={css.vbox(0).fillWidth.overflowAuto.bord2(0, 0, 85)}>
|
|
352
398
|
<div className={css.hbox(0).hsl(0, 0, 96).colorhsl(0, 0, 20).boldStyle}>
|
|
353
399
|
{NODE_TABLE_COLUMNS.map(col =>
|
|
354
400
|
<div className={css.width(col.width).flexShrink0.pad2(6).ellipsis}>{col.label}</div>
|
|
355
401
|
)}
|
|
402
|
+
{machineColumns.map(col =>
|
|
403
|
+
<div className={css.width(MACHINE_LATENCY_WIDTH_PX).flexShrink0.pad2(6).ellipsis} title={`latency and path values to ${col.label}`}>→ {col.label}</div>
|
|
404
|
+
)}
|
|
356
405
|
</div>
|
|
357
406
|
{rows.map((row, i) => {
|
|
358
407
|
let firstOfMachine = i === 0 || rows[i - 1].machineId !== row.machineId;
|
|
359
408
|
let isSelected = row.machineId === selected;
|
|
409
|
+
let latencies = nodeMachineLatency.get(row.nodeId) || {};
|
|
360
410
|
return <div
|
|
361
411
|
className={css.hbox(0).button.fillWidth.hsl(0, 0, isSelected ? 92 : 99)
|
|
362
412
|
.borderTop(firstOfMachine ? "1px solid hsl(0, 0%, 80%)" : "1px solid hsl(0, 0%, 93%)")}
|
|
@@ -370,6 +420,42 @@ class NodeInfoTable extends qreact.Component<{ rows: NodeRow[]; selectedMachine:
|
|
|
370
420
|
title={text}
|
|
371
421
|
>{text}</div>;
|
|
372
422
|
})}
|
|
423
|
+
{machineColumns.map(col => {
|
|
424
|
+
let cell = latencies[col.id];
|
|
425
|
+
return <div
|
|
426
|
+
className={css.width(MACHINE_LATENCY_WIDTH_PX).flexShrink0.pad2(6).ellipsis.colorhsl(0, 0, 25)}
|
|
427
|
+
title={cell?.tooltip || ""}
|
|
428
|
+
>{cell?.text || ""}</div>;
|
|
429
|
+
})}
|
|
430
|
+
</div>;
|
|
431
|
+
})}
|
|
432
|
+
</div>;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Nodes that never reported their latencies — all we have is their nodeId (and possibly a DNS-resolved machine IP), so this is a much narrower table than NodeInfoTable, still grouped by machine.
|
|
437
|
+
class UnresponsiveNodesTable extends qreact.Component<{ nodeIds: string[]; machineIp: Map<string, string> }> {
|
|
438
|
+
render() {
|
|
439
|
+
let rows = this.props.nodeIds.map(nodeId => ({ nodeId, machineId: machineIdOf(nodeId) }));
|
|
440
|
+
let sep = String.fromCharCode(1);
|
|
441
|
+
sort(rows, row => `${row.machineId}${sep}${threadLabel(row.nodeId)}`);
|
|
442
|
+
return <div className={css.vbox(0).fillWidth.overflowAuto.bord2(0, 0, 85)}>
|
|
443
|
+
<div className={css.hbox(0).hsl(0, 0, 96).colorhsl(0, 0, 20).boldStyle}>
|
|
444
|
+
<div className={css.width(130).flexShrink0.pad2(6).ellipsis}>IP</div>
|
|
445
|
+
<div className={css.width(96).flexShrink0.pad2(6).ellipsis}>Machine</div>
|
|
446
|
+
<div className={css.width(130).flexShrink0.pad2(6).ellipsis}>Thread:Port</div>
|
|
447
|
+
<div className={css.width(520).flexShrink0.pad2(6).ellipsis}>Node Id</div>
|
|
448
|
+
</div>
|
|
449
|
+
{rows.map((row, i) => {
|
|
450
|
+
let firstOfMachine = i === 0 || rows[i - 1].machineId !== row.machineId;
|
|
451
|
+
return <div
|
|
452
|
+
className={css.hbox(0).fillWidth.hsl(0, 0, 99)
|
|
453
|
+
.borderTop(firstOfMachine ? "1px solid hsl(0, 0%, 80%)" : "1px solid hsl(0, 0%, 93%)")}
|
|
454
|
+
>
|
|
455
|
+
<div className={css.width(130).flexShrink0.pad2(6).ellipsis}>{firstOfMachine && this.props.machineIp.get(row.machineId) || ""}</div>
|
|
456
|
+
<div className={css.width(96).flexShrink0.pad2(6).ellipsis}>{firstOfMachine && row.machineId.slice(0, ID_CHARS) || ""}</div>
|
|
457
|
+
<div className={css.width(130).flexShrink0.pad2(6).ellipsis}>{threadLabel(row.nodeId)}</div>
|
|
458
|
+
<div className={css.width(520).flexShrink0.pad2(6).ellipsis.colorhsl(0, 0, 45)} title={row.nodeId}>{row.nodeId}</div>
|
|
373
459
|
</div>;
|
|
374
460
|
})}
|
|
375
461
|
</div>;
|
|
@@ -453,19 +539,56 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
453
539
|
if (unique.size) machineNetworks.set(machineId, [...unique]);
|
|
454
540
|
}
|
|
455
541
|
|
|
542
|
+
// One latency column per machine: each row (node) gets its own min · median · max latency to that machine's
|
|
543
|
+
// threads (a machine has several threads, so a cell is a range, not one number).
|
|
544
|
+
let machineColumns = [...machineAllThreads.keys()].map(id => {
|
|
545
|
+
let networks = machineNetworks.get(id) || [];
|
|
546
|
+
let label = networks.length ? `${id.slice(0, ID_CHARS)} (${networks.join(" | ")})` : id.slice(0, ID_CHARS);
|
|
547
|
+
return { id, label, threads: machineAllThreads.get(id) || [] };
|
|
548
|
+
});
|
|
549
|
+
sort(machineColumns, column => column.id);
|
|
550
|
+
let nodeMachineLatency = new Map<string, { [machineId: string]: { text: string; tooltip: string } }>();
|
|
551
|
+
for (let nodeId of nodeIds) {
|
|
552
|
+
let sourceLatencies = latencyMaps.get(nodeId);
|
|
553
|
+
let traffic = trafficMaps.get(nodeId);
|
|
554
|
+
let byMachine: { [machineId: string]: { text: string; tooltip: string } } = {};
|
|
555
|
+
for (let column of machineColumns) {
|
|
556
|
+
let cell = machineLatencyCell({ sourceLatencies, threads: column.threads, traffic });
|
|
557
|
+
if (cell.text) byMachine[column.id] = cell;
|
|
558
|
+
}
|
|
559
|
+
nodeMachineLatency.set(nodeId, byMachine);
|
|
560
|
+
}
|
|
561
|
+
|
|
456
562
|
// Total bytes on each thread-pair link (both directions). Both endpoints report the same total, so take the max.
|
|
457
563
|
let pairKey = (a: string, b: string) => a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
458
564
|
let pairTraffic = new Map<string, number>();
|
|
565
|
+
// Path values per thread pair, split by direction (fwd = lower id → higher id). Both endpoints report each
|
|
566
|
+
// direction (one as sent, one as received), so take the max of the two estimates.
|
|
567
|
+
let pairPv = new Map<string, { fwd: number; back: number }>();
|
|
459
568
|
for (let [reporter, traffic] of trafficMaps) {
|
|
460
569
|
for (let [peer, data] of Object.entries(traffic.perNode || {})) {
|
|
461
570
|
if (reporter === peer || !respondedSet.has(peer)) continue;
|
|
462
571
|
let key = pairKey(reporter, peer);
|
|
463
572
|
pairTraffic.set(key, Math.max(pairTraffic.get(key) || 0, (data?.sent || 0) + (data?.received || 0)));
|
|
573
|
+
let pv = pairPv.get(key);
|
|
574
|
+
if (!pv) {
|
|
575
|
+
pv = { fwd: 0, back: 0 };
|
|
576
|
+
pairPv.set(key, pv);
|
|
577
|
+
}
|
|
578
|
+
let sentRate = data?.pathValuesSent || 0;
|
|
579
|
+
let receivedRate = data?.pathValuesReceived || 0;
|
|
580
|
+
if (reporter < peer) {
|
|
581
|
+
pv.fwd = Math.max(pv.fwd, sentRate);
|
|
582
|
+
pv.back = Math.max(pv.back, receivedRate);
|
|
583
|
+
} else {
|
|
584
|
+
pv.fwd = Math.max(pv.fwd, receivedRate);
|
|
585
|
+
pv.back = Math.max(pv.back, sentRate);
|
|
586
|
+
}
|
|
464
587
|
}
|
|
465
588
|
}
|
|
466
589
|
|
|
467
|
-
// Machine-to-machine latency =
|
|
468
|
-
let machinePairLatency = new Map<string,
|
|
590
|
+
// Machine-to-machine latency = minimum over all cross-machine thread-pair latencies; traffic = summed pair traffic.
|
|
591
|
+
let machinePairLatency = new Map<string, number>();
|
|
469
592
|
for (let [sourceId, latencies] of latencyMaps) {
|
|
470
593
|
let sourceMachine = machineIdOf(sourceId);
|
|
471
594
|
for (let [destId, latencyMs] of Object.entries(latencies)) {
|
|
@@ -473,13 +596,8 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
473
596
|
let destMachine = machineIdOf(destId);
|
|
474
597
|
if (sourceMachine === destMachine) continue;
|
|
475
598
|
let key = pairKey(sourceMachine, destMachine);
|
|
476
|
-
let
|
|
477
|
-
|
|
478
|
-
agg = { sum: 0, count: 0 };
|
|
479
|
-
machinePairLatency.set(key, agg);
|
|
480
|
-
}
|
|
481
|
-
agg.sum += latencyMs;
|
|
482
|
-
agg.count++;
|
|
599
|
+
let existing = machinePairLatency.get(key);
|
|
600
|
+
machinePairLatency.set(key, existing === undefined ? latencyMs : Math.min(existing, latencyMs));
|
|
483
601
|
}
|
|
484
602
|
}
|
|
485
603
|
let machinePairTraffic = new Map<string, number>();
|
|
@@ -491,6 +609,28 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
491
609
|
let mk = pairKey(ma, mb);
|
|
492
610
|
machinePairTraffic.set(mk, (machinePairTraffic.get(mk) || 0) + weight);
|
|
493
611
|
}
|
|
612
|
+
// Sum thread-pair path value flows up to machine pairs, keeping direction (fwd = lower machine id → higher).
|
|
613
|
+
let machinePairPv = new Map<string, { fwd: number; back: number }>();
|
|
614
|
+
for (let [key, pv] of pairPv) {
|
|
615
|
+
let [a, b] = key.split("|");
|
|
616
|
+
let ma = machineIdOf(a);
|
|
617
|
+
let mb = machineIdOf(b);
|
|
618
|
+
if (ma === mb) continue;
|
|
619
|
+
let mk = pairKey(ma, mb);
|
|
620
|
+
let entry = machinePairPv.get(mk);
|
|
621
|
+
if (!entry) {
|
|
622
|
+
entry = { fwd: 0, back: 0 };
|
|
623
|
+
machinePairPv.set(mk, entry);
|
|
624
|
+
}
|
|
625
|
+
// The thread pair's fwd direction may be flipped relative to the machine pair's ordering.
|
|
626
|
+
if (ma < mb) {
|
|
627
|
+
entry.fwd += pv.fwd;
|
|
628
|
+
entry.back += pv.back;
|
|
629
|
+
} else {
|
|
630
|
+
entry.fwd += pv.back;
|
|
631
|
+
entry.back += pv.fwd;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
494
634
|
|
|
495
635
|
let nodes: LatencyGraphNode[] = [...machineRespondedThreads.keys()].map(machineId => {
|
|
496
636
|
let allThreads = machineAllThreads.get(machineId) || [];
|
|
@@ -502,18 +642,25 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
502
642
|
};
|
|
503
643
|
});
|
|
504
644
|
let links: LatencyGraphLink[] = [];
|
|
505
|
-
for (let [key,
|
|
645
|
+
for (let [key, latencyMs] of machinePairLatency) {
|
|
506
646
|
let [source, destination] = key.split("|");
|
|
507
|
-
|
|
647
|
+
let pv = machinePairPv.get(key);
|
|
648
|
+
let extraLabel: LatencyGraphLabelLine | undefined;
|
|
649
|
+
if (pv && (pv.fwd || pv.back)) {
|
|
650
|
+
extraLabel = { text: `↑${formatNumber(pv.fwd)}/s ↓${formatNumber(pv.back)}/s values`, color: PATHVALUE_COLOR };
|
|
651
|
+
}
|
|
652
|
+
links.push({ source, destination, latencyMs, weight: machinePairTraffic.get(key) || 0, extraLabel });
|
|
508
653
|
}
|
|
509
654
|
|
|
510
655
|
// One row per thread node, grouped so a machine's threads sit together; the selected machine sorts to the top.
|
|
511
656
|
// Single composite key: selected-first, then machine, then thread — the low separator keeps segments ordered.
|
|
512
|
-
let rows = nodeIds.map(nodeId => buildNodeRow({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic: trafficMaps.get(nodeId), machineIp, machineNetworks }));
|
|
657
|
+
let rows = nodeIds.filter(nodeId => respondedSet.has(nodeId)).map(nodeId => buildNodeRow({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic: trafficMaps.get(nodeId), machineIp, machineNetworks }));
|
|
513
658
|
let selected = selectedMachineParam.value;
|
|
514
659
|
let sep = String.fromCharCode(1);
|
|
515
660
|
sort(rows, row => `${row.machineId === selected ? 0 : 1}${sep}${row.machineId}${sep}${row.threadPort}`);
|
|
516
661
|
|
|
662
|
+
let unresponsiveNodeIds = nodeIds.filter(nodeId => !respondedSet.has(nodeId));
|
|
663
|
+
|
|
517
664
|
return <div className={css.vbox(8).fillWidth}>
|
|
518
665
|
<div className={css.hbox(14)}>
|
|
519
666
|
<h2 className={css.margin(0)}>Latency Graph ({machineRespondedThreads.size} machines · {respondedSet.size}/{nodeIds.length} nodes reported)</h2>
|
|
@@ -539,7 +686,11 @@ class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: N
|
|
|
539
686
|
</div>
|
|
540
687
|
<div className={css.colorhsl(0, 0, 50)}>Click a node to sort its information to the top of the table below.</div>
|
|
541
688
|
<h2 className={css.margin(0)}>Nodes ({rows.length})</h2>
|
|
542
|
-
<NodeInfoTable rows={rows} selectedMachine={selected} />
|
|
689
|
+
<NodeInfoTable rows={rows} selectedMachine={selected} machineColumns={machineColumns} nodeMachineLatency={nodeMachineLatency} />
|
|
690
|
+
{unresponsiveNodeIds.length > 0 && <>
|
|
691
|
+
<h2 className={css.margin(0)}>Unresponsive Nodes ({unresponsiveNodeIds.length})</h2>
|
|
692
|
+
<UnresponsiveNodesTable nodeIds={unresponsiveNodeIds} machineIp={machineIp} />
|
|
693
|
+
</>}
|
|
543
694
|
</div>;
|
|
544
695
|
}
|
|
545
696
|
}
|
|
@@ -204,6 +204,7 @@ async function processPendingValidityChecks(now: number) {
|
|
|
204
204
|
let splitIndex = binarySearchBasic2(pendingValidityChecks, check => check.discoveredAt, { discoveredAt: threshold } as PendingValidityCheck);
|
|
205
205
|
if (splitIndex < 0) splitIndex = ~splitIndex;
|
|
206
206
|
|
|
207
|
+
let pathsToResolve = new Set<string>();
|
|
207
208
|
for (let i = 0; i < splitIndex; i++) {
|
|
208
209
|
let pendingCheck = pendingValidityChecks[i];
|
|
209
210
|
let currentValue = authorityStorage.getValueAtOrBeforeTime(pendingCheck.path);
|
|
@@ -215,15 +216,14 @@ async function processPendingValidityChecks(now: number) {
|
|
|
215
216
|
ourValid: pendingCheck.ourValid,
|
|
216
217
|
remoteValid: pendingCheck.remoteValid,
|
|
217
218
|
remoteNodeId: pendingCheck.remoteNodeId,
|
|
218
|
-
reason: "Pending validity check aged past MAX_CHANGE_AGE.
|
|
219
|
+
reason: "Pending validity check aged past MAX_CHANGE_AGE. Resolving the valid state to valid on all authorities.",
|
|
219
220
|
});
|
|
220
|
-
|
|
221
|
-
for (let [authorityNodeId, _] of allAuthorities.entries()) {
|
|
222
|
-
let serialized = await pathValueSerializer.serialize([currentValue]);
|
|
223
|
-
await PathAuditerController.nodes[authorityNodeId].ingestPathValues(serialized);
|
|
224
|
-
}
|
|
221
|
+
pathsToResolve.add(pendingCheck.path);
|
|
225
222
|
}
|
|
226
223
|
}
|
|
224
|
+
if (pathsToResolve.size > 0) {
|
|
225
|
+
await resolveValidStateDisagreements(pathsToResolve);
|
|
226
|
+
}
|
|
227
227
|
|
|
228
228
|
if (splitIndex > 0) {
|
|
229
229
|
for (let i = 0; i < splitIndex; i++) {
|
|
@@ -233,6 +233,73 @@ async function processPendingValidityChecks(now: number) {
|
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
async function resolveValidStateDisagreements(paths: Set<string>) {
|
|
237
|
+
let values: PathValue[] = [];
|
|
238
|
+
for (let path of paths) {
|
|
239
|
+
let value = authorityStorage.getValueAtOrBeforeTime(path);
|
|
240
|
+
if (value) {
|
|
241
|
+
values.push(value);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (values.length === 0) return;
|
|
245
|
+
|
|
246
|
+
console.info(`Resolving valid state disagreements`, { pathCount: values.length });
|
|
247
|
+
|
|
248
|
+
let otherAuthorities = PathRouter.getAllAuthoritiesForValues(values);
|
|
249
|
+
let anyValidByPath = new Map<string, boolean>();
|
|
250
|
+
for (let value of values) {
|
|
251
|
+
anyValidByPath.set(value.path, !!value.valid);
|
|
252
|
+
}
|
|
253
|
+
for (let [authorityNodeId, authorityValues] of otherAuthorities.entries()) {
|
|
254
|
+
let requests = authorityValues.map(value => ({ path: value.path, time: value.time }));
|
|
255
|
+
let responses = await PathAuditerController.nodes[authorityNodeId].getValidStates(requests);
|
|
256
|
+
for (let response of responses) {
|
|
257
|
+
if (response.valid) {
|
|
258
|
+
anyValidByPath.set(response.path, true);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let forcedByPath = new Map<string, PathValue>();
|
|
264
|
+
for (let value of values) {
|
|
265
|
+
if (!anyValidByPath.get(value.path)) continue;
|
|
266
|
+
console.info(`Resolving valid state disagreement to valid`, {
|
|
267
|
+
path: value.path,
|
|
268
|
+
timeId: value.time.time,
|
|
269
|
+
timeIdFull: value.time,
|
|
270
|
+
ourValid: value.valid,
|
|
271
|
+
});
|
|
272
|
+
if (value.valid) {
|
|
273
|
+
forcedByPath.set(value.path, value);
|
|
274
|
+
} else {
|
|
275
|
+
forcedByPath.set(value.path, { ...value, valid: true });
|
|
276
|
+
}
|
|
277
|
+
stats.fixedCount++;
|
|
278
|
+
}
|
|
279
|
+
if (forcedByPath.size === 0) return;
|
|
280
|
+
|
|
281
|
+
for (let [authorityNodeId, authorityValues] of otherAuthorities.entries()) {
|
|
282
|
+
let toSend: PathValue[] = [];
|
|
283
|
+
for (let value of authorityValues) {
|
|
284
|
+
let forced = forcedByPath.get(value.path);
|
|
285
|
+
if (forced) {
|
|
286
|
+
toSend.push(forced);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (toSend.length === 0) continue;
|
|
290
|
+
let serialized = await pathValueSerializer.serialize(toSend);
|
|
291
|
+
await PathAuditerController.nodes[authorityNodeId].ingestPathValues(serialized, true);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
validStateComputer.ingestValuesAndValidStates({
|
|
295
|
+
pathValues: Array.from(forcedByPath.values()),
|
|
296
|
+
parentSyncs: [],
|
|
297
|
+
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
298
|
+
doNotArchive: true,
|
|
299
|
+
forceValidStates: true,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
236
303
|
function trackSyncAge(config: {
|
|
237
304
|
path: string;
|
|
238
305
|
ourTimeId: number;
|
|
@@ -364,7 +431,7 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
364
431
|
ourValid: ourExact.valid,
|
|
365
432
|
remoteValid: response.valid,
|
|
366
433
|
remoteNodeId: nodeId,
|
|
367
|
-
reason: "Remote says our value is invalid, but we think it's valid.
|
|
434
|
+
reason: "Remote says our value is invalid, but we think it's valid. Resolving the valid state to valid on all authorities.",
|
|
368
435
|
});
|
|
369
436
|
} else {
|
|
370
437
|
if (!pendingValidityCheckPaths.has(request.path)) {
|
|
@@ -439,16 +506,7 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
439
506
|
}
|
|
440
507
|
|
|
441
508
|
if (pathsToForceSync.size > 0) {
|
|
442
|
-
|
|
443
|
-
let valueToForce = authorityStorage.getValueAtOrBeforeTime(path);
|
|
444
|
-
if (valueToForce) {
|
|
445
|
-
let allAuthorities = PathRouter.getAllAuthoritiesForValues([valueToForce]);
|
|
446
|
-
for (let [authorityNodeId, _] of allAuthorities.entries()) {
|
|
447
|
-
let serialized = await pathValueSerializer.serialize([valueToForce]);
|
|
448
|
-
await PathAuditerController.nodes[authorityNodeId].ingestPathValues(serialized);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
509
|
+
await resolveValidStateDisagreements(pathsToForceSync);
|
|
452
510
|
}
|
|
453
511
|
|
|
454
512
|
stats.totalAudited += pathsToAudit.length;
|
|
@@ -505,13 +563,14 @@ class PathAuditerService {
|
|
|
505
563
|
return await pathValueSerializer.serialize(results);
|
|
506
564
|
}
|
|
507
565
|
|
|
508
|
-
public async ingestPathValues(serializedValues: Buffer[]): Promise<void> {
|
|
566
|
+
public async ingestPathValues(serializedValues: Buffer[], forceValidStates?: boolean): Promise<void> {
|
|
509
567
|
let values = await pathValueSerializer.deserialize(serializedValues);
|
|
510
568
|
validStateComputer.ingestValuesAndValidStates({
|
|
511
569
|
pathValues: values,
|
|
512
570
|
parentSyncs: [],
|
|
513
571
|
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
514
572
|
doNotArchive: true,
|
|
573
|
+
forceValidStates,
|
|
515
574
|
});
|
|
516
575
|
}
|
|
517
576
|
|
|
@@ -116,6 +116,17 @@ function logProfileMeasuresTimingsNow() {
|
|
|
116
116
|
thresholdInTable: 0
|
|
117
117
|
});
|
|
118
118
|
};
|
|
119
|
+
export function logUnfiltered(depth = 2) {
|
|
120
|
+
let profile = measureObj.finish();
|
|
121
|
+
measureObj = startMeasure();
|
|
122
|
+
logMeasureTable(profile, {
|
|
123
|
+
name: `all logs at ${new Date().toLocaleString()}`,
|
|
124
|
+
mergeDepth: depth,
|
|
125
|
+
minTimeToLog: 0,
|
|
126
|
+
maxTableEntries: 10000000,
|
|
127
|
+
thresholdInTable: 0
|
|
128
|
+
});
|
|
129
|
+
};
|
|
119
130
|
|
|
120
131
|
|
|
121
132
|
registerPeriodic(logProfileMeasuresTimingsNow);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { qreact } from "../4-dom/qreact";
|
|
2
2
|
import { Button } from "./Button";
|
|
3
3
|
import { URLParam } from "./URLParam";
|
|
4
|
+
import { mainResets } from "./urlResetGroups";
|
|
4
5
|
import { InputLabelURL } from "./InputLabel";
|
|
5
6
|
import { css } from "typesafecss";
|
|
6
7
|
import { sort } from "socket-function/src/misc";
|
|
@@ -8,7 +9,7 @@ import { formatTime } from "socket-function/src/formatting/format";
|
|
|
8
9
|
|
|
9
10
|
export type LatencyGraphLabelLine = { text: string; color?: string; };
|
|
10
11
|
export type LatencyGraphNode = { id: string; label?: string; latitude?: number; longitude?: number; labelLines?: LatencyGraphLabelLine[]; weight?: number; };
|
|
11
|
-
export type LatencyGraphLink = { source: string; destination: string; latencyMs: number; weight?: number; };
|
|
12
|
+
export type LatencyGraphLink = { source: string; destination: string; latencyMs: number; weight?: number; extraLabel?: LatencyGraphLabelLine; };
|
|
12
13
|
export type LatencyGraphProps = {
|
|
13
14
|
nodes: LatencyGraphNode[];
|
|
14
15
|
links: LatencyGraphLink[];
|
|
@@ -22,15 +23,15 @@ export type LatencyGraphProps = {
|
|
|
22
23
|
|
|
23
24
|
const DEFAULT_CONNECTIONS = 4;
|
|
24
25
|
// Nearest neighbors drawn per node (the layout still uses every measured latency; this only thins the drawn lines).
|
|
25
|
-
const connectionsParam = new URLParam("lgConnections", DEFAULT_CONNECTIONS);
|
|
26
|
+
const connectionsParam = new URLParam("lgConnections", DEFAULT_CONNECTIONS, { reset: [mainResets] });
|
|
26
27
|
const DEFAULT_LATENCY_EXPONENT = 0.5;
|
|
27
28
|
// Exponent on (latency / reference) when mapping to layout distance, so far nodes push apart harder as it grows.
|
|
28
|
-
const latencyExponentParam = new URLParam("lgLatencyExponent", DEFAULT_LATENCY_EXPONENT);
|
|
29
|
-
const geoParam = new URLParam("lgGeo", false);
|
|
29
|
+
const latencyExponentParam = new URLParam("lgLatencyExponent", DEFAULT_LATENCY_EXPONENT, { reset: [mainResets] });
|
|
30
|
+
const geoParam = new URLParam("lgGeo", false, { reset: [mainResets] });
|
|
30
31
|
// On by default: each drawn connection shows its latency and (when a weight formatter is given) its traffic.
|
|
31
|
-
const showLatenciesParam = new URLParam("lgShowLatencies", true);
|
|
32
|
+
const showLatenciesParam = new URLParam("lgShowLatencies", true, { reset: [mainResets] });
|
|
32
33
|
// The config panel is collapsed by default so it doesn't eat the canvas; clicking the header expands it.
|
|
33
|
-
const configOpenParam = new URLParam("lgConfigOpen", false);
|
|
34
|
+
const configOpenParam = new URLParam("lgConfigOpen", false, { reset: [mainResets] });
|
|
34
35
|
// Pixels per degree of latitude/longitude in geographic mode (equirectangular projection).
|
|
35
36
|
const GEO_SCALE = 6;
|
|
36
37
|
|
|
@@ -62,7 +63,6 @@ const LINE_MAX_WIDTH = 7;
|
|
|
62
63
|
const LABEL_LINE_HEIGHT = 13;
|
|
63
64
|
const ZOOM_STEP = 1.1;
|
|
64
65
|
const MIN_SCALE = 0.05;
|
|
65
|
-
const MAX_SCALE = 12;
|
|
66
66
|
// Screen-space margins left around the content when auto-fitting. The top gets much more room because node labels
|
|
67
67
|
// stack upward above the nodes (and are drawn at a fixed screen size regardless of zoom).
|
|
68
68
|
const FIT_PAD = 60;
|
|
@@ -84,6 +84,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
84
84
|
maxNodeWeight = 0;
|
|
85
85
|
pairWeight = new Map<number, number>();
|
|
86
86
|
maxPairWeight = 0;
|
|
87
|
+
pairExtraLabel = new Map<number, LatencyGraphLabelLine>();
|
|
87
88
|
// Cached from props in render() so draw() (a rAF callback, not synced) never reads this.props.
|
|
88
89
|
formatWeight: ((weight: number) => string) | undefined = undefined;
|
|
89
90
|
selectedId: string | undefined = undefined;
|
|
@@ -135,6 +136,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
135
136
|
this.positionsY = new Float64Array(n);
|
|
136
137
|
this.ingestLinks();
|
|
137
138
|
this.computeWeights();
|
|
139
|
+
this.computePairLabels();
|
|
138
140
|
this.computeSolveEdges();
|
|
139
141
|
this.computeRenderEdges();
|
|
140
142
|
this.applyLayout();
|
|
@@ -166,6 +168,19 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
computePairLabels() {
|
|
172
|
+
let n = this.nodes.length;
|
|
173
|
+
let index = this.nodeIndex();
|
|
174
|
+
this.pairExtraLabel = new Map();
|
|
175
|
+
for (let link of this.props.links) {
|
|
176
|
+
if (!link.extraLabel) continue;
|
|
177
|
+
let a = index.get(link.source);
|
|
178
|
+
let b = index.get(link.destination);
|
|
179
|
+
if (a === undefined || b === undefined || a === b) continue;
|
|
180
|
+
this.pairExtraLabel.set(Math.min(a, b) * n + Math.max(a, b), link.extraLabel);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
nodeIndex() {
|
|
170
185
|
let index = new Map<string, number>();
|
|
171
186
|
for (let [i, node] of this.nodes.entries()) {
|
|
@@ -519,7 +534,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
519
534
|
let fit = Math.min((width - 2 * FIT_PAD) / worldW, (height - FIT_PAD_TOP - FIT_PAD) / worldH) * FIT_ZOOM;
|
|
520
535
|
// Only apply the fit if it's sane — never let a NaN/degenerate value nuke the whole view.
|
|
521
536
|
if (Number.isFinite(fit) && fit > 0 && Number.isFinite(minX) && Number.isFinite(minY)) {
|
|
522
|
-
this.viewScale = Math.max(MIN_SCALE,
|
|
537
|
+
this.viewScale = Math.max(MIN_SCALE, fit);
|
|
523
538
|
this.panX = -((minX + maxX) / 2 - centroidX) * this.viewScale;
|
|
524
539
|
// Center within the padded band, which sits lower than the middle because of the reserved top margin.
|
|
525
540
|
let targetCenterY = (FIT_PAD_TOP + (height - FIT_PAD)) / 2;
|
|
@@ -558,9 +573,14 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
558
573
|
let midX = (toScreenX(this.positionsX[edge.a]) + toScreenX(this.positionsX[edge.b])) / 2;
|
|
559
574
|
let midY = (toScreenY(this.positionsY[edge.a]) + toScreenY(this.positionsY[edge.b])) / 2;
|
|
560
575
|
|
|
561
|
-
let
|
|
562
|
-
let
|
|
563
|
-
let
|
|
576
|
+
let pk = Math.min(edge.a, edge.b) * this.nodes.length + Math.max(edge.a, edge.b);
|
|
577
|
+
let traffic = this.pairWeight.get(pk) || 0;
|
|
578
|
+
let subLabels: LatencyGraphLabelLine[] = [];
|
|
579
|
+
let trafficLabel = this.formatWeight && traffic > 0 && this.formatWeight(traffic) || undefined;
|
|
580
|
+
if (trafficLabel) subLabels.push({ text: trafficLabel });
|
|
581
|
+
let extraLabel = this.pairExtraLabel.get(pk);
|
|
582
|
+
if (extraLabel) subLabels.push(extraLabel);
|
|
583
|
+
let latencyY = subLabels.length ? midY - 7 : midY;
|
|
564
584
|
|
|
565
585
|
ctx.font = "10px sans-serif";
|
|
566
586
|
let latencyLabel = formatTime(edge.latency);
|
|
@@ -570,13 +590,14 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
570
590
|
ctx.fillStyle = "hsl(0, 0%, 72%)";
|
|
571
591
|
ctx.fillText(latencyLabel, midX, latencyY);
|
|
572
592
|
|
|
573
|
-
|
|
593
|
+
for (let [si, subLabel] of subLabels.entries()) {
|
|
594
|
+
let sy = midY + 7 + si * 12;
|
|
574
595
|
ctx.font = "9px sans-serif";
|
|
575
|
-
let
|
|
596
|
+
let subWidth = ctx.measureText(subLabel.text).width;
|
|
576
597
|
ctx.fillStyle = "hsla(0, 0%, 0%, 0.5)";
|
|
577
|
-
ctx.fillRect(midX -
|
|
578
|
-
ctx.fillStyle = "hsl(0, 0%, 58%)";
|
|
579
|
-
ctx.fillText(
|
|
598
|
+
ctx.fillRect(midX - subWidth / 2 - 2, sy - 6, subWidth + 4, 12);
|
|
599
|
+
ctx.fillStyle = subLabel.color || "hsl(0, 0%, 58%)";
|
|
600
|
+
ctx.fillText(subLabel.text, midX, sy);
|
|
580
601
|
}
|
|
581
602
|
}
|
|
582
603
|
}
|
|
@@ -698,7 +719,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
698
719
|
let worldX = this.centroidX + (e.offsetX - this.viewWidth / 2 - this.panX) / this.viewScale;
|
|
699
720
|
let worldY = this.centroidY + (e.offsetY - this.viewHeight / 2 - this.panY) / this.viewScale;
|
|
700
721
|
let factor = e.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
|
|
701
|
-
let newScale = Math.max(MIN_SCALE,
|
|
722
|
+
let newScale = Math.max(MIN_SCALE, this.viewScale * factor);
|
|
702
723
|
this.viewScale = newScale;
|
|
703
724
|
this.panX = e.offsetX - this.viewWidth / 2 - (worldX - this.centroidX) * newScale;
|
|
704
725
|
this.panY = e.offsetY - this.viewHeight / 2 - (worldY - this.centroidY) * newScale;
|
|
@@ -748,8 +769,10 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
748
769
|
let my = e.clientY - rect.top;
|
|
749
770
|
this.mouseX = mx;
|
|
750
771
|
this.mouseY = my;
|
|
751
|
-
//
|
|
752
|
-
|
|
772
|
+
// The move listener is on window (so panning keeps working off-canvas), so only hover while actually over the
|
|
773
|
+
// canvas — otherwise the hover would stick forever once the cursor leaves.
|
|
774
|
+
let inside = 0 <= mx && mx < rect.width && 0 <= my && my < rect.height;
|
|
775
|
+
this.hoverNode = inside ? this.nearestNodeTo(mx, my) : undefined;
|
|
753
776
|
this.scheduleFrame();
|
|
754
777
|
};
|
|
755
778
|
|
|
@@ -825,6 +848,7 @@ export class LatencyGraph extends qreact.Component<LatencyGraphProps> {
|
|
|
825
848
|
// Same node/link counts (same order): refresh the node objects so label text that changed as data streamed
|
|
826
849
|
// in — or when the traffic window toggled — shows up, without disturbing the settled layout/positions.
|
|
827
850
|
this.nodes = this.props.nodes.slice();
|
|
851
|
+
this.computePairLabels();
|
|
828
852
|
if (weightSig !== this.builtWeightSig) {
|
|
829
853
|
this.builtWeightSig = weightSig;
|
|
830
854
|
this.computeWeights();
|
package/src/server.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { disableMeasurements } from "socket-function/src/profiling/measure";
|
|
2
|
+
// NOTE: Profiling seems to make us use about twice as much memory, at least in the bad case where we have many tiny watchers.
|
|
3
|
+
if (typeof document === "undefined" && (process.argv.includes("--noprofile") || process.argv.includes("--nprofile"))) {
|
|
4
|
+
disableMeasurements();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
|
|
1
8
|
import "./forceProduction";
|
|
2
9
|
import "./inject";
|
|
3
10
|
|