querysub 0.13.0 → 0.15.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.13.0",
3
+ "version": "0.15.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",
@@ -24,7 +24,7 @@
24
24
  "node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
25
25
  "pako": "^2.1.0",
26
26
  "preact": "^10.11.3",
27
- "socket-function": "^0.34.0",
27
+ "socket-function": "^0.36.0",
28
28
  "terser": "^5.31.0",
29
29
  "typesafecss": "^0.6.3",
30
30
  "yaml": "^2.5.0",
@@ -2,7 +2,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { getArchives } from "../-a-archives/archives";
3
3
  import { getDomain, isDevDebugbreak, isNoNetwork } from "../config";
4
4
  import { measureWrap } from "socket-function/src/profiling/measure";
5
- import { isNode, sha256Hash, timeInMinute, timeInSecond } from "socket-function/src/misc";
5
+ import { isNode, sha256Hash, throttleFunction, timeInMinute, timeInSecond } from "socket-function/src/misc";
6
6
  import { errorToUndefined, errorToUndefinedSilent, ignoreErrors, logErrors, timeoutToUndefined, timeoutToUndefinedSilent } from "../errors";
7
7
  import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
8
8
  import { delay, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
@@ -21,9 +21,9 @@ import { getPublicIP } from "../misc/networking";
21
21
 
22
22
  import dns from "dns/promises";
23
23
  import { isDefined } from "../misc";
24
- import { diskLog } from "../diagnostics/logs/diskLogger";
24
+ import { diskLog, noDiskLogPrefix } from "../diagnostics/logs/diskLogger";
25
25
 
26
- let HEARTBEAT_INTERVAL = timeInMinute * 5;
26
+ let HEARTBEAT_INTERVAL = timeInMinute * 2;
27
27
  // Interval which we check other heartbeats
28
28
  let CHECK_INTERVAL = HEARTBEAT_INTERVAL;
29
29
  // If the heartbeat is older than thing, it fails the dead check
@@ -37,6 +37,8 @@ let SUICIDE_HEARTBEAT_THRESHOLD = timeInMinute * 15;
37
37
 
38
38
  let CLIENTSIDE_POLL_RATE = timeInMinute * 5;
39
39
 
40
+ let API_AUDIT_RATE = timeInSecond * 5;
41
+
40
42
  let MEMORY_AUDIT_RATE = timeInMinute * 5;
41
43
  let MEMORY_AUDIT_COUNT = 3;
42
44
 
@@ -64,6 +66,9 @@ export function getOwnNodeId(): string {
64
66
  }
65
67
  return nodeId;
66
68
  }
