querysub 0.508.0 → 0.509.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.
Files changed (48) hide show
  1. package/bin/function-public.js +2 -0
  2. package/bin/server.js +0 -2
  3. package/package.json +3 -4
  4. package/spec.txt +5 -0
  5. package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
  6. package/src/-g-core-values/NodeCapabilities.ts +4 -0
  7. package/src/-g-core-values/scheduledShutdown.ts +48 -0
  8. package/src/0-path-value-core/AuthorityLookup.ts +50 -9
  9. package/src/0-path-value-core/PathRouter.ts +10 -31
  10. package/src/0-path-value-core/PathRouterRouteOverride.ts +14 -98
  11. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +1 -4
  12. package/src/0-path-value-core/PathValueCommitter.ts +1 -6
  13. package/src/0-path-value-core/PathValueController.ts +5 -7
  14. package/src/0-path-value-core/PathWatcher.ts +8 -11
  15. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +8 -22
  16. package/src/0-path-value-core/pathValueCore.ts +4 -5
  17. package/src/1-path-client/RemoteWatcher.ts +47 -34
  18. package/src/2-proxy/archiveMoveHarness.ts +2 -1
  19. package/src/3-path-functions/PathFunctionHelpers.ts +11 -10
  20. package/src/3-path-functions/PathFunctionRunner.ts +165 -24
  21. package/src/3-path-functions/functionReplay.ts +2 -0
  22. package/src/3-path-functions/syncSchema.ts +0 -2
  23. package/src/4-deploy/edgeBootstrap.ts +10 -0
  24. package/src/4-deploy/edgeClientWatcher.tsx +4 -6
  25. package/src/4-deploy/edgeNodes.ts +52 -12
  26. package/src/4-querysub/FunctionRunnerTracking.ts +290 -0
  27. package/src/4-querysub/Querysub.ts +11 -16
  28. package/src/4-querysub/QuerysubController.ts +13 -11
  29. package/src/4-querysub/permissions.ts +3 -0
  30. package/src/4-querysub/querysubPrediction.ts +2 -2
  31. package/src/config.ts +1 -3
  32. package/src/deployManager/components/ServiceDetailPage.tsx +127 -73
  33. package/src/deployManager/components/ServicesListPage.tsx +3 -3
  34. package/src/deployManager/machineApplyMainCode.ts +139 -153
  35. package/src/deployManager/machineController.ts +12 -61
  36. package/src/deployManager/machineSchema.ts +84 -3
  37. package/src/deployManager/setupMachineMain.ts +2 -1
  38. package/src/diagnostics/grossStats/GrossStatsController.ts +3 -2
  39. package/src/diagnostics/logs/errorTickets/autoFixer.ts +3 -2
  40. package/src/diagnostics/logs/logGitHashes.ts +1 -1
  41. package/src/diagnostics/managementPages.tsx +2 -0
  42. package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +36 -28
  43. package/src/diagnostics/misc-pages/FunctionCaptureAnalyzePage.tsx +1 -1
  44. package/src/library-components/NetworkSelector.tsx +77 -0
  45. package/src/misc.ts +19 -0
  46. package/src/user-implementation/userData.ts +1 -1
  47. package/src/zipThreaded.ts +3 -2
  48. package/bin/server-dev.js +0 -11
