querysub 0.616.0 → 0.619.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 +1 -1
- package/src/-a-archives/archives2.ts +6 -6
- package/src/-f-node-discovery/NodeDiscovery.ts +2 -2
- package/src/0-path-value-core/PathValueController.ts +4 -0
- package/src/0-path-value-core/PathValueStats.ts +83 -0
- package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +0 -5
- package/src/1-path-client/RemoteWatcher.ts +4 -2
- package/src/deployManager/MachinesPage.tsx +3 -0
- package/src/deployManager/urlParams.ts +1 -1
- package/src/diagnostics/managementPages.tsx +4 -6
- package/src/diagnostics/misc-pages/FunctionRunnersSection.tsx +38 -0
- package/src/diagnostics/misc-pages/LatencyGraphSection.tsx +524 -0
- package/src/diagnostics/misc-pages/PathValueServersSection.tsx +154 -0
- package/src/diagnostics/misc-pages/RoutingTablePage.tsx +34 -597
- package/src/diagnostics/misc-pages/SnapshotViewer.tsx +1 -2
package/package.json
CHANGED
|
@@ -73,7 +73,7 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
|
|
|
73
73
|
]);
|
|
74
74
|
|
|
75
75
|
return createArchives({
|
|
76
|
-
version:
|
|
76
|
+
version: 22,
|
|
77
77
|
sources,
|
|
78
78
|
});
|
|
79
79
|
}
|
|
@@ -131,28 +131,28 @@ function getSafeDomain() {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export function getArchives2(folder: string) {
|
|
134
|
-
return nestBucket(folder, archivesBuilderCache(getSafeDomain(), {
|
|
134
|
+
return nestBucket(folder, archivesBuilderCache(getSafeDomain(), {}));
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/** The rights are buffered server-side, so if you do many writes at once, it won't slow down the disk and it won't result in many writes to back backblades. Of course, this does mean if the server crashes, you do lose some data.
|
|
138
138
|
*/
|
|
139
139
|
export function getArchives2Buffered(folder: string, bufferDelay = timeInMinute * 5) {
|
|
140
|
-
return nestBucket(folder, archivesBuilderCache(getSafeDomain() + "-fast", {
|
|
140
|
+
return nestBucket(folder, archivesBuilderCache(getSafeDomain() + "-fast", { fast: true, writeDelay: bufferDelay }));
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
export function getArchives2BufferedPublic(folder: string, bufferDelay = timeInMinute * 5) {
|
|
144
|
-
return nestBucket(folder, archivesBuilderCache(getSafeDomain() + "-public-fast", {
|
|
144
|
+
return nestBucket(folder, archivesBuilderCache(getSafeDomain() + "-public-fast", { fast: true, writeDelay: bufferDelay, public: true }));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
export function getArchives2PrivateImmutable(folder: string) {
|
|
148
|
-
return nestBucket(folder, archivesBuilderCache(getSafeDomain() + "-private-immutable", {
|
|
148
|
+
return nestBucket(folder, archivesBuilderCache(getSafeDomain() + "-private-immutable", { immutable: true }));
|
|
149
149
|
}
|
|
150
150
|
function getAllowedOrigins(domain: string) {
|
|
151
151
|
return [`https://${domain}`, `https://127-0-0-1.${domain}:7007`];
|
|
152
152
|
}
|
|
153
153
|
export function getArchives2PublicImmutable(folder: string) {
|
|
154
154
|
let domain = getSafeDomain();
|
|
155
|
-
return nestBucket(folder, archivesBuilderCache(domain + "-public-immutable", {
|
|
155
|
+
return nestBucket(folder, archivesBuilderCache(domain + "-public-immutable", { immutable: true, public: true, allowedOrigins: getAllowedOrigins(getDomain()) }));
|
|
156
156
|
}
|
|
157
157
|
export function getArchives2Public(folder: string) {
|
|
158
158
|
let domain = getSafeDomain();
|
|
@@ -541,9 +541,9 @@ async function runServerSyncLoops() {
|
|
|
541
541
|
let suicideThreshold = Date.now() - SUICIDE_HEARTBEAT_THRESHOLD;
|
|
542
542
|
if (!lastTime || lastTime < suicideThreshold) {
|
|
543
543
|
if (!lastTime) {
|
|
544
|
-
console.error(red(`Self node was removed due to not
|
|
544
|
+
console.error(red(`Self node was removed due to heartbeat file not existing (${selfNodeId}). Terminating self process, as it likely has very stale data.`));
|
|
545
545
|
} else {
|
|
546
|
-
console.error(red(`Self node was has very old heartbeat. Terminating self process, as it likely has very stale data
|
|
546
|
+
console.error(red(`Self node was has very old heartbeat. Terminating self process, as it likely has very stale data. ${formatDateTime(lastTime)} < ${formatDateTime(suicideThreshold)}, now is ${formatDateTime(Date.now())}`));
|
|
547
547
|
}
|
|
548
548
|
process.exit();
|
|
549
549
|
}
|
|
@@ -13,6 +13,7 @@ import { getSlowdown, isDiskAudit, getDomain } from "../config";
|
|
|
13
13
|
import { decodeNodeId } from "sliftutils/misc/https/certs";
|
|
14
14
|
import { areNodeIdsEqual, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
15
15
|
import { recordPathValuesSent, recordPathValuesReceived } from "../-f-node-discovery/TrafficTracking";
|
|
16
|
+
import { recordPathValuesSentStats, recordPathValuesReceivedStats } from "./PathValueStats";
|
|
16
17
|
import { getNodeIdIP } from "socket-function/src/nodeCache";
|
|
17
18
|
import { authorityLookup } from "./AuthorityLookup";
|
|
18
19
|
import { timeoutToError } from "../errors";
|
|
@@ -60,6 +61,7 @@ export class PathValueControllerBase {
|
|
|
60
61
|
let { pathValues, nodeId } = config;
|
|
61
62
|
|
|
62
63
|
pathValueSendCount += pathValues.length;
|
|
64
|
+
recordPathValuesSentStats(pathValues);
|
|
63
65
|
let serializedValues = await measureBlock(() => pathValueSerializer.serialize(pathValues, { compress: Querysub.COMPRESS_NETWORK }), "createValues|serialize");
|
|
64
66
|
if (isDebugLogEnabled()) {
|
|
65
67
|
for (let value of pathValues) {
|
|
@@ -119,6 +121,7 @@ export class PathValueControllerBase {
|
|
|
119
121
|
let { nodeId, initialTriggers } = config;
|
|
120
122
|
pathValueSendCount += changes.length;
|
|
121
123
|
recordPathValuesSent({ count: changes.length, nodeId });
|
|
124
|
+
recordPathValuesSentStats(changes);
|
|
122
125
|
let buffers = await measureBlock(() => pathValueSerializer.serialize(changes, {
|
|
123
126
|
noLocks: !config.keepLocks,
|
|
124
127
|
compress: getCompressNetwork(),
|
|
@@ -156,6 +159,7 @@ export class PathValueControllerBase {
|
|
|
156
159
|
values = await measureBlock(() => pathValueSerializer.deserialize(valueBuffers), "sendData|deserialize");
|
|
157
160
|
ActionsHistory.OnRead(values);
|
|
158
161
|
recordPathValuesReceived({ count: values.length, nodeId: callerId });
|
|
162
|
+
recordPathValuesReceivedStats(values);
|
|
159
163
|
}
|
|
160
164
|
if (debugOnSendData) {
|
|
161
165
|
debugOnSendData(values, callerId);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
2
|
+
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
3
|
+
import { TreeSummary } from "sliftutils/treeSummary";
|
|
4
|
+
import type { SummaryEntry } from "sliftutils/treeSummary";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
Per-server, in-memory stats for path values this node has sent and received (never persisted). Two things are
|
|
8
|
+
tracked: a running total count for each direction, and a TreeSummary of the paths in each direction — a bounded
|
|
9
|
+
prefix tree that stays O(1000s of nodes) no matter how many distinct paths flow through, so we can show which
|
|
10
|
+
paths dominate without keeping every path. Cleared wholesale by the clear endpoint.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PATH_SUMMARY_EXPECTED_OUTPUT_COUNT = 100;
|
|
14
|
+
|
|
15
|
+
export type PathValueSummaryState = { count: number };
|
|
16
|
+
export type PathValueTotals = { sent: number; received: number };
|
|
17
|
+
export type PathValueDirection = "sent" | "received";
|
|
18
|
+
|
|
19
|
+
// Only the path matters for the tree; callers pass whole PathValues but we never look at anything else.
|
|
20
|
+
type PathValueLike = { path: string };
|
|
21
|
+
|
|
22
|
+
function makeTree() {
|
|
23
|
+
return new TreeSummary<PathValueLike, PathValueSummaryState>({
|
|
24
|
+
getPath: value => value.path,
|
|
25
|
+
createSummary: () => ({ count: 0 }),
|
|
26
|
+
addToSummary: (_value, summary) => { summary.count++; },
|
|
27
|
+
mergeSummaries: (target, source) => { target.count += source.count; },
|
|
28
|
+
getWeight: summary => summary.count,
|
|
29
|
+
expectedOutputCount: PATH_SUMMARY_EXPECTED_OUTPUT_COUNT,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let totalSent = 0;
|
|
34
|
+
let totalReceived = 0;
|
|
35
|
+
let sentTree = makeTree();
|
|
36
|
+
let receivedTree = makeTree();
|
|
37
|
+
|
|
38
|
+
export function recordPathValuesSentStats(pathValues: PathValueLike[]) {
|
|
39
|
+
totalSent += pathValues.length;
|
|
40
|
+
for (let value of pathValues) {
|
|
41
|
+
sentTree.add(value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function recordPathValuesReceivedStats(pathValues: PathValueLike[]) {
|
|
45
|
+
totalReceived += pathValues.length;
|
|
46
|
+
for (let value of pathValues) {
|
|
47
|
+
receivedTree.add(value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function clearPathValueStats() {
|
|
51
|
+
totalSent = 0;
|
|
52
|
+
totalReceived = 0;
|
|
53
|
+
sentTree = makeTree();
|
|
54
|
+
receivedTree = makeTree();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class PathValueStatsControllerBase {
|
|
58
|
+
// Cheap: just the two running totals, so the server list can render every server without pulling any tree data.
|
|
59
|
+
public async getTotals(): Promise<PathValueTotals> {
|
|
60
|
+
return { sent: totalSent, received: totalReceived };
|
|
61
|
+
}
|
|
62
|
+
// Heavy: the top-N path breakdown for one direction. Only fetched when a server row is expanded.
|
|
63
|
+
public async getSummary(direction: PathValueDirection, maxCount: number): Promise<SummaryEntry<PathValueSummaryState>[]> {
|
|
64
|
+
let tree = direction === "sent" ? sentTree : receivedTree;
|
|
65
|
+
return tree.getSummaries(maxCount);
|
|
66
|
+
}
|
|
67
|
+
public async clear(): Promise<void> {
|
|
68
|
+
clearPathValueStats();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const PathValueStatsController = SocketFunction.register(
|
|
73
|
+
"PathValueStatsController-3f9a1c72-2b64-4e8d-9c1a-7d5e0b3f6a21",
|
|
74
|
+
new PathValueStatsControllerBase(),
|
|
75
|
+
() => ({
|
|
76
|
+
getTotals: {},
|
|
77
|
+
getSummary: {},
|
|
78
|
+
clear: {},
|
|
79
|
+
}),
|
|
80
|
+
() => ({
|
|
81
|
+
hooks: [requiresNetworkTrustHook],
|
|
82
|
+
})
|
|
83
|
+
);
|
|
@@ -215,9 +215,4 @@ export async function loadSnapshot(config: {
|
|
|
215
215
|
for (let i = 0; i < 10; i++) {
|
|
216
216
|
console.log(green(`!!!Finished loading snapshot!!!`));
|
|
217
217
|
}
|
|
218
|
-
|
|
219
|
-
if (!config.noExit && !isPublic()) {
|
|
220
|
-
// Exit so we know when this is done. Really, all the servers need to be restarted after we load the snapshot, anyway.
|
|
221
|
-
process.exit();
|
|
222
|
-
}
|
|
223
218
|
}
|
|
@@ -35,7 +35,9 @@ const SHUTDOWN_REHOME_WINDOW = timeInMinute * 3;
|
|
|
35
35
|
const SHUTDOWN_REHOME_POLL_INTERVAL = timeInSecond * 30;
|
|
36
36
|
|
|
37
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 =
|
|
38
|
+
const LATENCY_REHOME_FACTOR = 1.5;
|
|
39
|
+
// Jitter floor added to the candidate's latency before the factor comparison. Below roughly this many milliseconds the difference between two nodes is noise, not speed (1ms vs 2ms isn't "twice as fast"), and without the floor a candidate that momentarily reads near-zero always looks infinitely faster, so we rehome on pure jitter. Added only to the candidate, keeping us biased against needless moves.
|
|
40
|
+
const LATENCY_REHOME_JITTER_MS = 50;
|
|
39
41
|
// Both latencies must be medians over this many samples — with fewer we don't trust either number enough to move watches over it.
|
|
40
42
|
const LATENCY_REHOME_HISTORY_COUNT = 10;
|
|
41
43
|
const LATENCY_REHOME_POLL_INTERVAL = timeInMinute;
|
|
@@ -186,7 +188,7 @@ export class RemoteWatcher {
|
|
|
186
188
|
if (!areAuthoritySpecsEquivalent(entry.authoritySpec, candidate.authoritySpec)) continue;
|
|
187
189
|
let candidateLatency = getNodeLatencyMedian({ nodeId: candidate.nodeId, historyCount: LATENCY_REHOME_HISTORY_COUNT });
|
|
188
190
|
if (!candidateLatency || candidateLatency.historyUsed < LATENCY_REHOME_HISTORY_COUNT) continue;
|
|
189
|
-
if (candidateLatency.latency * LATENCY_REHOME_FACTOR > current.latency) continue;
|
|
191
|
+
if ((candidateLatency.latency + LATENCY_REHOME_JITTER_MS) * LATENCY_REHOME_FACTOR > current.latency) continue;
|
|
190
192
|
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
193
|
logErrors(this.refreshAllWatches(authorityId));
|
|
192
194
|
break;
|
|
@@ -10,6 +10,7 @@ import { MachineDetailPage } from "./components/MachineDetailPage";
|
|
|
10
10
|
import { Anchor } from "../library-components/ATag";
|
|
11
11
|
import { DeployPage } from "./components/DeployPage";
|
|
12
12
|
import { StoragePage } from "./components/storage/StoragePage";
|
|
13
|
+
import { RoutingTablePage } from "../diagnostics/misc-pages/RoutingTablePage";
|
|
13
14
|
|
|
14
15
|
export class MachinesPage extends qreact.Component {
|
|
15
16
|
private renderTabs() {
|
|
@@ -22,6 +23,7 @@ export class MachinesPage extends qreact.Component {
|
|
|
22
23
|
{ key: "services", label: "Services", otherKeys: ["service-detail"] },
|
|
23
24
|
{ key: "deploy", label: "Deploy", otherKeys: [] },
|
|
24
25
|
{ key: "storage", label: "Storage", otherKeys: [] },
|
|
26
|
+
{ key: "routing", label: "PathValues / Functions Routing", otherKeys: [] },
|
|
25
27
|
].map(tab => {
|
|
26
28
|
let isActive = currentViewParam.value === tab.key || tab.otherKeys.includes(currentViewParam.value);
|
|
27
29
|
return <Anchor noStyles key={tab.key}
|
|
@@ -50,6 +52,7 @@ export class MachinesPage extends qreact.Component {
|
|
|
50
52
|
{currentViewParam.value === "machine-detail" && <MachineDetailPage />}
|
|
51
53
|
{currentViewParam.value === "deploy" && <DeployPage />}
|
|
52
54
|
{currentViewParam.value === "storage" && <StoragePage />}
|
|
55
|
+
{currentViewParam.value === "routing" && <RoutingTablePage />}
|
|
53
56
|
</div>
|
|
54
57
|
</div>;
|
|
55
58
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { URLParam } from "../library-components/URLParam";
|
|
2
2
|
|
|
3
|
-
export type ViewType = "services" | "machines" | "service-detail" | "machine-detail" | "deploy" | "storage";
|
|
3
|
+
export type ViewType = "services" | "machines" | "service-detail" | "machine-detail" | "deploy" | "storage" | "routing";
|
|
4
4
|
|
|
5
5
|
export const currentViewParam = new URLParam<ViewType>("machineview", "machines");
|
|
6
6
|
export const selectedServiceIdParam = new URLParam("serviceId", "");
|
|
@@ -80,12 +80,6 @@ export async function registerManagementPages2(config: {
|
|
|
80
80
|
let inputPages: typeof config.pages = [];
|
|
81
81
|
|
|
82
82
|
|
|
83
|
-
inputPages.push({
|
|
84
|
-
title: "Routing Table",
|
|
85
|
-
componentName: "RoutingTablePage",
|
|
86
|
-
controllerName: "RoutingTablePageController",
|
|
87
|
-
getModule: () => import("./misc-pages/RoutingTablePage"),
|
|
88
|
-
});
|
|
89
83
|
inputPages.push({
|
|
90
84
|
title: "LOG VIEWER",
|
|
91
85
|
componentName: "LogViewer3",
|
|
@@ -404,6 +398,10 @@ class ManagementRoot extends qreact.Component {
|
|
|
404
398
|
managementPageURL.getOverride("MachinesPage"),
|
|
405
399
|
currentViewParam.getOverride("storage"),
|
|
406
400
|
]}>Storage</ATag>
|
|
401
|
+
<ATag values={[
|
|
402
|
+
managementPageURL.getOverride("MachinesPage"),
|
|
403
|
+
currentViewParam.getOverride("routing"),
|
|
404
|
+
]}>PathValues / Functions Routing</ATag>
|
|
407
405
|
{pages.map(page =>
|
|
408
406
|
<ATag values={[{ param: managementPageURL, value: page.componentName }]}>{page.title}</ATag>
|
|
409
407
|
)}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module.allowclient = true;
|
|
2
|
+
|
|
3
|
+
import { qreact } from "../../4-dom/qreact";
|
|
4
|
+
import { css } from "typesafecss";
|
|
5
|
+
import { formatTime } from "socket-function/src/formatting/format";
|
|
6
|
+
import { getFunctionRunnerIndex } from "../../4-querysub/FunctionRunnerTracking";
|
|
7
|
+
import { FUNCTION_RUNNER_COLOR } from "../../misc/nodeCategoryColors";
|
|
8
|
+
import { AuthorityRangeBar } from "./RoutingTablePage";
|
|
9
|
+
|
|
10
|
+
export class FunctionRunnersSection extends qreact.Component {
|
|
11
|
+
render() {
|
|
12
|
+
let index = getFunctionRunnerIndex();
|
|
13
|
+
let nodes = index?.nodes || [];
|
|
14
|
+
return <div className={css.vbox(8).fillWidth}>
|
|
15
|
+
<h2>Function Runners ({nodes.length})</h2>
|
|
16
|
+
{nodes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(no function runners found yet)</div>}
|
|
17
|
+
{nodes.map(node =>
|
|
18
|
+
<div className={css.vbox(4).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}>
|
|
19
|
+
<div className={css.hbox(10).fillWidth}>
|
|
20
|
+
<span className={css.boldStyle}>{node.nodeId}</span>
|
|
21
|
+
<span className={css.color(FUNCTION_RUNNER_COLOR)}>networks: {node.networks.join(", ")}</span>
|
|
22
|
+
{!node.isPublic && <span className={css.colorhsl(0, 70, 35)}>(non-public)</span>}
|
|
23
|
+
<span>latency {formatTime(node.averageLatency)}</span>
|
|
24
|
+
<span>up for {formatTime(Date.now() - node.startupTime)}</span>
|
|
25
|
+
<span className={css.colorhsl(0, 0, 40).ellipsis}>{node.entryPoint}</span>
|
|
26
|
+
</div>
|
|
27
|
+
{node.shards.map(shard =>
|
|
28
|
+
<div className={css.hbox(10).fillWidth}>
|
|
29
|
+
<span>{shard.shardRange.startFraction.toFixed(4)} - {shard.shardRange.endFraction.toFixed(4)}</span>
|
|
30
|
+
<AuthorityRangeBar start={shard.shardRange.startFraction} end={shard.shardRange.endFraction} />
|
|
31
|
+
{shard.secondaryShardRange && <span className={css.colorhsl(0, 0, 50)}>secondary {shard.secondaryShardRange.startFraction.toFixed(4)} - {shard.secondaryShardRange.endFraction.toFixed(4)}</span>}
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
36
|
+
</div>;
|
|
37
|
+
}
|
|
38
|
+
}
|