69
+ export function getMountNodeId(): string | undefined | "" {
70
+ return SocketFunction.mountedNodeId;
71
+ }
67
72
  export function getOwnNodeIdAssert(): string {
68
73
  let nodeId = SocketFunction.mountedNodeId;
69
74
  if (!nodeId) {
@@ -154,19 +159,8 @@ function addNodeIdBase(nodeId: string) {
154
159
  allNodeIds2.add(nodeId);
155
160
  onNodesChanged();
156
161
  }
157
- function removeNodeIds(nodeIds: string[]) {
158
- diskLog("removeNodeIds", { nodeIds });
159
- nodeIds = nodeIds.filter(nodeId => allNodeIds2.has(nodeId));
160
- if (nodeIds.length === 0) return;
161
- for (let nodeId of nodeIds) {
162
- if (logging) {
163
- console.log(red(`Removed node directly ${nodeId}`));
164
- }
165
- allNodeIds2.delete(nodeId);
166
- }
167
- onNodesChanged();
168
- }
169
162
  function setNodeIds(nodeIds: string[]) {
163
+ nodeIds = nodeIds.filter(x => x !== SPECIAL_NODE_ID_FOR_UNMOUNTED_NODE);
170
164
  diskLog("setNodeIds", { nodeIds });
171
165
  // Also try all localhost ports, if we are in dev mode
172
166
  if (isNode() && isDevDebugbreak()) {
@@ -250,25 +244,18 @@ async function syncArchives() {
250
244
  async function runHeartbeatAuditLoop() {
251
245
  await onNodeDiscoveryReady();
252
246
  let deadCount = new Map<string, number>();
253
- await runInfinitePollCallAtStart(CHECK_INTERVAL, async () => {
247
+ // 90% of the normal interval, so we don't run at the same tmie as the other audit
248
+ await runInfinitePollCallAtStart(CHECK_INTERVAL * 0.9, async () => {
254
249
  if (shutdown) return;
255
250
  // Wait a bit longer, to try to prevent all nodes from synchronizing their audit times.
256
- await delay(HEARTBEAT_INTERVAL * Math.random() * 0.1);
251
+ await delay(CHECK_INTERVAL * Math.random() * 0.1);
257
252
  //console.log(magenta(`Auditing node list`));
258
253
 
259
254
  let deadTime = Date.now() - DEAD_THRESHOLD;
260
255
  let nodeIds = await archives().find("");
261
- // Should be unusual, but... if we find new nodes, add them, and let everyone else know?
262
- for (let nodeId of nodeIds) {
263
- if (!allNodeIds2.has(nodeId)) {
264
- console.log(yellow(`Found unexpected node, adding it and broadcasting it, ${nodeId}`));
265
- addNodeId(nodeId);
266
- for (let nodeId of allNodeIds2) {
267
- if (isOwnNodeId(nodeId)) continue;
268
- ignoreErrors(NodeDiscoveryController.nodes[nodeId].addNode(nodeId));
269
- }
270
- }
271
- }
256
+ // We spent the money checking the node list, so we might as well update it
257
+ setNodeIds(nodeIds);
258
+
272
259
  let removedNodeIds: string[] = [];
273
260
  for (let nodeId of nodeIds) {
274
261
  let lastTime = Number((await archives().get(nodeId))?.toString()) || 0;
@@ -291,62 +278,45 @@ async function runHeartbeatAuditLoop() {
291
278
  }
292
279
  }
293
280
 
294
- removeNodeIds(removedNodeIds);
295
-
296
- // Broadcast to all nodes the removal
297
- for (let nodeId of allNodeIds2) {
298
- if (isOwnNodeId(nodeId)) continue;
299
- ignoreErrors(NodeDiscoveryController.nodes[nodeId].removeNodes(removedNodeIds));
281
+ if (removedNodeIds.length > 0) {
282
+ void tellEveryoneNodesChanges();
300
283
  }
301
284
  });
302
285
  }
303
286
 
304
- async function isOutOfSync() {
305
- let auditNodes = shuffle(Array.from(allNodeIds2), Date.now()).slice(0, MEMORY_AUDIT_COUNT);
306
- for (let nodeId of auditNodes) {
307
- if (nodeId === getOwnNodeId()) continue;
308
-
309
- let stableWait = timeInSecond * 30;
310
- while (true) {
311
- let hash = await errorToUndefinedSilent(NodeDiscoveryController.nodes[nodeId].getAllNodesHash());
312
- if (!hash) break;
313
- let ownHash = getAllNodesHash();
314
- if (hash === ownHash) break;
315
- await delay(stableWait);
316
- let hash2 = await errorToUndefinedSilent(NodeDiscoveryController.nodes[nodeId].getAllNodesHash());
317
- if (!hash2) break;
318
- if (hash === hash2) {
319
- console.log(yellow(`Node discovery is out of sync, syncing with disk. Told by node ${nodeId}, our hash of ${ownHash} is wrong and should be ${hash2}`));
320
- return true;
321
- }
322
- // Otherwise, the hash changed, so... we maybe we just caught it while it was changing.
323
- // Wait less time, to try to catch up with the rate of change
324
- stableWait /= 2;
325
- if (stableWait < timeInSecond) {
326
- // It is changing so fast we better just sync with the disk
327
- console.log(yellow(`Node discovery is changing fast and likely out of sync, resyncing with disk. Told by node ${nodeId}`));
328
- return true;
329
- }
287
+ async function fastMemorySync(retries = 3) {
288
+ let checkNode = shuffle(Array.from(allNodeIds2).filter(x => x !== getOurNodeId()), Date.now()).slice(0, MEMORY_AUDIT_COUNT).at(0);
289
+ if (!checkNode) return;
290
+ let otherNodes = await errorToUndefinedSilent(NodeDiscoveryController.nodes[checkNode].getAllNodeIds());
291
+ if (!otherNodes) {
292
+ if (retries > 0) {
293
+ await fastMemorySync(retries - 1);
330
294
  }
295
+ return;
296
+ }
297
+ // If they are missing nodes that's fine. We constantly have extra nodes, and have to function correctly
298
+ // with extra nodes. However, if we are missing nodes, we'd prefer to have them quickly, so we should
299
+ // sync now.
300
+ let missingNodes = otherNodes.filter(nodeId => !allNodeIds2.has(nodeId));
301
+ if (missingNodes.length > 0) {
302
+ console.log(yellow(`Node list is missing nodes, resyncing node`), { checkNode, missingNodes, otherNodes });
303
+ await syncArchives();
331
304
  }
332
305
  }
333
306
 
334
307
  async function runMemoryAuditLoop() {
335
308
  await onNodeDiscoveryReady();
336
- runInfinitePoll(MEMORY_AUDIT_RATE, async () => {
337
- // In retrospec, reading all nodes every 5 minutes is fine. It costs about 0.035 USD
338
- // per month to do this, which... is a lot less than the server to run this will cost!
339
- //if (await isOutOfSync()) {
340
- await syncArchives();
341
- //}
342
- });
309
+ runInfinitePoll(MEMORY_AUDIT_RATE, syncArchives);
310
+ runInfinitePoll(API_AUDIT_RATE, fastMemorySync);
343
311
  }
344
312
 
345
313
  async function writeHeartbeat() {
346
314
  if (shutdown) return;
347
315
  let now = Date.now();
348
316
  console.log(green(`Writing heartbeat ${formatDateTime(now)}`));
349
- await archives().set(getOwnNodeId(), Buffer.from(now + ""));
317
+ let nodeId = getMountNodeId();
318
+ if (!nodeId) return;
319
+ await archives().set(nodeId, Buffer.from(now + ""));
350
320
  }
351
321
 
352
322
  async function runMainSyncLoops(discoveryReady: PromiseObj<void>) {
@@ -491,23 +461,22 @@ export async function nodeDiscoveryShutdown() {
491
461
  if (isServer()) {
492
462
  await archives().del(getOwnNodeId());
493
463
  }
494
- // Don't bother to remove the node from ourself, as we will be shutting down shortly
495
- // (but do bother to tell other nodes!)
496
- await Promise.allSettled(Array.from(allNodeIds2).map(async nodeId => {
497
- if (isOwnNodeId(nodeId)) return;
498
- await timeoutToUndefinedSilent(timeInSecond * 5, errorToUndefinedSilent(NodeDiscoveryController.nodes[nodeId].removeNodes([getOwnNodeId()])));
499
- }));
464
+ void tellEveryoneNodesChanges();
500
465
  }
466
+ const tellEveryoneNodesChanges = throttleFunction(1000, function tellEveryoneNodesChanges() {
467
+ for (let nodeId of allNodeIds2) {
468
+ if (isOwnNodeId(nodeId)) continue;
469
+ ignoreErrors(NodeDiscoveryController.nodes[nodeId].resyncNodes());
470
+ }
471
+ });
501
472
 
502
473
 
503
474
  class NodeDiscoveryControllerBase {
504
475
  public async addNode(nodeId: string) {
505
476
  addNodeId(nodeId);
506
477
  }
507
- public async removeNodes(nodeIds: string[]) {
508
- let callerId = SocketFunction.getCaller().nodeId;
509
- console.log(`remote removeNode request`, { callerId, nodeIds });
510
- removeNodeIds(nodeIds);
478
+ public async resyncNodes() {
479
+ await syncArchives();
511
480
  }
512
481
  public async getAllNodesHash(): Promise<string> {
513
482
  return getAllNodesHash();
@@ -529,7 +498,7 @@ const NodeDiscoveryController = SocketFunction.register(
529
498
  new NodeDiscoveryControllerBase(),
530
499
  () => ({
531
500
  addNode: { hooks: [requiresNetworkTrustHook] },
532
- removeNodes: { hooks: [requiresNetworkTrustHook] },
501
+ resyncNodes: { hooks: [requiresNetworkTrustHook] },
533
502
  getAllNodesHash: { hooks: [requiresNetworkTrustHook] },
534
503
  // Skip client hooks, so we don't block on authentication (IdentityController), as some of these functions
535
504
  // are needed for authentication to finish!
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { SocketFunction } from "socket-function/SocketFunction";
6
6
  import { SocketRegistered } from "socket-function/SocketFunctionTypes";
7
- import { errorToUndefined, errorToUndefinedSilent, timeoutToUndefined, timeoutToUndefinedSilent } from "../errors";
7
+ import { errorToUndefined, errorToUndefinedSilent, timeoutToUndefinedSilent } from "../errors";
8
8
  import { getAllNodeIds } from "../-f-node-discovery/NodeDiscovery";
9
9
  import { green, red, yellow } from "socket-function/src/formatting/logColors";
10
10
  import { shuffle } from "../misc/random";
@@ -55,7 +55,6 @@ class PathValueCommitter {
55
55
  // Returns true if all the writes are accepted by some node (this should almost always be the case)
56
56
  public commitValues(values: PathValue[], predictWrites: "predictWrites" | undefined): void {
57
57
  if (values.length === 0) return;
58
-
59
58
  ActionsHistory.OnWrite(values);
60
59
 
61
60
  let now = Date.now();
@@ -23,7 +23,7 @@ import { LOCAL_DOMAIN_PATH } from "../0-path-value-core/NodePathAuthorities";
23
23
  import { registerPeriodic } from "../diagnostics/periodic";
24
24
  import { remoteWatcher } from "../1-path-client/RemoteWatcher";
25
25
  import { Schema2, Schema2Fncs } from "./schema2";
26
- import { getDomain } from "../config";
26
+ import { devDebugbreak, getDomain } from "../config";
27
27
 
28
28
  import type { CallSpec } from "../3-path-functions/PathFunctionRunner";
29
29
  import type { FunctionMetadata } from "../3-path-functions/syncSchema";
@@ -597,15 +597,6 @@ export class PathValueProxyWatcher {
597
597
  throw new Error(`Tried to write a non-local path in a "noLocks" watcher, ${watcher.debugName}, path ${pathStr}`);
598
598
  }
599
599
 
600
- if (!pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
601
- // Copy the value, to ensure any proxy values aren't attempted to be written
602
- // to the database. A bit slower, but... it should be fine. We COULD do this
603
- // later on, but this makes it easier to attribute lag and errors to the original source.
604
- // - If we had a proxy, we WOULD copy it eventually, but we would copy it later,
605
- // which would cause an error, because the reader wouldn't be trackable.
606
- value = deepCloneCborx(value);
607
- }
608
-
609
600
  if (watcher.permissionsChecker) {
610
601
  if (!watcher.permissionsChecker.checkPermissions(pathStr).allowed) {
611
602
  if (value !== specialObjectWriteValue) {
@@ -713,6 +704,14 @@ export class PathValueProxyWatcher {
713
704
  };
714
705
  addWrites(pathStr, value);
715
706
  } else {
707
+ if (!pathStr.startsWith(LOCAL_DOMAIN_PATH)) {
708
+ // Copy the value, to ensure any proxy values aren't attempted to be written
709
+ // to the database. A bit slower, but... it should be fine. We COULD do this
710
+ // later on, but this makes it easier to attribute lag and errors to the original source.
711
+ // - If we had a proxy, we WOULD copy it eventually, but we would copy it later,
712
+ // which would cause an error, because the reader wouldn't be trackable.
713
+ value = deepCloneCborx(value);
714
+ }
716
715
  watcher.pendingWrites.set(pathStr, value);
717
716
  }
718
717
  };
@@ -32,8 +32,11 @@ import { setRecord } from "../-b-authorities/dnsAuthority";
32
32
  import tls from "tls";
33
33
  import net from "net";
34
34
  import { ButtonSelector } from "../library-components/ButtonSelector";
35
- import { assertIsManagementUser } from "./managementPages";
35
+ import { assertIsManagementUser, managementPageURL } from "./managementPages";
36
36
  import { SocketRegistered } from "socket-function/SocketFunctionTypes";
37
+ import { ATag } from "../library-components/ATag";
38
+ import { filterURL, selectedNodeId } from "./logs/DiskLoggerPage";
39
+ import { getSyncedController } from "../library-components/SyncedController";
37
40
 
38
41
 
39
42
  type NodeData = {
@@ -162,7 +165,7 @@ export class NodeViewer extends qreact.Component {
162
165
 
163
166
  let tables: { [key: string]: NodeData[] } = {};
164
167
  tables["PathValueServer"] = [];
165
- tables["FunctionRunner"] = [];
168
+ tables["querysub-function"] = [];
166
169
  tables["Querysub"] = [];
167
170
  for (let datum of nodeDatas) {
168
171
  if (datum.live_authorityPaths?.length) {
@@ -170,7 +173,7 @@ export class NodeViewer extends qreact.Component {
170
173
  } else if (datum.live_exposedControllers?.includes("QuerysubController-6db5ef05-7563-4473-a440-2f64f03fe6ef")) {
171
174
  tables["Querysub"].push(datum);
172
175
  } else if (datum.live_entryPoint?.endsWith("function.js")) {
173
- tables["FunctionRunner"].push(datum);
176
+ tables["querysub-function"].push(datum);
174
177
  } else {
175
178
  let entryName = datum.live_entryPoint?.replaceAll("\\", "/").split("/").pop() || "Unknown";
176
179
  tables[entryName] = tables[entryName] || [];
@@ -179,7 +182,7 @@ export class NodeViewer extends qreact.Component {
179
182
  }
180
183
 
181
184
  let builtinGroups = {
182
- "Default": ["buttons", "devToolsURL", "ip", "uptime", "Heap", "All Memory", "Blocking Lag", "port", "threadId", "machineId", "apiError", "live_entryPoint"],
185
+ "Default": ["buttons", "devToolsURL", "nodeId", "ip", "uptime", "Heap", "All Memory", "Blocking Lag", "port", "threadId", "machineId", "apiError", "live_entryPoint"],
183
186
  };
184
187
  // Column => group
185
188
  let builtInGroupsLookup = new Map<string, string>();
@@ -256,6 +259,23 @@ export class NodeViewer extends qreact.Component {
256
259
  );
257
260
  },
258
261
  },
262
+ nodeId: {
263
+ formatter: (obj) => {
264
+ let str = String(obj);
265
+ if (str.startsWith("http")) return formatValue(obj);
266
+ https://noproxy.querysub.com:1111/?page=test&showingmanagement&managementpage=DiskLoggerPage&nodeId=b4d19fba5f79d48c3.b68f46ffaffad0636.querysub.com%3A41863&filter=%22%5C%22__nodeId%5C%22%3A%5C%22b654f522bf3a67c49.b68f46ffaffad0636.querysub.com%3A33473%5C%22%22
267
+
268
+ return (
269
+ <ATag values={[
270
+ managementPageURL.getOverride("DiskLoggerPage"),
271
+ selectedNodeId.getOverride(str),
272
+ filterURL.getOverride(`"__nodeId":"${str}"`),
273
+ ]}>
274
+ Logs
275
+ </ATag>
276
+ );
277
+ },
278
+ },
259
279
  ip: {},
260
280
  ...x.table?.columns,
261
281
  //capabilities: null,
@@ -500,4 +520,6 @@ export const NodeViewerController = SocketFunction.register(
500
520
  // Auto expose, for getMiscInfo. This is protected via assertIsManagementUser anyways.
501
521
  //noAutoExpose: true,
502
522
  }
503
- );
523
+ );
524
+
525
+ export let nodeViewerController = getSyncedController(NodeViewerController);
@@ -18,7 +18,7 @@ import { LogType } from "../errorLogs/ErrorLogCore";
18
18
  import { canHaveChildren } from "socket-function/src/types";
19
19
  import { measureBlock } from "socket-function/src/profiling/measure";
20
20
  import { binarySearchBasic, list, sort, timeInDay, timeInHour } from "socket-function/src/misc";
21
- import { NodeViewerController } from "../NodeViewer";
21
+ import { NodeViewerController, nodeViewerController } from "../NodeViewer";
22
22
  import { red } from "socket-function/src/formatting/logColors";
23
23
  import { errorMessage, formatValue, genericFormat, warnMessage } from "../../5-diagnostics/GenericFormat";
24
24
  import { hslToRGB } from "socket-function/src/formatting/colors";
@@ -51,9 +51,9 @@ import { TimeRangeSelector } from "../../library-components/TimeRangeSelector";
51
51
  module.hotreload = true;
52
52
  module.hotreload = true;
53
53
 
54
- let selectedNodeId = new URLParam("nodeId", "");
54
+ export let selectedNodeId = new URLParam("nodeId", "");
55
55
 
56
- let filterURL = new URLParam("filter", "");
56
+ export let filterURL = new URLParam("filter", "");
57
57
  let sortOldestFirst = new URLParam("oldestFirst", false);
58
58
  let selectedFields = new URLParam("selectedFields", "");
59
59
 
@@ -426,12 +426,7 @@ const processLogs = cacheShallowConfigArgEqual((config: {
426
426
  return true;
427
427
  })).filter(x => x.length > 0);
428
428
  function logMatches(filter: string, log: LogObj) {
429
- for (let [key, value] of Object.entries(log)) {
430
- if (key.toLowerCase().includes(filter)) return true;
431
- if (canHaveChildren(value) && JSON.stringify(value).toLowerCase().includes(filter)) return true;
432
- if (String(value).toLowerCase().includes(filter)) return true;
433
- }
434
- return false;
429
+ return JSON.stringify(log).toLowerCase().includes(filter);
435
430
  }
436
431
  logMatchesFilter = function logMatchesFilter(log: LogObj) {
437
432
  return (
@@ -567,6 +562,4 @@ export const DiskLoggerController = SocketFunction.register(
567
562
  }
568
563
  );
569
564
 
570
- let diskLoggerController = getSyncedController(DiskLoggerController);
571
-
572
- let nodeViewerController = getSyncedController(NodeViewerController);
565
+ let diskLoggerController = getSyncedController(DiskLoggerController);