querysub 0.518.0 → 0.520.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.518.0",
3
+ "version": "0.520.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",
@@ -70,7 +70,7 @@
70
70
  "pako": "^2.1.0",
71
71
  "peggy": "^5.0.6",
72
72
  "sliftutils": "^1.7.5",
73
- "socket-function": "^1.2.5",
73
+ "socket-function": "^1.2.11",
74
74
  "terser": "^5.31.0",
75
75
  "typenode": "^6.6.1",
76
76
  "typesafecss": "^0.32.0",
@@ -0,0 +1,116 @@
1
+ /*
2
+ The single source of truth for node-to-node latency. Each server periodically pings every other node
3
+ (reusing NodeDiscovery's isAlive as a lightweight round-trip) and keeps a rolling-average latency per node.
4
+
5
+ Like FunctionRunnerTracking, this is PER SERVER, in memory (latency depends on who is measuring) and is NEVER
6
+ written to the database. Polls are spread out over the interval (spreadCallsOverTime) plus a small random jitter,
7
+ so we never hit every node at once.
8
+ */
9
+
10
+ import { SocketFunction } from "socket-function/SocketFunction";
11
+ import { timeInMinute, timeInSecond } from "socket-function/src/misc";
12
+ import { lazy } from "socket-function/src/caching";
13
+ import { delay, runInfinitePollCallAtStart } from "socket-function/src/batching";
14
+ import { isServer } from "../config2";
15
+ import { spreadCallsOverTime } from "../misc";
16
+ import { logErrors, timeoutToUndefinedSilent } from "../errors";
17
+ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
18
+ import { NodeDiscoveryController, getAllNodeIds, getOwnNodeId, isOwnNodeId } from "./NodeDiscovery";
19
+ import { blue } from "socket-function/src/formatting/logColors";
20
+ import { formatTime } from "socket-function/src/formatting/format";
21
+
22
+ const LATENCY_POLL_INTERVAL = timeInMinute;
23
+ const PING_TIMEOUT = timeInSecond * 5;
24
+ // Rolling average size. Small, so latency changes show up quickly.
25
+ const LATENCY_SAMPLE_LIMIT = 20;
26
+ // Forget nodes we haven't been able to reach for this long.
27
+ const NODE_EXPIRY_TIME = LATENCY_POLL_INTERVAL * 5;
28
+
29
+ export type NodeLatencyInfo = {
30
+ averageLatency: number;
31
+ sampleCount: number;
32
+ lastSeen: number;
33
+ };
34
+
35
+ // otherNodeId => our rolling latency to it
36
+ let latencyByNode = new Map<string, NodeLatencyInfo>();
37
+
38
+ export function getNodeLatencyInfo(nodeId: string): NodeLatencyInfo | undefined {
39
+ void startLatencyTracking();
40
+ return latencyByNode.get(nodeId);
41
+ }
42
+ export function getNodeLatency(nodeId: string): number | undefined {
43
+ return getNodeLatencyInfo(nodeId)?.averageLatency;
44
+ }
45
+
46
+ /** Our measured latency to every node we have reached, as a plain map (for sending over the wire). */
47
+ export function getOwnLatencies(): { [nodeId: string]: number } {
48
+ void startLatencyTracking();
49
+ let result: { [nodeId: string]: number } = {};
50
+ for (let [nodeId, info] of latencyByNode) {
51
+ result[nodeId] = info.averageLatency;
52
+ }
53
+ return result;
54
+ }
55
+
56
+ function recordLatency(nodeId: string, latency: number) {
57
+ let prev = latencyByNode.get(nodeId);
58
+ let sampleCount = Math.min(prev?.sampleCount || 0, LATENCY_SAMPLE_LIMIT);
59
+ latencyByNode.set(nodeId, {
60
+ averageLatency: ((prev?.averageLatency || 0) * sampleCount + latency) / (sampleCount + 1),
61
+ sampleCount: sampleCount + 1,
62
+ lastSeen: Date.now(),
63
+ });
64
+ }
65
+
66
+ async function pingNode(nodeId: string) {
67
+ // A little extra jitter on top of the spread, so pings don't line up across nodes.
68
+ await delay(Math.random() * 2000);
69
+ let start = Date.now();
70
+ let alive = await timeoutToUndefinedSilent(PING_TIMEOUT, NodeDiscoveryController.nodes[nodeId].isAlive());
71
+ if (!alive) return;
72
+ recordLatency(nodeId, Date.now() - start);
73
+ }
74
+
75
+ async function pollLatencies() {
76
+ let nodeIds = (await getAllNodeIds()).filter(nodeId => !isOwnNodeId(nodeId));
77
+ await spreadCallsOverTime(nodeIds, LATENCY_POLL_INTERVAL, pingNode);
78
+
79
+ let now = Date.now();
80
+ for (let [nodeId, info] of Array.from(latencyByNode)) {
81
+ if (info.lastSeen < now - NODE_EXPIRY_TIME) {
82
+ latencyByNode.delete(nodeId);
83
+ }
84
+ }
85
+ }
86
+
87
+ export const startLatencyTracking = lazy(async () => {
88
+ if (!isServer()) return;
89
+ console.log(blue(`Starting latency tracking (pinging every node every ${formatTime(LATENCY_POLL_INTERVAL)})`));
90
+ await runInfinitePollCallAtStart(LATENCY_POLL_INTERVAL, pollLatencies);
91
+ });
92
+
93
+ class LatencyControllerBase {
94
+ /** This node's measured latency to every other node it has reached. */
95
+ public async getLatencies(): Promise<{ [nodeId: string]: number }> {
96
+ return getOwnLatencies();
97
+ }
98
+ public async getOwnNodeId(): Promise<string> {
99
+ return getOwnNodeId();
100
+ }
101
+ }
102
+ export const LatencyController = SocketFunction.register(
103
+ "LatencyController-4c1f8a20-2e73-4f6b-9a1e-6b0d3c5a7e91",
104
+ new LatencyControllerBase(),
105
+ () => ({
106
+ getLatencies: { hooks: [requiresNetworkTrustHook] },
107
+ getOwnNodeId: { hooks: [requiresNetworkTrustHook] },
108
+ }),
109
+ () => ({})
110
+ );
111
+
112
+ if (isServer()) {
113
+ setImmediate(() => {
114
+ logErrors(startLatencyTracking());
115
+ });
116
+ }
@@ -156,6 +156,14 @@ export async function getAllNodeIds() {
156
156
  return Array.from(allNodeIds2);
157
157
  }
