querysub 0.518.0 → 0.520.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/-f-node-discovery/LatencyTracking.ts +116 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +9 -1
- package/src/-f-node-discovery/TrafficTracking.ts +159 -0
- package/src/0-path-value-core/PathRouter.ts +53 -5
- package/src/0-path-value-core/PathValueController.ts +3 -0
- package/src/0-path-value-core/pathValueArchives.ts +2 -1
- package/src/0-path-value-core/startupAuthority.ts +2 -2
- package/src/3-path-functions/PathFunctionRunner.ts +2 -0
- package/src/4-querysub/FunctionRunnerTracking.ts +5 -8
- package/src/4-querysub/Querysub.ts +4 -2
- package/src/4-querysub/QuerysubController.ts +2 -0
- package/src/4-querysub/querysubPrediction.ts +12 -11
- package/src/deployManager/components/MachineDetailPage.tsx +2 -2
- package/src/deployManager/components/ServiceDetailPage.tsx +60 -9
- package/src/deployManager/components/ServicesListPage.tsx +2 -2
- package/src/deployManager/components/Tools.tsx +6 -10
- package/src/deployManager/machineApplyMainCode.ts +13 -11
- package/src/deployManager/machineSchema.ts +50 -6
- package/src/diagnostics/managementPages.tsx +3 -3
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +324 -0
- package/src/diagnostics/pathAuditer.ts +75 -40
- package/src/library-components/LatencyGraph.tsx +1450 -0
- package/src/src.d.ts +3 -1
- package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +0 -146
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
module.allowclient = true;
|
|
2
|
+
|
|
3
|
+
import { qreact } from "../../4-dom/qreact";
|
|
4
|
+
import { css } from "typesafecss";
|
|
5
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
|
+
import { getAllNodeIds, getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
7
|
+
import { LatencyController } from "../../-f-node-discovery/LatencyTracking";
|
|
8
|
+
import { TrafficController, TrafficStats } from "../../-f-node-discovery/TrafficTracking";
|
|
9
|
+
import { getDomain } from "../../config";
|
|
10
|
+
import { decodeNodeId } from "sliftutils/misc/https/certs";
|
|
11
|
+
import { getSyncedController } from "../../library-components/SyncedController";
|
|
12
|
+
import { assertIsManagementUser } from "../managementPages";
|
|
13
|
+
import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilities";
|
|
14
|
+
import { timeoutToUndefinedSilent } from "../../errors";
|
|
15
|
+
import { sort } from "socket-function/src/misc";
|
|
16
|
+
import type { AuthoritySpec } from "../../0-path-value-core/PathRouter";
|
|
17
|
+
import { getFunctionRunnerIndex, FunctionRunnerNodeInfo } from "../../4-querysub/FunctionRunnerTracking";
|
|
18
|
+
import { formatTime, formatNumber } from "socket-function/src/formatting/format";
|
|
19
|
+
import { LatencyGraph, LatencyGraphNode, LatencyGraphLink, LatencyGraphLabelLine } from "../../library-components/LatencyGraph";
|
|
20
|
+
|
|
21
|
+
const ID_CHARS = 8;
|
|
22
|
+
// Green means querysub, blue means path value, purple means function runner.
|
|
23
|
+
const FUNCTION_COLOR = "hsl(280, 65%, 72%)";
|
|
24
|
+
const PATHVALUE_COLOR = "hsl(210, 75%, 68%)";
|
|
25
|
+
const QUERYSUB_COLOR = "hsl(140, 60%, 60%)";
|
|
26
|
+
|
|
27
|
+
const PROBE_TIMEOUT_MS = 5000;
|
|
28
|
+
const RANGE_BAR_WIDTH_PX = 360;
|
|
29
|
+
const RANGE_BAR_HEIGHT_PX = 14;
|
|
30
|
+
const LATENCY_GRAPH_HEIGHT_PX = 720;
|
|
31
|
+
|
|
32
|
+
type NodeAuthorityInfo = {
|
|
33
|
+
nodeId: string;
|
|
34
|
+
entryPoint?: string;
|
|
35
|
+
spec?: AuthoritySpec;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class RoutingTablePageControllerBase {
|
|
39
|
+
public async getAllNodeAuthoritySpecs(): Promise<NodeAuthorityInfo[]> {
|
|
40
|
+
let nodes = await getAllNodeIds();
|
|
41
|
+
return Promise.all(nodes.map(async (nodeId): Promise<NodeAuthorityInfo> => {
|
|
42
|
+
let metadata = await timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, NodeCapabilitiesController.nodes[nodeId].getMetadata());
|
|
43
|
+
return { nodeId, entryPoint: metadata?.entryPoint, spec: metadata?.authoritySpec };
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
// Proxies a single node's own latency map (browser can't talk to nodes directly). One call per node, so the
|
|
47
|
+
// graph can render each node's latencies as they arrive, and just skip nodes that are slow or unreachable.
|
|
48
|
+
public async getNodeLatencies(nodeId: string): Promise<{ [nodeId: string]: number } | undefined> {
|
|
49
|
+
return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, LatencyController.nodes[nodeId].getLatencies());
|
|
50
|
+
}
|
|
51
|
+
// Renamed (was getNodeTraffic) because the returned shape changed — a new name busts any stale synced-call cache.
|
|
52
|
+
public async getNodeTrafficStats(nodeId: string): Promise<TrafficStats | undefined> {
|
|
53
|
+
return timeoutToUndefinedSilent(PROBE_TIMEOUT_MS, TrafficController.nodes[nodeId].getTrafficStats());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const RoutingTablePageController = SocketFunction.register(
|
|
58
|
+
"AuthoritySpecPageController-7b8c9d0e-1f2a-3b4c-5d6e-7f8090a1b2c3",
|
|
59
|
+
new RoutingTablePageControllerBase(),
|
|
60
|
+
() => ({
|
|
61
|
+
getAllNodeAuthoritySpecs: {},
|
|
62
|
+
getNodeLatencies: {},
|
|
63
|
+
getNodeTrafficStats: {},
|
|
64
|
+
}),
|
|
65
|
+
() => ({
|
|
66
|
+
hooks: [assertIsManagementUser],
|
|
67
|
+
}),
|
|
68
|
+
{
|
|
69
|
+
noAutoExpose: true,
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const RoutingTableSynced = getSyncedController(RoutingTablePageController, {
|
|
74
|
+
reads: { getAllNodeAuthoritySpecs: ["nodeAuthoritySpecs"] },
|
|
75
|
+
writes: {},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
class AuthorityRangeBar extends qreact.Component<{ start: number; end: number }> {
|
|
79
|
+
render() {
|
|
80
|
+
let startPct = this.props.start * 100;
|
|
81
|
+
let widthPct = (this.props.end - this.props.start) * 100;
|
|
82
|
+
return <div className={css.relative.size(RANGE_BAR_WIDTH_PX, RANGE_BAR_HEIGHT_PX).flexShrink0.hsl(0, 0, 92).bord2(0, 0, 75)}>
|
|
83
|
+
<div className={css.absolute.top(0).left(`${startPct}%`).size(`${widthPct}%`, "100%").hsl(210, 65, 50)} />
|
|
84
|
+
</div>;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo }> {
|
|
89
|
+
state = {
|
|
90
|
+
expanded: false,
|
|
91
|
+
};
|
|
92
|
+
render() {
|
|
93
|
+
let info = this.props.info;
|
|
94
|
+
let spec = info.spec!;
|
|
95
|
+
let expanded = this.state.expanded;
|
|
96
|
+
return <div
|
|
97
|
+
className={css.button.vbox(6).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}
|
|
98
|
+
onClick={() => this.state.expanded = !expanded}
|
|
99
|
+
>
|
|
100
|
+
<div className={css.hbox(10).fillWidth}>
|
|
101
|
+
<span>{expanded ? "▼" : "▶"}</span>
|
|
102
|
+
<span className={css.boldStyle}>{info.nodeId}</span>
|
|
103
|
+
<span>{spec.routeStart.toFixed(4)} - {spec.routeEnd.toFixed(4)}</span>
|
|
104
|
+
<AuthorityRangeBar start={spec.routeStart} end={spec.routeEnd} />
|
|
105
|
+
<span className={css.colorhsl(0, 0, 50)}>width {(spec.routeEnd - spec.routeStart).toFixed(4)}</span>
|
|
106
|
+
{spec.excludeDefault && <span className={css.colorhsl(0, 70, 35)}>(excludes default)</span>}
|
|
107
|
+
<span className={css.colorhsl(0, 0, 40).ellipsis.flexFillWidth}>{info.entryPoint || "(no entry point)"}</span>
|
|
108
|
+
</div>
|
|
109
|
+
{expanded &&
|
|
110
|
+
<div className={css.vbox(2)}>
|
|
111
|
+
<div className={css.boldStyle}>Prefixes ({spec.prefixes.length}):</div>
|
|
112
|
+
{spec.prefixes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(none)</div>}
|
|
113
|
+
{spec.prefixes.map(p =>
|
|
114
|
+
<div key={p.originalPrefix} className={css.colorhsl(0, 0, 25)}>{p.originalPrefix}</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
}
|
|
118
|
+
</div>;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
class FunctionRunnersSection extends qreact.Component {
|
|
123
|
+
render() {
|
|
124
|
+
let index = getFunctionRunnerIndex();
|
|
125
|
+
let nodes = index?.nodes || [];
|
|
126
|
+
return <div className={css.vbox(8).fillWidth}>
|
|
127
|
+
<h2>Function Runners ({nodes.length})</h2>
|
|
128
|
+
{nodes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(no function runners found yet)</div>}
|
|
129
|
+
{nodes.map(node =>
|
|
130
|
+
<div className={css.vbox(4).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}>
|
|
131
|
+
<div className={css.hbox(10).fillWidth}>
|
|
132
|
+
<span className={css.boldStyle}>{node.nodeId}</span>
|
|
133
|
+
<span className={css.colorhsl(210, 60, 40)}>networks: {node.networks.join(", ")}</span>
|
|
134
|
+
{!node.isPublic && <span className={css.colorhsl(0, 70, 35)}>(non-public)</span>}
|
|
135
|
+
<span>latency {formatTime(node.averageLatency)}</span>
|
|
136
|
+
<span>up for {formatTime(Date.now() - node.startupTime)}</span>
|
|
137
|
+
<span className={css.colorhsl(0, 0, 40).ellipsis}>{node.entryPoint}</span>
|
|
138
|
+
</div>
|
|
139
|
+
{node.shards.map(shard =>
|
|
140
|
+
<div className={css.hbox(10).fillWidth}>
|
|
141
|
+
<span>{shard.shardRange.startFraction.toFixed(4)} - {shard.shardRange.endFraction.toFixed(4)}</span>
|
|
142
|
+
<AuthorityRangeBar start={shard.shardRange.startFraction} end={shard.shardRange.endFraction} />
|
|
143
|
+
{shard.secondaryShardRange && <span className={css.colorhsl(0, 0, 50)}>secondary {shard.secondaryShardRange.startFraction.toFixed(4)} - {shard.secondaryShardRange.endFraction.toFixed(4)}</span>}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function nodeLabelLines(config: {
|
|
153
|
+
nodeId: string;
|
|
154
|
+
runner: FunctionRunnerNodeInfo | undefined;
|
|
155
|
+
info: NodeAuthorityInfo | undefined;
|
|
156
|
+
traffic: TrafficStats | undefined;
|
|
157
|
+
}): LatencyGraphLabelLine[] {
|
|
158
|
+
let { nodeId, runner, info, traffic } = config;
|
|
159
|
+
let parts = decodeNodeId(nodeId, getDomain(), "allowMissingThreadId");
|
|
160
|
+
let thread = (parts?.threadId || "?").slice(0, ID_CHARS);
|
|
161
|
+
let machine = (parts?.machineId || nodeId).slice(0, ID_CHARS);
|
|
162
|
+
let lines: LatencyGraphLabelLine[] = [{ text: `${thread}:${parts?.port ?? "?"}` }];
|
|
163
|
+
// The last segment of the entry-point path is a good human name; shown on one line with the server (machine) id.
|
|
164
|
+
let entryPoint = info?.entryPoint || runner?.entryPoint;
|
|
165
|
+
let name = entryPoint ? entryPoint.split(/[\\/]/).filter(Boolean).at(-1) : undefined;
|
|
166
|
+
lines.push({ text: name ? `${name} ${machine}` : machine });
|
|
167
|
+
// Total data this node sent / received, and the percent of it that went outside our known node list.
|
|
168
|
+
if (traffic) {
|
|
169
|
+
let totals = nodeTotalTraffic(traffic);
|
|
170
|
+
let sum = totals.sent + totals.received;
|
|
171
|
+
if (sum > 0) {
|
|
172
|
+
let outsidePct = Math.round((totals.outside / sum) * 100);
|
|
173
|
+
lines.push({ text: `↑${formatNumber(totals.sent)}B ↓${formatNumber(totals.received)}B ${outsidePct}%⊘` });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// All function-runner info on one purple line: networks, shard ranges, calls, private.
|
|
177
|
+
if (runner) {
|
|
178
|
+
let fnParts: string[] = [];
|
|
179
|
+
if (runner.networks.length) fnParts.push(runner.networks.join(" "));
|
|
180
|
+
for (let shard of runner.shards) {
|
|
181
|
+
fnParts.push(`fn ${shard.shardRange.startFraction.toFixed(3)}-${shard.shardRange.endFraction.toFixed(3)}`);
|
|
182
|
+
}
|
|
183
|
+
fnParts.push(`${formatNumber(traffic?.functionsExecuted ?? 0)} calls`);
|
|
184
|
+
if (!runner.isPublic) fnParts.push("PRIVATE");
|
|
185
|
+
lines.push({ text: fnParts.join(" "), color: FUNCTION_COLOR });
|
|
186
|
+
}
|
|
187
|
+
// All path-value info on one blue line: shard range (only if real) and values sent.
|
|
188
|
+
let spec = info?.spec;
|
|
189
|
+
let pvParts: string[] = [];
|
|
190
|
+
if (spec && spec.routeStart >= 0 && spec.routeEnd >= 0) {
|
|
191
|
+
pvParts.push(`pv ${spec.routeStart.toFixed(3)}-${spec.routeEnd.toFixed(3)}`);
|
|
192
|
+
}
|
|
193
|
+
if (traffic && (traffic.pathValuesSent || traffic.pathValuesReceived)) {
|
|
194
|
+
pvParts.push(`↑${formatNumber(traffic.pathValuesSent)} ↓${formatNumber(traffic.pathValuesReceived)} values`);
|
|
195
|
+
}
|
|
196
|
+
if (pvParts.length) {
|
|
197
|
+
lines.push({ text: pvParts.join(" "), color: PATHVALUE_COLOR });
|
|
198
|
+
}
|
|
199
|
+
// Querysub activity: how many function calls were submitted to this node's QuerysubController.
|
|
200
|
+
if (traffic?.querysubCalls) {
|
|
201
|
+
lines.push({ text: `${formatNumber(traffic.querysubCalls)} addCalls`, color: QUERYSUB_COLOR });
|
|
202
|
+
}
|
|
203
|
+
return lines;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function nodeTotalTraffic(traffic: TrafficStats): { sent: number; received: number; outside: number; } {
|
|
207
|
+
let sent = traffic.outside.sent;
|
|
208
|
+
let received = traffic.outside.received;
|
|
209
|
+
for (let d of Object.values(traffic.perNode)) {
|
|
210
|
+
sent += d.sent;
|
|
211
|
+
received += d.received;
|
|
212
|
+
}
|
|
213
|
+
return { sent, received, outside: traffic.outside.sent + traffic.outside.received };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
class LatencyGraphSection extends qreact.Component<{ nodeIds: string[]; infos: NodeAuthorityInfo[] }> {
|
|
217
|
+
render() {
|
|
218
|
+
let nodeIds = this.props.nodeIds;
|
|
219
|
+
let synced = RoutingTableSynced(getBrowserUrlNode());
|
|
220
|
+
|
|
221
|
+
let index = getFunctionRunnerIndex();
|
|
222
|
+
let runnerByNode = new Map<string, FunctionRunnerNodeInfo>();
|
|
223
|
+
for (let node of index?.nodes ?? []) {
|
|
224
|
+
runnerByNode.set(node.nodeId, node);
|
|
225
|
+
}
|
|
226
|
+
let infoByNode = new Map(this.props.infos.map(info => [info.nodeId, info]));
|
|
227
|
+
|
|
228
|
+
// One synced call per node for each data source; each resolves independently so the graph fills in progressively.
|
|
229
|
+
let latencyMaps = new Map<string, { [nodeId: string]: number }>();
|
|
230
|
+
let trafficMaps = new Map<string, TrafficStats>();
|
|
231
|
+
for (let nodeId of nodeIds) {
|
|
232
|
+
let latencies = synced.getNodeLatencies(nodeId);
|
|
233
|
+
if (latencies) latencyMaps.set(nodeId, latencies);
|
|
234
|
+
let traffic = synced.getNodeTrafficStats(nodeId);
|
|
235
|
+
if (traffic) trafficMaps.set(nodeId, traffic);
|
|
236
|
+
}
|
|
237
|
+
// Only render nodes that reported their latencies — ones that never respond probably don't exist.
|
|
238
|
+
let respondedSet = new Set(latencyMaps.keys());
|
|
239
|
+
|
|
240
|
+
// Total bytes on each node-pair link (both directions). Both endpoints report the same link total, so we take
|
|
241
|
+
// the max instead of summing. Circle size = a node's total traffic.
|
|
242
|
+
let pairKey = (a: string, b: string) => a < b ? `${a}|${b}` : `${b}|${a}`;
|
|
243
|
+
let pairTraffic = new Map<string, number>();
|
|
244
|
+
for (let [reporter, traffic] of trafficMaps) {
|
|
245
|
+
for (let [peer, data] of Object.entries(traffic.perNode)) {
|
|
246
|
+
if (reporter === peer || !respondedSet.has(peer)) continue;
|
|
247
|
+
let key = pairKey(reporter, peer);
|
|
248
|
+
pairTraffic.set(key, Math.max(pairTraffic.get(key) || 0, data.sent + data.received));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let nodes: LatencyGraphNode[] = [...respondedSet].map(nodeId => {
|
|
253
|
+
let traffic = trafficMaps.get(nodeId);
|
|
254
|
+
let totals = traffic ? nodeTotalTraffic(traffic) : undefined;
|
|
255
|
+
return {
|
|
256
|
+
id: nodeId,
|
|
257
|
+
labelLines: nodeLabelLines({ nodeId, runner: runnerByNode.get(nodeId), info: infoByNode.get(nodeId), traffic }),
|
|
258
|
+
weight: totals ? totals.sent + totals.received : 0,
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
let links: LatencyGraphLink[] = [];
|
|
262
|
+
for (let [sourceId, latencies] of latencyMaps) {
|
|
263
|
+
for (let [destId, latencyMs] of Object.entries(latencies)) {
|
|
264
|
+
if (!respondedSet.has(destId)) continue;
|
|
265
|
+
links.push({ source: sourceId, destination: destId, latencyMs, weight: pairTraffic.get(pairKey(sourceId, destId)) || 0 });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Per-cluster summary: sum the same per-node metrics across the cluster's members.
|
|
270
|
+
let clusterSummary = (memberIds: string[]): LatencyGraphLabelLine[] => {
|
|
271
|
+
let sent = 0;
|
|
272
|
+
let received = 0;
|
|
273
|
+
let calls = 0;
|
|
274
|
+
let addCalls = 0;
|
|
275
|
+
let dataSent = 0;
|
|
276
|
+
let dataReceived = 0;
|
|
277
|
+
for (let id of memberIds) {
|
|
278
|
+
let traffic = trafficMaps.get(id);
|
|
279
|
+
if (!traffic) continue;
|
|
280
|
+
sent += traffic.pathValuesSent;
|
|
281
|
+
received += traffic.pathValuesReceived;
|
|
282
|
+
calls += traffic.functionsExecuted;
|
|
283
|
+
addCalls += traffic.querysubCalls;
|
|
284
|
+
let totals = nodeTotalTraffic(traffic);
|
|
285
|
+
dataSent += totals.sent;
|
|
286
|
+
dataReceived += totals.received;
|
|
287
|
+
}
|
|
288
|
+
let summary: LatencyGraphLabelLine[] = [];
|
|
289
|
+
summary.push({ text: `↑${formatNumber(dataSent)}B ↓${formatNumber(dataReceived)}B` });
|
|
290
|
+
summary.push({ text: `↑${formatNumber(sent)} ↓${formatNumber(received)} values`, color: PATHVALUE_COLOR });
|
|
291
|
+
summary.push({ text: `${formatNumber(calls)} calls`, color: FUNCTION_COLOR });
|
|
292
|
+
summary.push({ text: `${formatNumber(addCalls)} addCalls`, color: QUERYSUB_COLOR });
|
|
293
|
+
return summary;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return <div className={css.vbox(8).fillWidth}>
|
|
297
|
+
<h2 className={css.margin(0)}>Latency Graph ({respondedSet.size}/{nodeIds.length} nodes reported)</h2>
|
|
298
|
+
<div className={css.relative.fillWidth.height(LATENCY_GRAPH_HEIGHT_PX).bord2(0, 0, 85)}>
|
|
299
|
+
<LatencyGraph nodes={nodes} links={links} formatWeight={weight => formatNumber(weight) + "B"} clusterSummary={clusterSummary} />
|
|
300
|
+
</div>
|
|
301
|
+
</div>;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export class RoutingTablePage extends qreact.Component {
|
|
306
|
+
render() {
|
|
307
|
+
let infos = RoutingTableSynced(getBrowserUrlNode()).getAllNodeAuthoritySpecs();
|
|
308
|
+
if (!infos) {
|
|
309
|
+
return <div className={css.pad2(16)}>Loading routing table...</div>;
|
|
310
|
+
}
|
|
311
|
+
let allNodeIds = infos.map(x => x.nodeId);
|
|
312
|
+
let routableInfos = infos.filter(x => x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0);
|
|
313
|
+
sort(routableInfos, x => x.spec!.routeStart);
|
|
314
|
+
|
|
315
|
+
return <div className={css.vbox(12).pad2(16).fillWidth}>
|
|
316
|
+
<LatencyGraphSection nodeIds={allNodeIds} infos={infos} />
|
|
317
|
+
<h2>Routing Table ({routableInfos.length})</h2>
|
|
318
|
+
<div className={css.vbox(8).fillWidth}>
|
|
319
|
+
{routableInfos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
|
|
320
|
+
</div>
|
|
321
|
+
<FunctionRunnersSection />
|
|
322
|
+
</div>;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -308,7 +308,8 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
308
308
|
let valuesToSend: PathValue[] = [];
|
|
309
309
|
let pathsToForceSync = new Set<string>();
|
|
310
310
|
|
|
311
|
-
for (let response of responses) {
|
|
311
|
+
for (let [requestIndex, response] of responses.entries()) {
|
|
312
|
+
let request = requests[requestIndex];
|
|
312
313
|
let originalValue = originalValues.get(response.path);
|
|
313
314
|
let ourValue = authorityStorage.getValueAtOrBeforeTime(response.path) || createMissingEpochValue(response.path);
|
|
314
315
|
|
|
@@ -328,6 +329,64 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
328
329
|
|
|
329
330
|
if (ourValue.isTransparent && response.isTransparent) continue;
|
|
330
331
|
|
|
332
|
+
if (request.time) {
|
|
333
|
+
let ourExact = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
|
|
334
|
+
if (!ourExact || !ourExact.valid) continue;
|
|
335
|
+
if (compareTime(ourExact.time, epochTime) === 0) continue;
|
|
336
|
+
// The exact value = server does not have
|
|
337
|
+
// - Send it our value
|
|
338
|
+
if (!response.time) {
|
|
339
|
+
valuesToSend.push(ourExact);
|
|
340
|
+
trackSyncAge({
|
|
341
|
+
path: request.path,
|
|
342
|
+
ourTimeId: ourExact.time.time,
|
|
343
|
+
remoteTimeId: undefined,
|
|
344
|
+
ourValid: ourExact.valid,
|
|
345
|
+
remoteValid: response.valid,
|
|
346
|
+
remoteNodeId: nodeId,
|
|
347
|
+
reason: "Remote is missing our value, sending it to them",
|
|
348
|
+
});
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
// The exact value = server has, but says it's not valid
|
|
352
|
+
// - If time < MAX_CHANGE_AGE old, record this, and wait until MAX_CHANGE_AGE (put it in some other kind of queue), and then check again
|
|
353
|
+
// - If time >= MAX_CHANGE_AGE old, then tell it to change it's valid state to ours. Also, tell all other authorities that use this path.
|
|
354
|
+
// NOTE: We skew towards making values valid instead of invalid. So if there's disagreement, we always take the valid value instead of the invalid value.
|
|
355
|
+
// NOTE: The opposite case, if the remote is valid and ours is invalid, is handled by our latest value being older than the remote value, as we're never going to ask about an invalid value. It'll just implicitly change what the latest valid value is.
|
|
356
|
+
if (response.valid === false) {
|
|
357
|
+
let age = now - ourExact.time.time;
|
|
358
|
+
if (age >= MAX_CHANGE_AGE) {
|
|
359
|
+
pathsToForceSync.add(request.path);
|
|
360
|
+
trackSyncAge({
|
|
361
|
+
path: request.path,
|
|
362
|
+
ourTimeId: ourExact.time.time,
|
|
363
|
+
remoteTimeId: response.time.time,
|
|
364
|
+
ourValid: ourExact.valid,
|
|
365
|
+
remoteValid: response.valid,
|
|
366
|
+
remoteNodeId: nodeId,
|
|
367
|
+
reason: "Remote says our value is invalid, but we think it's valid. Telling all nodes about this value to ensure it's in sync everywhere.",
|
|
368
|
+
});
|
|
369
|
+
} else {
|
|
370
|
+
if (!pendingValidityCheckPaths.has(request.path)) {
|
|
371
|
+
let newCheck: PendingValidityCheck = {
|
|
372
|
+
path: request.path,
|
|
373
|
+
ourTimeId: ourExact.time.time,
|
|
374
|
+
remoteTimeId: response.time.time,
|
|
375
|
+
ourValid: ourExact.valid,
|
|
376
|
+
remoteValid: response.valid,
|
|
377
|
+
remoteNodeId: nodeId,
|
|
378
|
+
discoveredAt: now,
|
|
379
|
+
};
|
|
380
|
+
let insertIndex = binarySearchBasic2(pendingValidityChecks, check => check.discoveredAt, newCheck);
|
|
381
|
+
if (insertIndex < 0) insertIndex = ~insertIndex;
|
|
382
|
+
pendingValidityChecks.splice(insertIndex, 0, newCheck);
|
|
383
|
+
pendingValidityCheckPaths.add(request.path);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
331
390
|
// it's latest valid is newer than ours
|
|
332
391
|
// - Ask it for the value of the latest
|
|
333
392
|
if (response.valid && response.time && compareTime(response.time, ourValue.time) > 0) {
|
|
@@ -357,42 +416,6 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
357
416
|
reason: "Remote is missing our value, sending it to them",
|
|
358
417
|
});
|
|
359
418
|
}
|
|
360
|
-
// Our latest valid = server has, but says it's not valid
|
|
361
|
-
// - If time < MAX_CHANGE_AGE old, record this, and wait until MAX_CHANGE_AGE (put it in some other kind of queue), and then check again
|
|
362
|
-
// - If time >= MAX_CHANGE_AGE old, then tell it to change it's valid state to ours. Also, tell all other authorities that use this path.
|
|
363
|
-
// NOTE: We skew towards making values valid instead of invalid. So if there's disagreement, we always take the valid value instead of the invalid value.
|
|
364
|
-
// NOTE: The opposite case, if the remote is valid and ours is invalid, is handled by our latest value being older than the remote value, as we're never going to ask about an invalid value. It'll just implicitly change what the latest valid value is.
|
|
365
|
-
else if (!response.valid && ourValue.valid && ourValue.time !== epochTime) {
|
|
366
|
-
let age = now - ourValue.time.time;
|
|
367
|
-
if (age >= MAX_CHANGE_AGE) {
|
|
368
|
-
pathsToForceSync.add(response.path);
|
|
369
|
-
trackSyncAge({
|
|
370
|
-
path: response.path,
|
|
371
|
-
ourTimeId: ourValue.time.time,
|
|
372
|
-
remoteTimeId: response.time?.time,
|
|
373
|
-
ourValid: ourValue.valid,
|
|
374
|
-
remoteValid: response.valid,
|
|
375
|
-
remoteNodeId: nodeId,
|
|
376
|
-
reason: "Remote says our value is invalid, but we think it's valid. Telling all nodes about this value to ensure it's in sync everywhere.",
|
|
377
|
-
});
|
|
378
|
-
} else {
|
|
379
|
-
if (!pendingValidityCheckPaths.has(response.path)) {
|
|
380
|
-
let newCheck: PendingValidityCheck = {
|
|
381
|
-
path: response.path,
|
|
382
|
-
ourTimeId: ourValue.time.time,
|
|
383
|
-
remoteTimeId: response.time?.time,
|
|
384
|
-
ourValid: ourValue.valid ?? false,
|
|
385
|
-
remoteValid: response.valid,
|
|
386
|
-
remoteNodeId: nodeId,
|
|
387
|
-
discoveredAt: now,
|
|
388
|
-
};
|
|
389
|
-
let insertIndex = binarySearchBasic2(pendingValidityChecks, check => check.discoveredAt, newCheck);
|
|
390
|
-
if (insertIndex < 0) insertIndex = ~insertIndex;
|
|
391
|
-
pendingValidityChecks.splice(insertIndex, 0, newCheck);
|
|
392
|
-
pendingValidityCheckPaths.add(response.path);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
419
|
}
|
|
397
420
|
|
|
398
421
|
if (valuesToRequest.length > 0) {
|
|
@@ -404,6 +427,7 @@ async function auditAuthority(nodeId: string, pathsToAudit: { path: string }[],
|
|
|
404
427
|
pathValues: receivedValues,
|
|
405
428
|
parentSyncs: [],
|
|
406
429
|
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
430
|
+
doNotArchive: true,
|
|
407
431
|
});
|
|
408
432
|
}
|
|
409
433
|
}
|
|
@@ -447,7 +471,12 @@ class PathAuditerService {
|
|
|
447
471
|
public async getValidStates(requests: PathTimeRequest[]): Promise<ValidStateResponse[]> {
|
|
448
472
|
let results: ValidStateResponse[] = [];
|
|
449
473
|
for (let request of requests) {
|
|
450
|
-
let value
|
|
474
|
+
let value: PathValue | undefined;
|
|
475
|
+
if (request.time) {
|
|
476
|
+
value = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
|
|
477
|
+
} else {
|
|
478
|
+
value = authorityStorage.getValueAtOrBeforeTime(request.path);
|
|
479
|
+
}
|
|
451
480
|
results.push({
|
|
452
481
|
path: request.path,
|
|
453
482
|
time: value?.time,
|
|
@@ -462,8 +491,13 @@ class PathAuditerService {
|
|
|
462
491
|
public async getPathValues(requests: PathTimeRequest[]): Promise<Buffer[]> {
|
|
463
492
|
let results: PathValue[] = [];
|
|
464
493
|
for (let request of requests) {
|
|
465
|
-
let value
|
|
466
|
-
if (
|
|
494
|
+
let value: PathValue | undefined;
|
|
495
|
+
if (request.time) {
|
|
496
|
+
value = authorityStorage.getValueExactMaybeRejected(request.path, request.time);
|
|
497
|
+
} else {
|
|
498
|
+
value = authorityStorage.getValueAtOrBeforeTime(request.path);
|
|
499
|
+
}
|
|
500
|
+
if (value && value.valid) {
|
|
467
501
|
results.push(value);
|
|
468
502
|
}
|
|
469
503
|
}
|
|
@@ -477,6 +511,7 @@ class PathAuditerService {
|
|
|
477
511
|
pathValues: values,
|
|
478
512
|
parentSyncs: [],
|
|
479
513
|
initialTriggers: { values: new Set(), parentPaths: new Set() },
|
|
514
|
+
doNotArchive: true,
|
|
480
515
|
});
|
|
481
516
|
}
|
|
482
517
|
|