@@ -92,7 +92,8 @@ async function getGitHubApiKey(repoUrl: string, sshRemote: string, forceRefresh
92
92
  await open(tokenUrl);
93
93
 
94
94
  const rl = readline.createInterface({
95
- input: process.stdin,
95
+ // Cast, as different @types/node versions disagree on the stream types
96
+ input: process.stdin as unknown as NodeJS.ReadableStream,
96
97
  output: process.stdout
97
98
  });
98
99
 
@@ -12,6 +12,7 @@ import {
12
12
  import { getOwnNodeId, isOwnNodeId } from "../../-f-node-discovery/NodeDiscovery";
13
13
  import { authorityLookup } from "../../0-path-value-core/AuthorityLookup";
14
14
  import { timeoutToUndefinedSilent } from "../../errors";
15
+ import { spreadCallsOverTime } from "../../misc";
15
16
 
16
17
  const POLL_INTERVAL = timeInMinute;
17
18
  const PER_NODE_TIMEOUT = 30 * timeInSecond;
@@ -31,7 +32,7 @@ async function listClusterNodeIds(): Promise<string[]> {
31
32
  async function pollOnce() {
32
33
  let nodeIds = await listClusterNodeIds();
33
34
  let cutoff = Date.now() - RETENTION_MS;
34
- await Promise.all(nodeIds.map(async nodeId => {
35
+ await spreadCallsOverTime(nodeIds, POLL_INTERVAL, async nodeId => {
35
36
  if (isOwnNodeId(nodeId)) {
36
37
  polledBuckets.set(nodeId, getGrossStatsBuckets().filter(b => b.time >= cutoff));
37
38
  return;
@@ -46,7 +47,7 @@ async function pollOnce() {
46
47
  let merged = prev ? (result ? prev.concat(result.buckets) : prev) : (result?.buckets ?? []);
47
48
  merged = merged.filter(b => b.time >= cutoff);
48
49
  polledBuckets.set(nodeId, merged);
49
- }));
50
+ });
50
51
  }
51
52
 
52
53
  let ensurePolling = lazy(() => {
@@ -793,8 +793,9 @@ async function runClaude(prompt: string, takeNewUserComments: () => Promise<stri
793
793
  }
794
794
  });
795
795
  }
796
- forwardLines(child.stdout!, "[claude]", line => handleClaudeStreamLine(line));
797
- forwardLines(child.stderr!, "[claude:err]");
796
+ // Casts, as different @types/node versions disagree on the stream types
797
+ forwardLines(child.stdout as unknown as NodeJS.ReadableStream, "[claude]", line => handleClaudeStreamLine(line));
798
+ forwardLines(child.stderr as unknown as NodeJS.ReadableStream, "[claude:err]");
798
799
 
799
800
  let timedOut = false;
800
801
  let timeout = setTimeout(() => {
@@ -23,7 +23,7 @@ async function execPromise(command: string, options: child_process.ExecOptions)
23
23
  if (err) {
24
24
  reject(err);
25
25
  } else {
26
- resolve(stdout.trim());
26
+ resolve(String(stdout).trim());
27
27
  }
28
28
  });
29
29
  });
@@ -299,6 +299,8 @@ export async function isManagementUser() {
299
299
  ModuleId: schema.moduleId,
300
300
  FunctionId: functionId,
301
301
  runAtTime: getNextTime(),
302
+ // This call is only evaluated locally to check permissions, and is never committed, so it never reaches a FunctionRunner network
303
+ network: "",
302
304
  };
303
305
 
304
306
  let writes = await getCallWrites({
@@ -10,6 +10,8 @@ import { NodeCapabilitiesController } from "../../-g-core-values/NodeCapabilitie
10
10
  import { timeoutToUndefinedSilent } from "../../errors";
11
11
  import { sort } from "socket-function/src/misc";
12
12
  import type { AuthoritySpec } from "../../0-path-value-core/PathRouter";
13
+ import { getFunctionRunnerIndex } from "../../4-querysub/FunctionRunnerTracking";
14
+ import { formatTime } from "socket-function/src/formatting/format";
13
15
 
14
16
  const PROBE_TIMEOUT_MS = 5000;
15
17
  const RANGE_BAR_WIDTH_PX = 360;
@@ -79,7 +81,6 @@ class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo }> {
79
81
  <AuthorityRangeBar start={spec.routeStart} end={spec.routeEnd} />
80
82
  <span className={css.colorhsl(0, 0, 50)}>width {(spec.routeEnd - spec.routeStart).toFixed(4)}</span>
81
83
  {spec.excludeDefault && <span className={css.colorhsl(0, 70, 35)}>(excludes default)</span>}
82
- {spec.networks && spec.networks.length > 0 && <span className={css.colorhsl(210, 60, 40)}>networks: {spec.networks === "all" && "all" || Array.isArray(spec.networks) && spec.networks.join(", ") || ""}</span>}
83
84
  <span className={css.colorhsl(0, 0, 40).ellipsis.flexFillWidth}>{info.entryPoint || "(no entry point)"}</span>
84
85
  </div>
85
86
  {expanded &&
@@ -95,6 +96,36 @@ class AuthorityNodeRow extends qreact.Component<{ info: NodeAuthorityInfo }> {
95
96
  }
96
97
  }
97
98
 
99
+ class FunctionRunnersSection extends qreact.Component {
100
+ render() {
101
+ let index = getFunctionRunnerIndex();
102
+ let nodes = index?.nodes || [];
103
+ return <div className={css.vbox(8).fillWidth}>
104
+ <h2>Function Runners ({nodes.length})</h2>
105
+ {nodes.length === 0 && <div className={css.colorhsl(0, 0, 50)}>(no function runners found yet)</div>}
106
+ {nodes.map(node =>
107
+ <div className={css.vbox(4).pad2(10).fillWidth.bord2(0, 0, 85).hsl(0, 0, 99)}>
108
+ <div className={css.hbox(10).fillWidth}>
109
+ <span className={css.boldStyle}>{node.nodeId}</span>
110
+ <span className={css.colorhsl(210, 60, 40)}>networks: {node.networks.join(", ")}</span>
111
+ {!node.isPublic && <span className={css.colorhsl(0, 70, 35)}>(non-public)</span>}
112
+ <span>latency {formatTime(node.averageLatency)}</span>
113
+ <span>up for {formatTime(Date.now() - node.startupTime)}</span>
114
+ <span className={css.colorhsl(0, 0, 40).ellipsis}>{node.entryPoint}</span>
115
+ </div>
116
+ {node.shards.map(shard =>
117
+ <div className={css.hbox(10).fillWidth}>
118
+ <span>{shard.shardRange.startFraction.toFixed(4)} - {shard.shardRange.endFraction.toFixed(4)}</span>
119
+ <AuthorityRangeBar start={shard.shardRange.startFraction} end={shard.shardRange.endFraction} />
120
+ {shard.secondaryShardRange && <span className={css.colorhsl(0, 0, 50)}>secondary {shard.secondaryShardRange.startFraction.toFixed(4)} - {shard.secondaryShardRange.endFraction.toFixed(4)}</span>}
121
+ </div>
122
+ )}
123
+ </div>
124
+ )}
125
+ </div>;
126
+ }
127
+ }
128
+
98
129
  export class AuthoritySpecPage extends qreact.Component {
99
130
  render() {
100
131
  let infos = AuthoritySpecSynced(getBrowserUrlNode()).getAllNodeAuthoritySpecs();
@@ -104,35 +135,12 @@ export class AuthoritySpecPage extends qreact.Component {
104
135
  infos = infos.filter(x => x.spec && x.spec.routeStart >= 0 && x.spec.routeEnd >= 0);
105
136
  sort(infos, x => x.spec!.routeStart);
106
137
 
107
- let infosPerNetwork = new Map<string, NodeAuthorityInfo[]>();
108
- for (let info of infos) {
109
- let networks = info.spec!.networks;
110
- if (networks === "all") {
111
- networks = ["all"];
112
- }
113
- if (!networks || networks.length === 0) {
114
- networks = ["default"];
115
- }
116
- for (let network of networks) {
117
- let list = infosPerNetwork.get(network);
118
- if (!list) {
119
- list = [];
120
- infosPerNetwork.set(network, list);
121
- }
122
- list.push(info);
123
- }
124
- }
125
- let networkEntries = Array.from(infosPerNetwork.entries());
126
- sort(networkEntries, entry => entry[0] === "default" && " " || entry[0]);
127
-
128
138
  return <div className={css.vbox(12).pad2(16).fillWidth}>
129
139
  <h2>Routing Table ({infos.length})</h2>
130
- {networkEntries.map(([network, networkInfos]) =>
131
- <div className={css.vbox(8).fillWidth}>
132
- <h3>{network} ({networkInfos.length})</h3>
133
- {networkInfos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
134
- </div>
135
- )}
140
+ <div className={css.vbox(8).fillWidth}>
141
+ {infos.map(info => <AuthorityNodeRow key={info.nodeId} info={info} />)}
142
+ </div>
143
+ <FunctionRunnersSection />
136
144
  </div>;
137
145
  }
138
146
  }
@@ -158,7 +158,7 @@ export class FunctionCaptureAnalyzePage extends qreact.Component {
158
158
  return <div className={css.vbox(2).fillWidth}>
159
159
  <div className={css.fontWeight("bold")}>{title} ({formatNumber(rows.length)})</div>
160
160
  <div className={css.vbox(1).fillWidth.fontFamily("monospace").fontSize(12)}>
161
- {shown.map(row => <div className={css.whiteSpace("pre-wrap").wordBreak("break-all")}>{row}</div>)}
161
+ {shown.map(row => <div className={css.whiteSpace("pre-wrap").overflowWrap("break-word")}>{row}</div>)}
162
162
  </div>
163
163
  {remaining > 0 &&
164
164
  <Button onClick={() => Querysub.localCommit(() => { this.state.rowLimits[sectionId] = limit + ROW_LIMIT; })}>
@@ -0,0 +1,77 @@
1
+ import { css } from "typesafecss";
2
+ import { qreact } from "../4-dom/qreact";
3
+ import { formatTime } from "socket-function/src/formatting/format";
4
+ import { isCurrentUserSuperUser } from "../user-implementation/userData";
5
+ import { forcedNetworkURL, getAutoSelectedNetwork, getNetworkSelectionInfos, getSelectedNetwork } from "../4-querysub/FunctionRunnerTracking";
6
+
7
+ /** A dropdown showing all the function runner networks, which one our function calls are being put on, and allowing forcing a specific network (and unforcing it, going back to the automatic selection). */
8
+ export class NetworkSelector extends qreact.Component<{}> {
9
+ render() {
10
+ let infos = getNetworkSelectionInfos();
11
+ let forced = forcedNetworkURL.value;
12
+ let selected = getSelectedNetwork({ noThrow: true });
13
+ let auto = getAutoSelectedNetwork();
14
+ let isSuperUser = isCurrentUserSuperUser();
15
+
16
+ // Only networks that actually exist (have function runners) can be picked
17
+ let networks = infos.map(x => x.network);
18
+ if (forced && !networks.includes(forced)) {
19
+ networks.push(forced);
20
+ }
21
+
22
+ if (!selected) {
23
+ return <div className={css.hbox(6)}>
24
+ <span className={css.opacity(0.7)}>Network</span>
25
+ <span className={css.pad2(6, 2).bord2(0, 85, 55).colorhsl(0, 85, 55).boldStyle}>⛔ no function runner networks</span>
26
+ </div>;
27
+ }
28
+
29
+ return <div className={css.hbox(6)}>
30
+ <span className={css.opacity(0.7)}>Network</span>
31
+ <select
32
+ value={selected}
33
+ className={css.pad2(4, 2).hsl(0, 0, 16).colorhsl(0, 0, 90).bord2(0, 0, 30)}
34
+ onChange={e => {
35
+ forcedNetworkURL.value = e.currentTarget.value;
36
+ }}
37
+ >
38
+ {networks.map(network => {
39
+ let info = infos.find(x => x.network === network);
40
+ let label = network;
41
+ if (info) {
42
+ label += ` (${info.nodeCount} runner${info.nodeCount !== 1 && "s" || ""}, ${formatTime(info.averageLatency)} latency`;
43
+ if (info.averageCallTime) {
44
+ label += `, ${formatTime(info.averageCallTime)} avg call`;
45
+ }
46
+ if (!info.isPublic) {
47
+ label += `, non-public`;
48
+ }
49
+ if (info.dying) {
50
+ label += `, shutting down`;
51
+ }
52
+ label += `)`;
53
+ }
54
+ let disabled = !!info && !info.isPublic && !isSuperUser;
55
+ return <option value={network} disabled={disabled}>{label}</option>;
56
+ })}
57
+ </select>
58
+ {forced && <span
59
+ className={css.button.pad2(6, 2).hsl(35, 60, 22).colorhsl(0, 0, 90).bord2(35, 60, 40)}
60
+ title="The network is forced. Click to reset back to the automatically picked network."
61
+ onMouseDown={() => forcedNetworkURL.value = ""}
62
+ >
63
+ auto: {auto}
64
+ </span>}
65
+ {(() => {
66
+ let selectedInfo = infos.find(x => x.network === selected);
67
+ if (!selectedInfo || selectedInfo.nodeCount === 0) {
68
+ return <span className={css.pad2(6, 2).bord2(0, 85, 55).colorhsl(0, 85, 55).boldStyle}>⛔ no function runners on this network</span>;
69
+ }
70
+ if (selectedInfo.dying) {
71
+ return <span className={css.colorhsl(35, 85, 55)}>⚠️ this network is shutting down</span>;
72
+ }
73
+ return undefined;
74
+ })()}
75
+ </div>;
76
+ }
77
+ }
package/src/misc.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { canHaveChildren } from "socket-function/src/types";
2
+ import { delay } from "socket-function/src/batching";
2
3
 
3
4
  // TIMING: About 20MB/s
4
5
  export function createRandomText(count: number): string {
@@ -194,4 +195,22 @@ export function isAsyncFunction(func: unknown): boolean {
194
195
 
195
196
  export function maybeUndefined<T>(value: T): T | undefined {
196
197
  return value;
198
+ }
199
+
200
+ /** Runs `callback` for every item, but staggers the calls evenly across `totalTime` instead of
201
+ * firing them all at once, and awaits all of them before returning. Used inside a poll loop
202
+ * (with totalTime = the poll interval) so fanning out to many nodes doesn't spike — otherwise
203
+ * every dead node's timeout lands at the same instant each cycle. */
204
+ export async function spreadCallsOverTime<T>(
205
+ items: T[],
206
+ totalTime: number,
207
+ callback: (item: T, index: number) => Promise<void>
208
+ ): Promise<void> {
209
+ await Promise.all(items.map(async (item, index) => {
210
+ let startOffset = index / items.length * totalTime;
211
+ if (startOffset > 0) {
212
+ await delay(startOffset);
213
+ }
214
+ await callback(item, index);
215
+ }));
197
216
  }
@@ -892,7 +892,7 @@ export const LOCAL_STORAGE_USER_TYPE_KEY = "userType";
892
892
  export const LOCAL_STORAGE_COMMIT_DELAY_KEY = "LOCAL_STORAGE_COMMIT_DELAY_KEY";
893
893
  if (!isNode()) {
894
894
  logErrors((async () => {
895
- await Querysub.optionalStartupWait();
895
+ await Querysub.startupWait();
896
896
  let isLoggedIn = await Querysub.commitSynced(() => isCurrentUserType("user"));
897
897
  if (isLoggedIn) {
898
898
  Querysub.commit(() => functions.registerPageLoadTime());
@@ -64,8 +64,9 @@ async function spawnWorker() {
64
64
  let { promise, onFree } = messageQueue.shift()!;
65
65
  onFree();
66
66
  workerObj.queueDepth = messageQueue.length;
67
- console.log(`Worker error: ${err.message}`);
68
- promise.reject(err);
67
+ let error = err as Error;
68
+ console.log(`Worker error: ${error.stack || error}`);
69
+ promise.reject(error);
69
70
  });
70
71
  async function unzip(buffer: Buffer): Promise<Buffer> {
71
72
  let promise = new PromiseObj<Buffer>();
package/bin/server-dev.js DELETED
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- process.argv.push("--local");
4
- // Serves the dev network AND the default network, so one server can satisfy paths with no network as well as paths on our dev network.
5
- process.argv.push("--networkfile");
6
- process.argv.push("~/devnetwork.txt");
7
- process.argv.push("--network");
8
- process.argv.push("default");
9
-
10
- require("typenode");
11
- require("../src/server.ts");