158
158
 
159
+ /** Synchronous, best-effort: the last cached node list, without waiting for any sync. */
160
+ export function getCachedNodeIds(): string[] {
161
+ if (nodeOverrides) {
162
+ return nodeOverrides;
163
+ }
164
+ return Array.from(allNodeIds2);
165
+ }
166
+
159
167
  export async function syncNodesNow() {
160
168
  await syncArchives();
161
169
  }
@@ -680,7 +688,7 @@ class NodeDiscoveryControllerBase {
680
688
  return isNoNetwork();
681
689
  }
682
690
  }
683
- const NodeDiscoveryController = SocketFunction.register(
691
+ export const NodeDiscoveryController = SocketFunction.register(
684
692
  "NodeDiscoveryController-7991037e-fd9e-4085-b1db-52035487e72c",
685
693
  new NodeDiscoveryControllerBase(),
686
694
  () => ({
@@ -0,0 +1,159 @@
1
+ /*
2
+ Per-server, in-memory traffic counters (never written to the database). Everything is just an integer increment
3
+ into a 5-minute bucket; we keep the last 60 minutes (12 buckets) and drop older ones, so memory is bounded.
4
+
5
+ Byte traffic comes from SocketFunction.trackMessageSizes (upload/download per connection). The connection nodeId is
6
+ sometimes a raw client-connection id, so we map it to the nice node id (debugNodeId) lazily at read time — the hot
7
+ path is just a Map increment keyed by the raw id. At read we split traffic into per-known-node buckets and a single
8
+ "outside the network" aggregate for anything not in our cached node list (clients, etc.).
9
+ */
10
+
11
+ import { SocketFunction } from "socket-function/SocketFunction";
12
+ import { timeInMinute } from "socket-function/src/misc";
13
+ import { isClientNodeId } from "socket-function/src/nodeCache";
14
+ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
15
+ import { getCachedNodeIds } from "./NodeDiscovery";
16
+ import { debugNodeId } from "../-c-identity/IdentityController";
17
+
18
+ const BUCKET_MS = timeInMinute * 5;
19
+ const BUCKET_COUNT = 12;
20
+ // Sentinel key for traffic that isn't a network node (client connections, etc.). Aggregated so memory stays O(nodes),
21
+ // not O(client connections) — clients churn through many raw ids per hour and we never want a bucket per client.
22
+ const OUTSIDE_KEY = "";
23
+
24
+ export type NodeDataTraffic = { sent: number; received: number; };
25
+ export type TrafficStats = {
26
+ functionsExecuted: number;
27
+ pathValuesSent: number;
28
+ pathValuesReceived: number;
29
+ querysubCalls: number;
30
+ // Bytes to/from each known network node, plus one aggregate for everything outside the node list.
31
+ perNode: { [nodeId: string]: NodeDataTraffic };
32
+ outside: NodeDataTraffic;
33
+ };
34
+
35
+ function currentBucket() {
36
+ return Math.floor(Date.now() / BUCKET_MS);
37
+ }
38
+ function oldestBucket() {
39
+ return currentBucket() - BUCKET_COUNT + 1;
40
+ }
41
+
42
+ // bucket => count
43
+ let functionsExecuted = new Map<number, number>();
44
+ let pathValuesSent = new Map<number, number>();
45
+ let pathValuesReceived = new Map<number, number>();
46
+ let querysubCalls = new Map<number, number>();
47
+ // raw connection nodeId => (bucket => bytes)
48
+ let bytesSent = new Map<string, Map<number, number>>();
49
+ let bytesReceived = new Map<string, Map<number, number>>();
50
+
51
+ function addSimple(buckets: Map<number, number>, amount: number) {
52
+ let bucket = currentBucket();
53
+ buckets.set(bucket, (buckets.get(bucket) || 0) + amount);
54
+ }
55
+ function addKeyed(map: Map<string, Map<number, number>>, key: string, amount: number) {
56
+ let buckets = map.get(key);
57
+ if (!buckets) {
58
+ buckets = new Map();
59
+ map.set(key, buckets);
60
+ }
61
+ addSimple(buckets, amount);
62
+ }
63
+
64
+ export function recordFunctionExecuted() {
65
+ addSimple(functionsExecuted, 1);
66
+ }
67
+ export function recordPathValuesSent(count: number) {
68
+ if (count <= 0) return;
69
+ addSimple(pathValuesSent, count);
70
+ }
71
+ export function recordPathValuesReceived(count: number) {
72
+ if (count <= 0) return;
73
+ addSimple(pathValuesReceived, count);
74
+ }
75
+ export function recordQuerysubCall() {
76
+ addSimple(querysubCalls, 1);
77
+ }
78
+
79
+ // The hot path is O(1): a client-id check (a string prefix test) collapses all non-network traffic into one key, and
80
+ // everything else is keyed by the raw (server-style) id — bounded by node count — and resolved to a nice id at read.
81
+ function trafficKey(nodeId: string): string {
82
+ return isClientNodeId(nodeId) ? OUTSIDE_KEY : nodeId;
83
+ }
84
+ SocketFunction.trackMessageSizes.upload.push((size, nodeId) => addKeyed(bytesSent, trafficKey(nodeId), size));
85
+ SocketFunction.trackMessageSizes.download.push((size, nodeId) => addKeyed(bytesReceived, trafficKey(nodeId), size));
86
+
87
+ function sumRecent(buckets: Map<number, number>): number {
88
+ let oldest = oldestBucket();
89
+ let total = 0;
90
+ for (let [bucket, count] of Array.from(buckets)) {
91
+ if (bucket < oldest) {
92
+ buckets.delete(bucket);
93
+ continue;
94
+ }
95
+ total += count;
96
+ }
97
+ return total;
98
+ }
99
+
100
+ function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
101
+ let known = new Set(getCachedNodeIds());
102
+ let perNode: { [nodeId: string]: NodeDataTraffic } = {};
103
+ let outside: NodeDataTraffic = { sent: 0, received: 0 };
104
+ // At most (node count + 1) keys, so this whole pass is O(nodes). debugNodeId (a linear lookup) is only used for
105
+ // the rare server-style id that isn't already a known node.
106
+ let attribute = (rawNodeId: string, field: "sent" | "received", amount: number) => {
107
+ if (amount <= 0) return;
108
+ if (rawNodeId === OUTSIDE_KEY) {
109
+ outside[field] += amount;
110
+ return;
111
+ }
112
+ let nice = known.has(rawNodeId) ? rawNodeId : debugNodeId(rawNodeId);
113
+ if (known.has(nice)) {
114
+ let entry = perNode[nice];
115
+ if (!entry) {
116
+ entry = { sent: 0, received: 0 };
117
+ perNode[nice] = entry;
118
+ }
119
+ entry[field] += amount;
120
+ } else {
121
+ outside[field] += amount;
122
+ }
123
+ };
124
+ for (let [rawNodeId, buckets] of Array.from(bytesSent)) {
125
+ attribute(rawNodeId, "sent", sumRecent(buckets));
126
+ if (buckets.size === 0) bytesSent.delete(rawNodeId);
127
+ }
128
+ for (let [rawNodeId, buckets] of Array.from(bytesReceived)) {
129
+ attribute(rawNodeId, "received", sumRecent(buckets));
130
+ if (buckets.size === 0) bytesReceived.delete(rawNodeId);
131
+ }
132
+ return { perNode, outside };
133
+ }
134
+
135
+ export function getTrafficStats(): TrafficStats {
136
+ let data = aggregateData();
137
+ return {
138
+ functionsExecuted: sumRecent(functionsExecuted),
139
+ pathValuesSent: sumRecent(pathValuesSent),
140
+ pathValuesReceived: sumRecent(pathValuesReceived),
141
+ querysubCalls: sumRecent(querysubCalls),
142
+ perNode: data.perNode,
143
+ outside: data.outside,
144
+ };
145
+ }
146
+
147
+ class TrafficControllerBase {
148
+ public async getTrafficStats(): Promise<TrafficStats> {
149
+ return getTrafficStats();
150
+ }
151
+ }
152
+ export const TrafficController = SocketFunction.register(
153
+ "TrafficController-9d2b7e14-6a3f-4c81-b5e2-0f7c9a1d3e64",
154
+ new TrafficControllerBase(),
155
+ () => ({
156
+ getTrafficStats: { hooks: [requiresNetworkTrustHook] },
157
+ }),
158
+ () => ({})
159
+ );
@@ -5,6 +5,7 @@ import { PathValue } from "./pathValueCore";
5
5
  import { shuffle } from "../misc/random";
6
6
  import { fastHash } from "../misc/hash";
7
7
  import { getOwnNodeId, isOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
8
+ import { getNodeLatency } from "../-f-node-discovery/LatencyTracking";
8
9
  import { unique } from "../misc";
9
10
  import { measureFnc } from "socket-function/src/profiling/measure";
10
11
  import { getRoutingOverride, hasPrefixHash } from "./PathRouterRouteOverride";
@@ -16,6 +17,53 @@ import { getBufferInt } from "socket-function/src/bits";
16
17
  import { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH } from "./PathRouterConstants";
17
18
  export { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH };
18
19
 
20
+ // Latency-preferring routing: among candidate nodes, consider only the lowest few latencies and pick randomly,
21
+ // weighted by 1/(latency + offset) — so 0ms is twice as likely as 10ms. One very close node can dominate, which is
22
+ // fine (two nearby servers just talk to each other). Replaces the old uniform-random pick between candidates.
23
+ const LATENCY_CANDIDATE_LIMIT = 5;
24
+ const LATENCY_WEIGHT_OFFSET = 10;
25
+ // Latency assumed for a node we haven't measured yet, so new/unreached nodes stay eligible but aren't preferred.
26
+ const UNKNOWN_LATENCY_MS = 50;
27
+
28
+ function getRoutingLatency(nodeId: string): number {
29
+ if (isOwnNodeId(nodeId)) return 0;
30
+ return getNodeLatency(nodeId) ?? UNKNOWN_LATENCY_MS;
31
+ }
32
+
33
+ // In-place reorder of candidates by a latency-weighted random draw over the lowest LATENCY_CANDIDATE_LIMIT
34
+ // latencies, with the remaining nodes appended in latency order as fallback for range coverage.
35
+ // TODO: Also weight by each server's load. For now we don't — but single-threaded servers self-limit somewhat,
36
+ // since an overloaded server's latency spikes, which already makes it less likely to be chosen here.
37
+ function latencyWeightedShuffle<T extends { nodeId: string }>(arr: T[]) {
38
+ if (arr.length <= 1) return;
39
+ let withLatency = arr.map(x => ({ x, latency: getRoutingLatency(x.nodeId) }));
40
+ sort(withLatency, e => e.latency);
41
+ let pool = withLatency.slice(0, LATENCY_CANDIDATE_LIMIT);
42
+ let rest = withLatency.slice(LATENCY_CANDIDATE_LIMIT);
43
+ let ordered: T[] = [];
44
+ while (pool.length > 0) {
45
+ let total = 0;
46
+ for (let e of pool) {
47
+ total += 1 / (e.latency + LATENCY_WEIGHT_OFFSET);
48
+ }
49
+ let r = Math.random() * total;
50
+ let idx = 0;
51
+ while (idx < pool.length - 1) {
52
+ r -= 1 / (pool[idx].latency + LATENCY_WEIGHT_OFFSET);
53
+ if (r <= 0) break;
54
+ idx++;
55
+ }
56
+ ordered.push(pool[idx].x);
57
+ pool.splice(idx, 1);
58
+ }
59
+ for (let e of rest) {
60
+ ordered.push(e.x);
61
+ }
62
+ for (let i = 0; i < arr.length; i++) {
63
+ arr[i] = ordered[i];
64
+ }
65
+ }
66
+
19
67
  // Cases
20
68
  // 1) Whole path hash
21
69
  // 2) Prefix + child override
@@ -496,7 +544,7 @@ export class PathRouter {
496
544
 
497
545
  let preferredNodeIds = new Set(config?.preferredNodeIds ?? []);
498
546
  for (let group of cohesiveGroups) {
499
- shuffle(group, Math.random());
547
+ latencyWeightedShuffle(group);
500
548
  sort(group, x => (authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 2 || 0) + (preferredNodeIds.has(x.nodeId) && 0 || 1));
501
549
  let targetStart = target.routeStart;
502
550
  let targetEnd = target.routeEnd;
@@ -579,7 +627,7 @@ export class PathRouter {
579
627
  // Direct prefix. This happens for things like calls and functions, it requires more advanced routing as it means we're going to route between multiple servers, but... it is important
580
628
  let hasPrefix = allSources.filter(x => x.authoritySpec.prefixes.some(y => isPrefixParent(y, path))).map(x => x.authoritySpec);
581
629
  if (hasPrefix.length > 0) {
582
- shuffle(hasPrefix, Math.random());
630
+ latencyWeightedShuffle(hasPrefix);
583
631
  sort(hasPrefix, x => (authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 2 || 0) + (preferredNodeIds.has(x.nodeId) && 0 || 1));
584
632
 
585
633
  let missingRanges: { start: number; end: number }[] = [{
@@ -621,7 +669,7 @@ export class PathRouter {
621
669
  );
622
670
  });
623
671
  if (nestedMatches.length > 0) {
624
- shuffle(nestedMatches, Math.random());
672
+ latencyWeightedShuffle(nestedMatches);
625
673
  sort(nestedMatches, x => (authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 2 || 0) + (preferredNodeIds.has(x.nodeId) && 0 || 1));
626
674
  sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
627
675
  return {
@@ -640,7 +688,7 @@ export class PathRouter {
640
688
  });
641
689
  // Same as prefix matches. Not preferred, and not preferred over being under a prefix, but required for some root data, or data with no prefixes.
642
690
  if (fullPathMatches.length > 0) {
643
- shuffle(fullPathMatches, Math.random());
691
+ latencyWeightedShuffle(fullPathMatches);
644
692
  sort(fullPathMatches, x => (authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 2 || 0) + (preferredNodeIds.has(x.nodeId) && 0 || 1));
645
693
  sort(allSources, x => isOwnNodeId(x.nodeId) ? -1 : 1);
646
694
  let missingRanges: { start: number; end: number }[] = [{
@@ -688,7 +736,7 @@ export class PathRouter {
688
736
  @measureFnc
689
737
  public static getReadyAuthority(path: string): AuthorityEntry | undefined {
690
738
  let candidates = authorityLookup.getTopologySync();
691
- shuffle(candidates, Math.random());
739
+ latencyWeightedShuffle(candidates);
692
740
  // Only use nodes that are about to shut down if nothing else matches
693
741
  sort(candidates, x => authorityLookup.nodeIsShuttingDownSoon(x.nodeId) && 1 || 0);
694
742
  for (let candidate of candidates) {
@@ -12,6 +12,7 @@ import { debugNodeId, debugNodeThread } from "../-c-identity/IdentityController"
12
12
  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
+ import { recordPathValuesSent, recordPathValuesReceived } from "../-f-node-discovery/TrafficTracking";
15
16
  import { getNodeIdIP } from "socket-function/src/nodeCache";
16
17
  import { authorityLookup } from "./AuthorityLookup";
17
18
  import { timeoutToError } from "../errors";
@@ -110,6 +111,7 @@ export class PathValueControllerBase {
110
111
  let changes = config.pathValues;
111
112
  let { nodeId, initialTriggers } = config;
112
113
  pathValueSendCount += changes.length;
114
+ recordPathValuesSent(changes.length);
113
115
  let buffers = await pathValueSerializer.serialize(changes, {
114
116
  noLocks: !config.keepLocks,
115
117
  compress: getCompressNetwork(),
@@ -146,6 +148,7 @@ export class PathValueControllerBase {
146
148
  if (valueBuffers) {
147
149
  values = await pathValueSerializer.deserialize(valueBuffers);
148
150
  ActionsHistory.OnRead(values);
151
+ recordPathValuesReceived(values.length);
149
152
  }
150
153
 
151
154
  if (initialCreation) {
@@ -199,7 +199,8 @@ export class PathValueArchives {
199
199
  // break existing servers (and it could cause security issues, etc).
200
200
  if (time < maxAttemptArchiveTime) {
201
201
  devDebugbreak();
202
- console.error(`Tried to archive a value which is too far into the past, ${time} < ${maxAttemptArchiveTime}. To commit this to disk would break locks, and could leave the database in an invalid state. Killing server so clients will resync with a server that is in a better state.`);
202
+ let pastDistance = Date.now() - time;
203
+ console.error(`Tried to archive a value which is too far into the past, ${time} < ${maxAttemptArchiveTime} (${formatTime(pastDistance)} ago, ${formatTime(maxAttemptArchiveTime - time)} beyond our limit). To commit this to disk would break locks, and could leave the database in an invalid state. Killing server so clients will resync with a server that is in a better state.`);
203
204
  process.exit();
204
205
  }
205
206
  if (time < oldestTime) oldestTime = time;
@@ -34,7 +34,7 @@ export async function startupAuthority(spec: AuthoritySpec) {
34
34
  pathValues: values,
35
35
  parentSyncs: [],
36
36
  initialTriggers: { values: new Set(), parentPaths: new Set() },
37
- doNotArchive: true,
37
+ doNotArchive: new Set(values),
38
38
  });
39
39
  console.log(blue(`Finished ingesting values from source ${source.nodeId} (values: ${formatNumber(values.length)}`), { nodeId: source.nodeId, spec: debugSpec(source), values: values.length });
40
40
  }
@@ -62,7 +62,7 @@ export async function startupAuthority(spec: AuthoritySpec) {
62
62
  let flat = Object.values(snapshot.values).flat();
63
63
  console.log(blue(`Loaded snapshot in memory, found ${formatNumber(flat.length)} values`), { spec: debugSpec(spec), values: flat.length });
64
64
  // NOTE: If it's on disk, it can't be rejected, so there's no point in passing it through our validStateComputer (and passing it through is VERY slow).
65
- authorityStorage.ingestValues(flat, { doNotArchive: true });
65
+ authorityStorage.ingestValues(flat, { doNotArchive: new Set(flat) });
66
66
  console.log(blue(`Finished ingesting values from snapshot, found ${formatNumber(flat.length)} values`), { spec: debugSpec(spec), values: flat.length });
67
67
  })();
68
68
 
@@ -1,4 +1,5 @@
1
1
  import { batchFunction, delay, runInfinitePoll } from "socket-function/src/batching";
2
+ import { recordFunctionExecuted } from "../-f-node-discovery/TrafficTracking";
2
3
  import { cache, lazy } from "socket-function/src/caching";
3
4
  import { blue, magenta, yellow } from "socket-function/src/formatting/logColors";
4
5
  import { timeInHour, timeInMinute, timeInSecond } from "socket-function/src/misc";
@@ -645,6 +646,7 @@ export class PathFunctionRunner {
645
646
  private callStats = new Map<string, CallStats>();
646
647
 
647
648
  private async runCall(callSpec: CallSpec, functionSpec: FunctionSpec): Promise<void> {
649
+ recordFunctionExecuted();
648
650
  let callId = callSpec.CallId;
649
651
  let stats = this.callStats.get(callId);
650
652
  if (!stats) {
@@ -13,6 +13,7 @@ import { t } from "../2-proxy/schema2";
13
13
  import { createLocalSchema } from "./schemaHelpers";
14
14
  import { Querysub } from "./Querysub";
15
15
  import { getAllNodeIds, getOwnNodeId, watchDeltaNodeIds } from "../-f-node-discovery/NodeDiscovery";
16
+ import { getNodeLatencyInfo } from "../-f-node-discovery/LatencyTracking";
16
17
  import { FunctionRunnerInfoController, FunctionStatsSummary } from "../3-path-functions/PathFunctionRunner";
17
18
  import { URLParam } from "../library-components/URLParam";
18
19
  import { logErrors, timeoutToUndefined, timeoutToUndefinedSilent } from "../errors";
@@ -27,8 +28,6 @@ const POLL_INTERVAL = timeInMinute;
27
28
  const INDEX_POLL_INTERVAL = timeInMinute * 5;
28
29
  // Forget runners we haven't been able to reach for this long
29
30
  const NODE_EXPIRY_TIME = POLL_INTERVAL * 5;
30
- // Rolling latency average size. Small, so latency changes show up quickly.
31
- const LATENCY_SAMPLE_LIMIT = 20;
32
31
  // Only prefer runners that have been up at least this long
33
32
  const MIN_UP_TIME = timeInMinute * 5;
34
33
  // Only weight by call time once a network has at least this much total recorded call time
@@ -179,7 +178,6 @@ async function pollFunctionRunner(nodeId: string, logWarnings?: "logWarnings" |
179
178
  // Wait a bit so the node has time to set itself up, also to stagger the polling a little bit more.
180
179
  await delay(2000 + Math.random() * 3000);
181
180
  polledNodeIds.add(nodeId);
182
- let start = Date.now();
183
181
  // Only function runners expose this, so a failed call just means the node isn't a runner
184
182
  let runnerInfoPromise = FunctionRunnerInfoController.nodes[nodeId].getRunnerInfo({
185
183
  writeNodeId: getOwnNodeId(),
@@ -198,7 +196,6 @@ async function pollFunctionRunner(nodeId: string, logWarnings?: "logWarnings" |
198
196
  }
199
197
  return;
200
198
  }
201
- let latency = Date.now() - start;
202
199
 
203
200
  let networks = new Set<string>();
204
201
  for (let shard of runnerInfo.shards) {
@@ -207,11 +204,11 @@ async function pollFunctionRunner(nodeId: string, logWarnings?: "logWarnings" |
207
204
  }
208
205
  }
209
206
 
210
- let prev = nodeInfos.get(nodeId);
211
- let sampleCount = Math.min(prev?.latencySampleCount || 0, LATENCY_SAMPLE_LIMIT);
212
207
  if (!nodeInfos.has(nodeId)) {
213
208
  console.log(green(`[${formatDateTime(Date.now())}] Function runner ${nodeId} discovered, adding it to the runner index`), { nodeId });
214
209
  }
210
+ // Latency is now owned by LatencyTracking (the single source of truth), not measured here.
211
+ let latencyInfo = getNodeLatencyInfo(nodeId);
215
212
  nodeInfos.set(nodeId, {
216
213
  nodeId,
217
214
  entryPoint: runnerInfo.entryPoint,
@@ -220,8 +217,8 @@ async function pollFunctionRunner(nodeId: string, logWarnings?: "logWarnings" |
220
217
  isPublic: runnerInfo.shards.some(x => x.isPublic),
221
218
  networks: Array.from(networks),
222
219
  shards: runnerInfo.shards,
223
- averageLatency: ((prev?.averageLatency || 0) * sampleCount + latency) / (sampleCount + 1),
224
- latencySampleCount: sampleCount + 1,
220
+ averageLatency: latencyInfo?.averageLatency ?? 0,
221
+ latencySampleCount: latencyInfo?.sampleCount ?? 0,
225
222
  scheduledShutdownTime: runnerInfo.scheduledShutdownTime,
226
223
  });
227
224
  nodeTimings.set(nodeId, runnerInfo.timings);
@@ -846,7 +846,8 @@ export class Querysub {
846
846
  }
847
847
 
848
848
  let mountedNodeId = await SocketFunction.mount({
849
- public: isPublic(),
849
+ // IMPORTANT! It really breaks our network when there's nodes that want to be on the network but don't want to listen. So everything now has to be public and has to port forward.
850
+ public: true,
850
851
  port: config.port,
851
852
  autoForwardPort: true,
852
853
  ...await getThreadKeyCert(getDomain()),
@@ -1081,7 +1082,8 @@ export class Querysub {
1081
1082
 
1082
1083
  this.socketFunctionInit();
1083
1084
  let mountPromise = SocketFunction.mount({
1084
- public: isPublic(),
1085
+ // See hostServer for why this has to be always true
1086
+ public: true,
1085
1087
  port,
1086
1088
  autoForwardPort: true,
1087
1089
  ...await getThreadKeyCert(getDomain()),
@@ -1,5 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { cache, lazy } from "socket-function/src/caching";
3
+ import { recordQuerysubCall } from "../-f-node-discovery/TrafficTracking";
3
4
  import { appendToPathStr, getPathDepth, getPathStr1, getPathStr3 } from "../path";
4
5
  import { FunctionMetadata } from "../3-path-functions/syncSchema";
5
6
  import { RemoteWatcher, remoteWatcher } from "../1-path-client/RemoteWatcher";
@@ -534,6 +535,7 @@ export class QuerysubControllerBase {
534
535
 
535
536
  // NOTE: Calls are going to be temporary and random. Any user can use any call ID, so technically you could clobber other users' call IDs, or your our. There wouldn't really be any benefit. Nothing would really happen if you do that, so I don't believe these need to be kept secret. I think if you know someone else's call ID you might be able to read that data, but also it's securely random, so you're not going to be able to guess the call ID.
536
537
  public async addCall(call: CallSpec) {
538
+ recordQuerysubCall();
537
539
  if (isBootstrapOnly()) throw new Error(`Cannot add calls to bootstrap only server`);
538
540
  if (Querysub.DEBUG_CALLS) {
539
541
  console.log(`[Querysub] addCall @${debugTime(call.runAtTime)}: ${call.DomainName}.${call.ModuleId}.${call.FunctionId}`);
@@ -308,7 +308,7 @@ function predictCallBase(config: {
308
308
  pathValues: predictions.writes,
309
309
  parentSyncs: [],
310
310
  initialTriggers: { values: new Set(), parentPaths: new Set() },
311
- doNotArchive: true,
311
+ doNotArchive: new Set(predictions.writes),
312
312
  });
313
313
 
314
314
  if (Querysub.DEBUG_PREDICTIONS) {
@@ -324,19 +324,20 @@ function predictCallBase(config: {
324
324
  didCancel = true;
325
325
  if (!predictions) return;
326
326
  // Reject our predictions, as the call likely never got committed, so it will never be written
327
+ let rejectedWrites = predictions.writes.map(write => ({
328
+ path: write.path,
329
+ value: write.value,
330
+ locks: [],
331
+ lockCount: 0,
332
+ valid: false,
333
+ time: predictResultWrite.time,
334
+ isTransparent: false,
335
+ }));
327
336
  validStateComputer.ingestValuesAndValidStates({
328
- pathValues: predictions.writes.map(write => ({
329
- path: write.path,
330
- value: write.value,
331
- locks: [],
332
- lockCount: 0,
333
- valid: false,
334
- time: predictResultWrite.time,
335
- isTransparent: false,
336
- })),
337
+ pathValues: rejectedWrites,
337
338
  parentSyncs: [],
338
339
  initialTriggers: { values: new Set(), parentPaths: new Set() },
339
- doNotArchive: true,
340
+ doNotArchive: new Set(rejectedWrites),
340
341
  });
341
342
  }
342
343
 
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig } from "../machineSchema";
3
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceController, ServiceConfig, getMachineIdList } from "../machineSchema";
4
4
  import { css } from "typesafecss";
5
5
  import { currentViewParam, selectedMachineIdParam, selectedServiceIdParam } from "../urlParams";
6
6
  import { formatNumber, formatVeryNiceDateTime } from "socket-function/src/formatting/format";
@@ -34,7 +34,7 @@ export class MachineDetailPage extends qreact.Component {
34
34
  for (let serviceId of serviceList) {
35
35
  let serviceConfig = controller.getServiceConfig(serviceId);
36
36
  if (!serviceConfig?.parameters.deploy) continue;
37
- if (serviceConfig && (serviceConfig.parameters.machineIds || []).includes(selectedMachineId)) {
37
+ if (serviceConfig && getMachineIdList(serviceConfig.parameters).includes(selectedMachineId)) {
38
38
  relevantServiceConfigs.set(serviceId, serviceConfig);
39
39
  }
40
40
  }