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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.616.0",
3
+ "version": "0.619.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -73,7 +73,7 @@ function archiveBuilder(bucket: string, overrides: Partial<RemoteConfigBase>) {
73
73
  ]);
74
74
 
75
75
  return createArchives({
76
- version: 21,
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(), { noFullSync: true, }));
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", { noFullSync: true, fast: true, writeDelay: bufferDelay }));
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", { noFullSync: true, fast: true, writeDelay: bufferDelay, public: true }));
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", { noFullSync: true, immutable: true }));
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", { noFullSync: true, immutable: true, public: true, allowedOrigins: getAllowedOrigins(getDomain()) }));
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 heartbeating. Terminating self process, as it likely has very stale data.`));
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 = 2;
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
+ }