querysub 0.617.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.617.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",
@@ -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
  }
@@ -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
  )}
@@ -45,19 +45,18 @@ class PathSummaryTable extends qreact.Component<{ nodeId: string; direction: Pat
45
45
 
46
46
  // One row per path-value server, grouped by machine. Reads/writes lead the line; expanding reveals that server's
47
47
  // per-path breakdown for both directions.
48
- class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo; firstOfMachine: boolean }> {
48
+ class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo }> {
49
49
  state = {
50
50
  expanded: false,
51
51
  };
52
52
  render() {
53
53
  let info = this.props.info;
54
- let spec = info.spec!;
54
+ let spec = info.spec;
55
55
  let expanded = this.state.expanded;
56
56
  let totals = RoutingTableSynced(getBrowserUrlNode()).getNodePathTotals(info.nodeId);
57
57
  let machineId = machineIdOf(info.nodeId);
58
58
  return <div
59
- className={css.button.vbox(6).pad2(10).fillWidth.hsl(0, 0, 99)
60
- .borderTop(this.props.firstOfMachine ? "2px solid hsl(0, 0%, 78%)" : "1px solid hsl(0, 0%, 90%)")}
59
+ className={css.button.vbox(6).pad2(10).fillWidth.hsl(0, 0, 99).borderTop("1px solid hsl(0, 0%, 88%)")}
61
60
  onClick={() => this.state.expanded = !expanded}
62
61
  >
63
62
  <div className={css.hbox(10).fillWidth}>
@@ -66,12 +65,12 @@ class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo; first
66
65
  ↓{totals ? formatNumber(totals.received) : "…"} ↑{totals ? formatNumber(totals.sent) : "…"}
67
66
  </span>
68
67
  <span className={css.width(`${MACHINE_CELL_WIDTH_CH}ch`).flexShrink0.colorhsl(0, 0, 45)} title={machineId}>
69
- {this.props.firstOfMachine ? machineId.slice(0, ID_CHARS) : ""}
68
+ {machineId.slice(0, ID_CHARS)}
70
69
  </span>
71
70
  <span className={css.boldStyle} title={info.nodeId}>{threadLabel(info.nodeId)}</span>
72
- <span className={css.colorhsl(0, 0, 45)}>{spec.routeStart.toFixed(4)} - {spec.routeEnd.toFixed(4)}</span>
73
- <AuthorityRangeBar start={spec.routeStart} end={spec.routeEnd} />
74
- {spec.excludeDefault && <span className={css.colorhsl(0, 70, 35)}>(excludes default)</span>}
71
+ {spec && <span className={css.colorhsl(0, 0, 45)}>{spec.routeStart.toFixed(4)} - {spec.routeEnd.toFixed(4)}</span>}
72
+ {spec && <AuthorityRangeBar start={spec.routeStart} end={spec.routeEnd} />}
73
+ {spec?.excludeDefault && <span className={css.colorhsl(0, 70, 35)}>(excludes default)</span>}
75
74
  </div>
76
75
  {expanded &&
77
76
  <div className={css.hbox(16).wrap.fillWidth.alignItems("flex-start")} onClick={e => e.stopPropagation()}>
@@ -89,42 +88,67 @@ class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo; first
89
88
  }
90
89
  }
91
90
 
92
- // Invalidate: drop every cached synced call for this node so the whole page's data disappears and refetches. Clear:
93
- // wipe the in-memory path-value stats (totals and trees) on every node, then invalidate so the now-empty stats reload.
91
+ // Invalidate: drop every cached synced call for this node so the whole page's data disappears and refetches.
94
92
  function invalidateRoutingTable(): void {
95
- RoutingTableSynced(getBrowserUrlNode()).resetAll();
93
+ let synced = RoutingTableSynced(getBrowserUrlNode());
94
+ synced.getNodePathTotals.resetAll();
95
+ synced.getNodePathSummary.resetAll();
96
96
  }
97
+ // Clear: wipe the in-memory path-value stats (totals and trees) on every node, then drop only the stats caches so the
98
+ // now-empty stats reload — leaving the specs/latency/traffic caches untouched.
97
99
  async function clearAllPathStats(nodeIds: string[]): Promise<void> {
98
100
  let synced = RoutingTableSynced(getBrowserUrlNode());
99
101
  await Promise.all(nodeIds.map(nodeId => synced.clearNodePathStats.promise(nodeId).catch(() => { })));
100
- synced.resetAll();
102
+ synced.getNodePathTotals.resetAll();
103
+ synced.getNodePathSummary.resetAll();
104
+ }
105
+
106
+ // A list of server rows, most-active first (by total path values sent + received).
107
+ class PathServerList extends qreact.Component<{ infos: NodeAuthorityInfo[]; emptyText: string }> {
108
+ render() {
109
+ let synced = RoutingTableSynced(getBrowserUrlNode());
110
+ let sumOf = (nodeId: string) => {
111
+ let totals = synced.getNodePathTotals(nodeId);
112
+ return (totals?.sent ?? 0) + (totals?.received ?? 0);
113
+ };
114
+ let infos = this.props.infos.slice();
115
+ // Two stable passes: machine/thread as a tiebreak, then sent+received descending as the primary order (so rows
116
+ // with equal — or still-loading, hence 0 — totals keep a stable machine/thread order instead of jittering).
117
+ let sep = String.fromCharCode(1);
118
+ sort(infos, x => `${machineIdOf(x.nodeId)}${sep}${threadLabel(x.nodeId)}`);
119
+ sort(infos, x => -sumOf(x.nodeId));
120
+ if (!infos.length) return <div className={css.colorhsl(0, 0, 50)}>{this.props.emptyText}</div>;
121
+ return <div className={css.vbox(0).fillWidth.bord2(0, 0, 88)}>
122
+ {infos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
123
+ </div>;
124
+ }
101
125
  }
102
126
 
103
127
  export class PathValueServersSection extends qreact.Component<{ infos: NodeAuthorityInfo[] }> {
104
128
  render() {
105
129
  let infos = this.props.infos;
106
130
  let allNodeIds = infos.map(x => x.nodeId);
107
- let routableInfos = infos.filter(x => x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0);
108
- // Group each server's threads under their machine (machine, then thread), so a machine's servers sit together.
109
- let sep = String.fromCharCode(1);
110
- sort(routableInfos, x => `${machineIdOf(x.nodeId)}${sep}${threadLabel(x.nodeId)}`);
131
+ // Path value servers are the authorities (they own a route range). Every other node can still send/receive
132
+ // path values, so it gets its own list below.
133
+ let serverInfos = infos.filter(x => x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0);
134
+ let otherInfos = infos.filter(x => !(x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0));
111
135
 
112
136
  let buttonClass = css.pad2(12, 8).button.bord2(0, 0, 20).hsl(0, 0, 100).whiteSpace("nowrap");
113
137
  return <div className={css.vbox(8).fillWidth}>
114
138
  <div className={css.hbox(12).alignItems("center")}>
115
- <h2 className={css.margin(0)}>Path Value Servers ({routableInfos.length})</h2>
139
+ <h2 className={css.margin(0)}>Path Value Servers ({serverInfos.length})</h2>
116
140
  <button className={buttonClass} onClick={() => invalidateRoutingTable()}>Invalidate</button>
117
141
  <button className={buttonClass} onClick={() => void clearAllPathStats(allNodeIds)}>Clear stats</button>
118
142
  </div>
119
143
  <div className={css.fontSize(13).colorhsl(0, 0, 45)}>
120
144
  Path values each server has received (↓) and sent (↑) since startup or the last clear. Expand a server for its top {PATH_SUMMARY_LIMIT} paths in each direction.
121
145
  </div>
122
- <div className={css.vbox(0).fillWidth.bord2(0, 0, 88)}>
123
- {routableInfos.map((info, i) => {
124
- let firstOfMachine = i === 0 || machineIdOf(routableInfos[i - 1].nodeId) !== machineIdOf(info.nodeId);
125
- return <AuthorityNodeRow key={info.nodeId} info={info} firstOfMachine={firstOfMachine} />;
126
- })}
146
+ <PathServerList infos={serverInfos} emptyText="(no path value servers)" />
147
+ <h2 className={css.margin(0)}>Other Senders ({otherInfos.length})</h2>
148
+ <div className={css.fontSize(13).colorhsl(0, 0, 45)}>
149
+ Nodes that aren't path value servers but still send/receive path values.
127
150
  </div>
151
+ <PathServerList infos={otherInfos} emptyText="(no other senders)" />
128
152
  </div>;
129
153
  }
130
154
  }
@@ -107,9 +107,6 @@ export const RoutingTablePageController = SocketFunction.register(
107
107
  () => ({
108
108
  hooks: [assertIsManagementUser],
109
109
  }),
110
- {
111
- noAutoExpose: true,
112
- }
113
110
  );
114
111
 
115
112
  export const RoutingTableSynced = getSyncedController(RoutingTablePageController, {
@@ -126,7 +126,7 @@ export class SnapshotViewer extends qreact.Component {
126
126
  }
127
127
  <Button onClick={async () => {
128
128
  let didConfirm = await seriousConfirm({
129
- message: `Restoring will break all servers. This won't break your data, but you will have to restart all servers before the system will be stable again. Before then data will be random, and invalid writes might be allowed. It is recommend to run this with only 1 querysub server running, on a developers machine. Consider writing a database iterate script instead of restoring (similar to how gc and join work).`,
129
+ message: `Restoring will put the servers in a bad state. You're gonna need to restart all services after this.`,
130
130
  });
131
131
  if (!didConfirm) {
132
132
  console.log(red("Aborted restore"));
@@ -137,7 +137,6 @@ export class SnapshotViewer extends qreact.Component {
137
137
  overview: entry
138
138
  });
139
139
  console.log(green("Restored snapshot"));
140
- window.location.reload();
141
140
  }}>
142
141
  Restore
143
142
  </Button>