querysub 0.523.0 → 0.525.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.523.0",
3
+ "version": "0.525.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.12",
73
+ "socket-function": "^1.2.14",
74
74
  "terser": "^5.31.0",
75
75
  "typenode": "^6.6.1",
76
76
  "typesafecss": "^0.32.0",
@@ -19,7 +19,7 @@ import { waitForFirstTimeSync } from "socket-function/time/trueTimeShim";
19
19
  import { red } from "socket-function/src/formatting/logColors";
20
20
  import { isNode } from "typesafecss";
21
21
  import { areNodeIdsEqual, getOwnNodeId, getOwnThreadId } from "../-f-node-discovery/NodeDiscovery";
22
- import { timeInMinute } from "socket-function/src/misc";
22
+ import { timeInMinute, isIpDomain } from "socket-function/src/misc";
23
23
  import { isClient, isServer } from "../config2";
24
24
  import { getDomain } from "../config";
25
25
 
@@ -242,7 +242,7 @@ const changeIdentityOnce = cacheWeak(async function changeIdentityOnce(connectio
242
242
  certIssuer: issuer.cert.toString(),
243
243
  mountedPort: getNodeIdLocation(SocketFunction.mountedNodeId)?.port,
244
244
  debugEntryPoint: isServer() ? process.argv[1] : "browser",
245
- clientIsNode: isServer(),
245
+ clientIsNode: isServer() && !isIpDomain(nodeId),
246
246
  };
247
247
  let signature = sign(threadKeyCert, payload);
248
248
  await timeoutToError(
@@ -111,7 +111,7 @@ export async function isNodeTrusted(nodeId: string) {
111
111
  return await isTrusted(machineId);
112
112
  }
113
113
 
114
- const loadServerCert = cache(async (machineId: string) => {
114
+ export const loadServerCert = cache(async (machineId: string) => {
115
115
  // This cert isn't stored in the archives, but... this is fine?
116
116
  if (machineId === "127-0-0-1." + getDomain()) return;
117
117
  let certFile = await archives().get(machineId);
@@ -131,7 +131,7 @@ export const ensureWeAreTrusted = lazy(measureWrap(async () => {
131
131
  }
132
132
  }));
133
133
 
134
- async function loadTrustCerts(nodeId: string) {
134
+ export async function loadTrustCerts(nodeId: string) {
135
135
  let location = getNodeIdLocation(nodeId);
136
136
  if (location) {
137
137
  let machineId = getMachineId(location.address, getDomain());
@@ -43,6 +43,10 @@ export function getNodeLatency(nodeId: string): number | undefined {
43
43
  return getNodeLatencyInfo(nodeId)?.averageLatency;
44
44
  }
45
45
 
46
+ export function getCachedNodeLatencyInfoList(): Map<string, NodeLatencyInfo> {
47
+ return latencyByNode;
48
+ }
49
+
46
50
  /** Our measured latency to every node we have reached, as a plain map (for sending over the wire). */
47
51
  export function getOwnLatencies(): { [nodeId: string]: number } {
48
52
  void startLatencyTracking();
@@ -72,9 +76,11 @@ async function pingNode(nodeId: string) {
72
76
  recordLatency(nodeId, Date.now() - start);
73
77
  }
74
78
 
79
+ let firstCall = true;
75
80
  async function pollLatencies() {
76
81
  let nodeIds = (await getAllNodeIds()).filter(nodeId => !isOwnNodeId(nodeId));
77
- await spreadCallsOverTime(nodeIds, LATENCY_POLL_INTERVAL, pingNode);
82
+ let spread = firstCall ? 0 : LATENCY_POLL_INTERVAL;
83
+ await spreadCallsOverTime(nodeIds, spread, pingNode);
78
84
 
79
85
  let now = Date.now();
80
86
  for (let [nodeId, info] of Array.from(latencyByNode)) {
@@ -82,6 +88,7 @@ async function pollLatencies() {
82
88
  latencyByNode.delete(nodeId);
83
89
  }
84
90
  }
91
+ firstCall = false;
85
92
  }
86
93
 
87
94
  export const startLatencyTracking = lazy(async () => {
@@ -26,14 +26,14 @@ const BUCKET_HISTORY = BUCKET_SIZE * 20;
26
26
  // not O(client connections) — clients churn through many raw ids per hour and we never want a bucket per client.
27
27
  const OUTSIDE_KEY = "";
28
28
 
29
- export type NodeDataTraffic = { sent: number; received: number; };
29
+ export type NodeDataTraffic = { sent: number; received: number; pathValuesSent: number; pathValuesReceived: number; };
30
30
  // All numbers are PER-SECOND rates (see file header).
31
31
  export type TrafficStats = {
32
32
  functionsExecuted: number;
33
33
  pathValuesSent: number;
34
34
  pathValuesReceived: number;
35
35
  querysubCalls: number;
36
- // Byte rates to/from each known network node, plus one aggregate for everything outside the node list.
36
+ // Byte and path value rates to/from each known network node, plus one aggregate for everything outside the node list.
37
37
  perNode: { [nodeId: string]: NodeDataTraffic };
38
38
  outside: NodeDataTraffic;
39
39
  };
@@ -48,6 +48,8 @@ let querysubCalls: BucketMap = new Map();
48
48
  // raw connection nodeId => buckets
49
49
  let bytesSent = new Map<string, BucketMap>();
50
50
  let bytesReceived = new Map<string, BucketMap>();
51
+ let pathValuesSentPerNode = new Map<string, BucketMap>();
52
+ let pathValuesReceivedPerNode = new Map<string, BucketMap>();
51
53
 
52
54
  function addSimple(buckets: BucketMap, amount: number) {
53
55
  let bucket = Math.floor(Date.now() / BUCKET_SIZE) * BUCKET_SIZE;
@@ -65,13 +67,15 @@ function addKeyed(map: Map<string, BucketMap>, key: string, amount: number) {
65
67
  export function recordFunctionExecuted() {
66
68
  addSimple(functionsExecuted, 1);
67
69
  }
68
- export function recordPathValuesSent(count: number) {
69
- if (count <= 0) return;
70
- addSimple(pathValuesSent, count);
70
+ export function recordPathValuesSent(config: { count: number; nodeId: string }) {
71
+ if (config.count <= 0) return;
72
+ addSimple(pathValuesSent, config.count);
73
+ addKeyed(pathValuesSentPerNode, trafficKey(config.nodeId), config.count);
71
74
  }
72
- export function recordPathValuesReceived(count: number) {
73
- if (count <= 0) return;
74
- addSimple(pathValuesReceived, count);
75
+ export function recordPathValuesReceived(config: { count: number; nodeId: string }) {
76
+ if (config.count <= 0) return;
77
+ addSimple(pathValuesReceived, config.count);
78
+ addKeyed(pathValuesReceivedPerNode, trafficKey(config.nodeId), config.count);
75
79
  }
76
80
  export function recordQuerysubCall() {
77
81
  addSimple(querysubCalls, 1);
@@ -111,10 +115,10 @@ function bucketRate(buckets: BucketMap, windowSize: number): number {
111
115
  function aggregateData(windowSize: number): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
112
116
  let known = new Set(getCachedNodeIds());
113
117
  let perNode: { [nodeId: string]: NodeDataTraffic } = {};
114
- let outside: NodeDataTraffic = { sent: 0, received: 0 };
118
+ let outside: NodeDataTraffic = { sent: 0, received: 0, pathValuesSent: 0, pathValuesReceived: 0 };
115
119
  // At most (node count + 1) keys, so this whole pass is O(nodes). debugNodeId (a linear lookup) is only used for
116
120
  // the rare server-style id that isn't already a known node.
117
- let attribute = (rawNodeId: string, field: "sent" | "received", rate: number) => {
121
+ let attribute = (rawNodeId: string, field: keyof NodeDataTraffic, rate: number) => {
118
122
  if (rate <= 0) return;
119
123
  if (rawNodeId === OUTSIDE_KEY) {
120
124
  outside[field] += rate;
@@ -124,7 +128,7 @@ function aggregateData(windowSize: number): { perNode: { [nodeId: string]: NodeD
124
128
  if (known.has(nice)) {
125
129
  let entry = perNode[nice];
126
130
  if (!entry) {
127
- entry = { sent: 0, received: 0 };
131
+ entry = { sent: 0, received: 0, pathValuesSent: 0, pathValuesReceived: 0 };
128
132
  perNode[nice] = entry;
129
133
  }
130
134
  entry[field] += rate;
@@ -132,14 +136,16 @@ function aggregateData(windowSize: number): { perNode: { [nodeId: string]: NodeD
132
136
  outside[field] += rate;
133
137
  }
134
138
  };
135
- for (let [rawNodeId, buckets] of Array.from(bytesSent)) {
136
- attribute(rawNodeId, "sent", bucketRate(buckets, windowSize));
137
- if (buckets.size === 0) bytesSent.delete(rawNodeId);
138
- }
139
- for (let [rawNodeId, buckets] of Array.from(bytesReceived)) {
140
- attribute(rawNodeId, "received", bucketRate(buckets, windowSize));
141
- if (buckets.size === 0) bytesReceived.delete(rawNodeId);
142
- }
139
+ let aggregateMap = (map: Map<string, BucketMap>, field: keyof NodeDataTraffic) => {
140
+ for (let [rawNodeId, buckets] of Array.from(map)) {
141
+ attribute(rawNodeId, field, bucketRate(buckets, windowSize));
142
+ if (buckets.size === 0) map.delete(rawNodeId);
143
+ }
144
+ };
145
+ aggregateMap(bytesSent, "sent");
146
+ aggregateMap(bytesReceived, "received");
147
+ aggregateMap(pathValuesSentPerNode, "pathValuesSent");
148
+ aggregateMap(pathValuesReceivedPerNode, "pathValuesReceived");
143
149
  return { perNode, outside };
144
150
  }
145
151
 
@@ -22,6 +22,9 @@ export { LOCAL_DOMAIN, LOCAL_DOMAIN_PATH };
22
22
  // fine (two nearby servers just talk to each other). Replaces the old uniform-random pick between candidates.
23
23
  const LATENCY_CANDIDATE_LIMIT = 5;
24
24
  const LATENCY_WEIGHT_OFFSET = 10;
25
+ // A candidate whose weight is below (the current max weight / this factor) is dropped to zero probability. So a node
26
+ // weighted 10x another (10x lower effective latency) is picked over it 100% of the time.
27
+ const LATENCY_WEIGHT_CUTOFF_FACTOR = 10;
25
28
  // Latency assumed for a node we haven't measured yet, so new/unreached nodes stay eligible but aren't preferred.
26
29
  const UNKNOWN_LATENCY_MS = 50;
27
30
 
@@ -42,14 +45,19 @@ function latencyWeightedShuffle<T extends { nodeId: string }>(arr: T[]) {
42
45
  let rest = withLatency.slice(LATENCY_CANDIDATE_LIMIT);
43
46
  let ordered: T[] = [];
44
47
  while (pool.length > 0) {
48
+ let weights = pool.map(e => 1 / (e.latency + LATENCY_WEIGHT_OFFSET));
49
+ let cutoff = Math.max(...weights) / LATENCY_WEIGHT_CUTOFF_FACTOR;
50
+ for (let i = 0; i < weights.length; i++) {
51
+ if (weights[i] < cutoff) weights[i] = 0;
52
+ }
45
53
  let total = 0;
46
- for (let e of pool) {
47
- total += 1 / (e.latency + LATENCY_WEIGHT_OFFSET);
54
+ for (let w of weights) {
55
+ total += w;
48
56
  }
49
57
  let r = Math.random() * total;
50
58
  let idx = 0;
51
59
  while (idx < pool.length - 1) {
52
- r -= 1 / (pool[idx].latency + LATENCY_WEIGHT_OFFSET);
60
+ r -= weights[idx];
53
61
  if (r <= 0) break;
54
62
  idx++;
55
63
  }
@@ -144,7 +144,7 @@ class PathValueCommitter {
144
144
  }
145
145
 
146
146
  private broadcastValues = batchFunction(
147
- { delay: 10, throttleWindow: 500, noMeasure: true },
147
+ { delay: 1, throttleWindow: 2000, noMeasure: true },
148
148
  async function internal_forwardWrites(valuesBatched: {
149
149
  values: Set<PathValue>;
150
150
  tryCount?: number;
@@ -327,7 +327,7 @@ class PathValueCommitter {
327
327
 
328
328
 
329
329
  public ingestRemoteValuesAndValidStates = batchFunction(
330
- { delay: 16, throttleWindow: 1000, name: "ingestRemoteValuesAndValidStates", noMeasure: true },
330
+ { delay: 1, throttleWindow: 1000, name: "ingestRemoteValuesAndValidStates", noMeasure: true },
331
331
  async (batched: RemoteValueAndValidState[]) => {
332
332
  const { remoteWatcher } = await import("../1-path-client/RemoteWatcher");
333
333
 
@@ -23,11 +23,18 @@ import { encodeCborx, decodeCborx } from "../misc/cloneHelpers";
23
23
  import { debugGetAllCallFactories } from "socket-function/src/nodeCache";
24
24
  import { delay } from "socket-function/src/batching";
25
25
  import { isDefined } from "../misc";
26
+ import { measureBlock } from "socket-function/src/profiling/measure";
26
27
  export { pathValueCommitter };
27
28
 
28
29
  let pathValueSendCount = 0;
29
30
  export function getPathValueSendCount() { return pathValueSendCount; }
30
31
 
32
+ // Debug hook, called with every batch of values sendData receives, before any filtering or ingest — so tests can observe exactly when values arrive, without worrying about values being ignored or batched downstream.
33
+ let debugOnSendData: ((values: PathValue[], sourceNodeId: string) => void) | undefined;
34
+ export function setDebugOnSendData(callback: typeof debugOnSendData) {
35
+ debugOnSendData = callback;
36
+ }
37
+
31
38
  // ONLY returns non-canGCValues that are valid
32
39
  export type AuditSnapshotEntry = {
33
40
  path: string;
@@ -53,7 +60,7 @@ export class PathValueControllerBase {
53
60
  let { pathValues, nodeId } = config;
54
61
 
55
62
  pathValueSendCount += pathValues.length;
56
- let serializedValues = await pathValueSerializer.serialize(pathValues, { compress: Querysub.COMPRESS_NETWORK });
63
+ let serializedValues = await measureBlock(() => pathValueSerializer.serialize(pathValues, { compress: Querysub.COMPRESS_NETWORK }), "createValues|serialize");
57
64
  if (isDebugLogEnabled()) {
58
65
  for (let value of pathValues) {
59
66
  auditLog("SEND CREATED VALUE", {
@@ -111,11 +118,11 @@ export class PathValueControllerBase {
111
118
  let changes = config.pathValues;
112
119
  let { nodeId, initialTriggers } = config;
113
120
  pathValueSendCount += changes.length;
114
- recordPathValuesSent(changes.length);
115
- let buffers = await pathValueSerializer.serialize(changes, {
121
+ recordPathValuesSent({ count: changes.length, nodeId });
122
+ let buffers = await measureBlock(() => pathValueSerializer.serialize(changes, {
116
123
  noLocks: !config.keepLocks,
117
124
  compress: getCompressNetwork(),
118
- });
125
+ }), "sendValues|serialize");
119
126
  this.logSendValues({ nodeId, pathValues: changes, initialTriggers, reason: config.reason });
120
127
  return await PathValueController.nodes[nodeId].sendData({
121
128
  valueBuffers: buffers,
@@ -137,7 +144,7 @@ export class PathValueControllerBase {
137
144
 
138
145
  }): Promise<void | "refused"> {
139
146
  let callerId = SocketFunction.getCaller().nodeId;
140
- let { initialCreation, valueBuffers } = config;
147
+ const { initialCreation, valueBuffers } = config;
141
148
 
142
149
  let slowdown = getSlowdown();
143
150
  if (slowdown) {
@@ -146,28 +153,34 @@ export class PathValueControllerBase {
146
153
 
147
154
  let values: PathValue[] = [];
148
155
  if (valueBuffers) {
149
- values = await pathValueSerializer.deserialize(valueBuffers);
156
+ values = await measureBlock(() => pathValueSerializer.deserialize(valueBuffers), "sendData|deserialize");
150
157
  ActionsHistory.OnRead(values);
151
- recordPathValuesReceived(values.length);
158
+ recordPathValuesReceived({ count: values.length, nodeId: callerId });
159
+ }
160
+ if (debugOnSendData) {
161
+ debugOnSendData(values, callerId);
152
162
  }
153
163
 
154
164
  if (initialCreation) {
155
- let sourceNodeId = debugNodeId(callerId);
156
- let rejected = false;
157
- let threshold = Date.now() - MAX_CHANGE_AGE;
158
- for (let value of values) {
159
- let pastThreshold = threshold - value.time.time;
160
- if (pastThreshold > 0) {
161
- rejected = true;
162
- console.error(`Rejecting values as one is too old. It likely got caught in the pipeline too long and now can't be committed without trying to undone history which can already been committed to disk.`, {
163
- path: value.path,
164
- timeId: value.time.time,
165
- sourceNodeId,
166
- sourceNodeThreadId: decodeNodeId(sourceNodeId, getDomain())?.threadId,
167
- totalValueCount: values.length,
168
- });
165
+ let rejected = measureBlock(() => {
166
+ let sourceNodeId = debugNodeId(callerId);
167
+ let anyRejected = false;
168
+ let threshold = Date.now() - MAX_CHANGE_AGE;
169
+ for (let value of values) {
170
+ let pastThreshold = threshold - value.time.time;
171
+ if (pastThreshold > 0) {
172
+ anyRejected = true;
173
+ console.error(`Rejecting values as one is too old. It likely got caught in the pipeline too long and now can't be committed without trying to undone history which can already been committed to disk.`, {
174
+ path: value.path,
175
+ timeId: value.time.time,
176
+ sourceNodeId,
177
+ sourceNodeThreadId: decodeNodeId(sourceNodeId, getDomain())?.threadId,
178
+ totalValueCount: values.length,
179
+ });
180
+ }
169
181
  }
170
- }
182
+ return anyRejected;
183
+ }, "sendData|ageCheck");
171
184
  if (rejected) {
172
185
  return "refused";
173
186
  }
@@ -189,7 +202,7 @@ export class PathValueControllerBase {
189
202
  }
190
203
 
191
204
  // Note which values are genuinely new to us BEFORE ingesting. We only re-share new values with the other authorities below; if we already had a value we already shared it when we first received it, so re-sharing is redundant and can clobber a newer value with an older copy.
192
- let valuesNewToUs = config.initialCreation && values.filter(value => !authorityStorage.getValueExactMaybeRejected(value.path, value.time)) || [];
205
+ let valuesNewToUs = measureBlock(() => config.initialCreation && values.filter(value => !authorityStorage.getValueExactMaybeRejected(value.path, value.time)) || [], "sendData|newToUsCheck");
193
206
 
194
207
  try {
195
208
  let initialTriggers = config.initialTriggers || { values: new Set(), parentPaths: new Set() };
@@ -205,7 +218,8 @@ export class PathValueControllerBase {
205
218
 
206
219
  if (config.initialCreation && valuesNewToUs.length > 0) {
207
220
  // Always shared the latest. If we don't have it, maybe something with ingestion delayed, we should still share it so we don't lose the value. If we haven't even ingested it, then it is our latest version of the value.
208
- valuesNewToUs = valuesNewToUs.map(value => authorityStorage.getValueExactMaybeRejected(value.path, value.time) || value);
221
+ valuesNewToUs = measureBlock(() => valuesNewToUs.map(value => authorityStorage.getValueExactMaybeRejected(value.path, value.time) || value), "sendData|newToUsRemap");
222
+ // Not measured: this waits on network sends to the other authorities.
209
223
  await PathValueControllerBase.authorityShareValues({ pathValues: valuesNewToUs });
210
224
  }
211
225
  }
@@ -242,13 +256,13 @@ export class PathValueControllerBase {
242
256
  auditLog("WATCH PARENT PATH", { path: value, sourceNodeId });
243
257
  }
244
258
  }
245
- pathWatcher.watchPath({
259
+ measureBlock(() => pathWatcher.watchPath({
246
260
  nodeId: callerId,
247
261
  paths: config.paths,
248
262
  parentPaths: config.parentPaths,
249
263
  initialTrigger: true,
250
264
  fullHistory: config.fullHistory,
251
- });
265
+ }), "watchLatest|watchPath");
252
266
  }
253
267
  public async unwatchLatest(config: WatchConfig) {
254
268
  let callerId = SocketFunction.getCaller().nodeId;
@@ -261,7 +275,7 @@ export class PathValueControllerBase {
261
275
  auditLog("UNWATCHING PARENT PATH", { path: value, sourceNodeId });
262
276
  }
263
277
  }
264
- pathWatcher.unwatchPath({ paths: config.paths, parentPaths: config.parentPaths, callback: callerId, reason: "PathValueController.unwatchLatest" });
278
+ measureBlock(() => pathWatcher.unwatchPath({ paths: config.paths, parentPaths: config.parentPaths, callback: callerId, reason: "PathValueController.unwatchLatest" }), "unwatchLatest|unwatchPath");
265
279
  }
266
280
 
267
281
  public static async getInitialValues(config: {
@@ -281,11 +295,11 @@ export class PathValueControllerBase {
281
295
  startTime: number;
282
296
  endTime: number;
283
297
  }): Promise<Buffer[]> {
284
- let values = authorityStorage.getAllValues(config);
285
- let buffers = await pathValueSerializer.serialize(values, {
298
+ let values = measureBlock(() => authorityStorage.getAllValues(config), "getInitialValues|getAllValues");
299
+ let buffers = await measureBlock(() => pathValueSerializer.serialize(values, {
286
300
  noLocks: true,
287
301
  compress: getCompressNetwork(),
288
- });
302
+ }), "getInitialValues|serialize");
289
303
  return buffers;
290
304
  }
291
305
 
@@ -304,23 +318,25 @@ export class PathValueControllerBase {
304
318
  let { spec } = config;
305
319
  let { compareTime } = await import("./pathValueCore");
306
320
 
307
- let allValues = authorityStorage.getAllValues({ spec, startTime: 0, endTime: Number.MAX_SAFE_INTEGER });
321
+ return measureBlock(() => {
322
+ let allValues = authorityStorage.getAllValues({ spec, startTime: 0, endTime: Number.MAX_SAFE_INTEGER });
308
323
 
309
- let pathToLatest = new Map<string, Time>();
310
- for (let value of allValues) {
311
- if (!value.valid) continue;
312
- if (value.canGCValue) continue;
324
+ let pathToLatest = new Map<string, Time>();
325
+ for (let value of allValues) {
326
+ if (!value.valid) continue;
327
+ if (value.canGCValue) continue;
313
328
 
314
- let existing = pathToLatest.get(value.path);
315
- if (!existing || compareTime(value.time, existing) > 0) {
316
- pathToLatest.set(value.path, value.time);
329
+ let existing = pathToLatest.get(value.path);
330
+ if (!existing || compareTime(value.time, existing) > 0) {
331
+ pathToLatest.set(value.path, value.time);
332
+ }
317
333
  }
318
- }
319
334
 
320
- let entries: AuditSnapshotEntry[] = Array.from(pathToLatest.entries()).map(([path, time]) => ({ path, time }));
335
+ let entries: AuditSnapshotEntry[] = Array.from(pathToLatest.entries()).map(([path, time]) => ({ path, time }));
321
336
 
322
- let encoded = encodeCborx(entries);
323
- return LZ4.compress(encoded);
337
+ let encoded = encodeCborx(entries);
338
+ return LZ4.compress(encoded);
339
+ }, "getAuditSnapshot|local");
324
340
  }
325
341
 
326
342
 
@@ -333,19 +349,22 @@ export class PathValueControllerBase {
333
349
  }
334
350
 
335
351
  public async getValuesByPathAndTime(entries: AuditSnapshotEntry[]): Promise<Buffer[]> {
336
- let values: PathValue[] = [];
337
- for (let entry of entries) {
338
- let value = authorityStorage.getValueExactMaybeRejected(entry.path, entry.time);
339
- if (!value) continue;
340
- if (!value.valid) continue;
341
- if (value.isTransparent) continue;
342
- if (value.canGCValue) continue;
343
- values.push(value);
344
- }
345
- let buffers = await pathValueSerializer.serialize(values, {
352
+ let values = measureBlock(() => {
353
+ let matched: PathValue[] = [];
354
+ for (let entry of entries) {
355
+ let value = authorityStorage.getValueExactMaybeRejected(entry.path, entry.time);
356
+ if (!value) continue;
357
+ if (!value.valid) continue;
358
+ if (value.isTransparent) continue;
359
+ if (value.canGCValue) continue;
360
+ matched.push(value);
361
+ }
362
+ return matched;
363
+ }, "getValuesByPathAndTime|gather");
364
+ let buffers = await measureBlock(() => pathValueSerializer.serialize(values, {
346
365
  noLocks: true,
347
366
  compress: getCompressNetwork(),
348
- });
367
+ }), "getValuesByPathAndTime|serialize");
349
368
  return buffers;
350
369
  }
351
370
  }
@@ -367,6 +386,7 @@ export const PathValueController = SocketFunction.register(
367
386
  hooks: [requiresNetworkTrustHook],
368
387
  }),
369
388
  {
370
- noFunctionMeasure: !isNode(),
389
+ // Most of these functions spend their time waiting on batching or remote calls, so auto-measuring them is misleading — instead the local work inside them is wrapped in individual measureBlocks.
390
+ noFunctionMeasure: true,
371
391
  }
372
392
  );
@@ -2,7 +2,7 @@ import { keyByArray, binarySearchIndex } from "socket-function/src/misc";
2
2
  import { measureFnc } from "socket-function/src/profiling/measure";
3
3
  import { isNode } from "typesafecss";
4
4
  import { isDiskAudit } from "../config";
5
- import { getParentPathStr, getPathFromStr } from "../path";
5
+ import { getParentPathStr, getPathFromStr, getPathStr4 } from "../path";
6
6
  import { auditLog } from "./auditLogs";
7
7
  import { PathRouter } from "./PathRouter";
8
8
  import { PathValue, authorityStorage, compareTime, debugPathValuePath, ReadLock, byLockGroup, isCoreQuiet, debugRejections, debugTime, debugPathValue, MAX_CHANGE_AGE, createMissingEpochValue, Time, MISSING_TRANSACTION_PART_TIMEOUT, isOurPrediction, DEFER_LOCK_WINDOW } from "./pathValueCore";
@@ -21,8 +21,9 @@ class ValidStateComputer {
21
21
  parentSyncs: { parentPath: string; sourceNodeId: string }[];
22
22
  initialTriggers: { values: Set<string>; parentPaths: Set<string> };
23
23
  doNotArchive?: boolean;
24
+ forceValidStates?: boolean;
24
25
  }) {
25
- let { pathValues, parentSyncs, doNotArchive } = config;
26
+ let { pathValues, parentSyncs, doNotArchive, forceValidStates } = config;
26
27
  let initialTriggers = { ...config.initialTriggers, initialTriggerNonHistoryWatchers: new Set<string>() };
27
28
 
28
29
  // TODO: We might want to add back optimizations for "no watches and no locks"?
@@ -30,6 +31,13 @@ class ValidStateComputer {
30
31
 
31
32
  let now = Date.now();
32
33
 
34
+ let forcedValidStates = new Map<string, boolean>();
35
+ if (forceValidStates) {
36
+ for (let value of pathValues) {
37
+ forcedValidStates.set(getForcedValidStateKey(value), !!value.valid);
38
+ }
39
+ }
40
+
33
41
  authorityStorage.addParentSyncs(parentSyncs);
34
42
 
35
43
  // Dedup by (path, time): if multiple values for the same (path, time) arrive in this batch,
@@ -216,6 +224,7 @@ class ValidStateComputer {
216
224
  Array.from(dependenciesChanged),
217
225
  now,
218
226
  initialPrevValidStates,
227
+ forcedValidStates,
219
228
  );
220
229
  let validStateChanged = result.changed;
221
230
  for (let d of result.deferred) {
@@ -319,6 +328,7 @@ class ValidStateComputer {
319
328
  valuePaths: PathValue[],
320
329
  now: number,
321
330
  prevValidStates: Map<PathValue, boolean | undefined>,
331
+ forcedValidStates?: Map<string, boolean>,
322
332
  ): { changed: PathValue[]; deferred: PathValue[] } {
323
333
  let changed: PathValue[] = [];
324
334
  let deferred: PathValue[] = [];
@@ -404,6 +414,11 @@ class ValidStateComputer {
404
414
 
405
415
  for (let pathValue of valueGroup) {
406
416
  let prevValidState = prevValidStates.get(pathValue);
417
+ let newValid: boolean = valid;
418
+ let forcedValid = forcedValidStates?.get(getForcedValidStateKey(pathValue));
419
+ if (forcedValid !== undefined) {
420
+ newValid = forcedValid;
421
+ }
407
422
  /* We used to log these values until 2026 June 28th 6:30 am
408
423
  if (valid && prevValidState === false) {
409
424
  console.info(`Accepting value that was previously rejected`, {
@@ -428,31 +443,32 @@ class ValidStateComputer {
428
443
  }
429
444
  */
430
445
  // IMPORTANT! This means if it didn't previously exist and it's presently rejected, we count that as a change, as it this is going from undefined to false. This is actually very useful, as a lot of places want to know if a write is rejected, so they can display it in the UI for the developer.
431
- if (valid === prevValidState && pathValue.valid === valid) continue;
446
+ if (newValid === prevValidState && pathValue.valid === newValid) continue;
432
447
 
433
448
  // NOTE: We might remove this logging later as it's pretty heavy, but right now we still have bugs somewhere here.
434
449
  {
435
- console.info(`Changed valid state ${JSON.stringify(prevValidState)} to ${JSON.stringify(valid)} (and path is ${JSON.stringify(pathValue.valid)})`, {
450
+ console.info(`Changed valid state ${JSON.stringify(prevValidState)} to ${JSON.stringify(newValid)} (and path is ${JSON.stringify(pathValue.valid)})`, {
436
451
  path: pathValue.path,
437
452
  timeId: pathValue.time.time,
438
453
  timeIdFull: pathValue.time,
439
- valid,
454
+ valid: newValid,
455
+ forced: forcedValid !== undefined,
440
456
  prevValidState,
441
457
  });
442
458
  }
443
459
 
444
460
  changed.push(pathValue);
445
461
 
446
- // NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it.
462
+ // NOTE: This should be the only place we set it, and the read-only flag is only for us to prevent anyone from setting it. Anyone who sets the valid state anywhere else will be absolutely fired.
447
463
  // @ts-expect-error
448
- pathValue.valid = valid;
464
+ pathValue.valid = newValid;
449
465
 
450
466
  // HACK: There are times when we might be evaluating the same path value multiple times at once (ex, multiple initial syncs at once). In which case we might not equal the stored path value, so we need to forcefully update the valid state of it.
451
467
  let storedPathValue = authorityStorage.getValueExactMaybeRejected(pathValue.path, pathValue.time);
452
468
  // I think this is fine. I think it happens if we receive it multiple times due to sharding. It's a bit inefficient, but... SHOULD be fine.
453
469
  if (storedPathValue && pathValue !== storedPathValue) {
454
470
  // @ts-expect-error
455
- storedPathValue.valid = valid;
471
+ storedPathValue.valid = newValid;
456
472
  }
457
473
  }
458
474
  }
@@ -541,3 +557,7 @@ class ValidStateComputer {
541
557
  }
542
558
  }
543
559
  export const validStateComputer = new ValidStateComputer();
560
+
561
+ function getForcedValidStateKey(pathValue: { time: Time; path: string }): string {
562
+ return getPathStr4(pathValue.path, String(pathValue.time.time), String(pathValue.time.version), String(pathValue.time.creatorId));
563
+ }
@@ -656,6 +656,19 @@ export class RemoteWatcher {
656
656
  return Array.from(new Set(this.remoteWatchPaths.values()));
657
657
  }
658
658
 
659
+ public getWatchedPathCountsPerNodeId(): Map<string, number> {
660
+ let counts = new Map<string, number>();
661
+ for (let nodeId of this.remoteWatchPaths.values()) {
662
+ counts.set(nodeId, (counts.get(nodeId) || 0) + 1);
663
+ }
664
+ for (let watchObj of this.remoteWatchParents2.values()) {
665
+ for (let range of watchObj.ranges) {
666
+ counts.set(range.authorityId, (counts.get(range.authorityId) || 0) + 1);
667
+ }
668
+ }
669
+ return counts;
670
+ }
671
+
659
672
 
660
673
 
661
674
  public async refreshAllWatches(authorityNodeId: string) {
@@ -1,5 +1,5 @@
1
1
  import { batchFunction, delay, runInfinitePoll } from "socket-function/src/batching";
2
- import { recordFunctionExecuted } from "../-f-node-discovery/TrafficTracking";
2
+ import { recordFunctionExecuted, recordQuerysubCall } from "../-f-node-discovery/TrafficTracking";
3
3
  import { cache, lazy } from "socket-function/src/caching";
4
4
  import { blue, magenta, yellow } from "socket-function/src/formatting/logColors";
5
5
  import { timeInHour, timeInMinute, timeInSecond } from "socket-function/src/misc";
@@ -56,6 +56,9 @@ setImmediate(() => {
56
56
  });
57
57
 
58
58
  export function commitCall(call: CallSpec) {
59
+ // Counted here (not in QuerysubController.addCall) so trusted nodes' direct writes are counted too — both the
60
+ // remote addCall path and the direct-write path funnel through here.
61
+ recordQuerysubCall();
59
62
  if (!call.network) {
60
63
  throw new Error(`Call has no network, so it would never run (no FunctionRunner would pick it up). Call: ${debugCallSpec(call)}`);
61
64
  }
@@ -357,7 +357,7 @@ async function edgeNodeFunction(config: {
357
357
  // Probes ALL the given nodes (even private / non-live ones), writing latencies to globalThis.EDGE_NODE_STATS (so the edge node dropdown can show them), and returns a latency-weighted random pick among `pickableHosts` (undefined if none respond).
358
358
  async function probeAndPick(probeNodes: EdgeNodeConfig[], pickableHosts: Set<string>): Promise<EdgeNodeConfig | undefined> {
359
359
  // Having this much lower latency than another node means we pick it 100% of the time (the weight scales linearly, from 1 at the best latency down to 0 at the best latency + this)
360
- const LATENCY_WEIGHT_WINDOW = 1000;
360
+ const LATENCY_WEIGHT_WINDOW = 300;
361
361
  // How many probes are in flight at once. The first batch fires simultaneously, so anything that responds more than LATENCY_WEIGHT_WINDOW after the first response has a latency at least that much higher than it, and so would never have been picked anyway.
362
362
  const PROBE_BATCH_SIZE = 5;
363
363
 
@@ -1,6 +1,5 @@
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";
4
3
  import { appendToPathStr, getPathDepth, getPathStr1, getPathStr3 } from "../path";
5
4
  import { FunctionMetadata } from "../3-path-functions/syncSchema";
6
5
  import { RemoteWatcher, remoteWatcher } from "../1-path-client/RemoteWatcher";
@@ -535,7 +534,6 @@ export class QuerysubControllerBase {
535
534
 
536
535
  // 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.
537
536
  public async addCall(call: CallSpec) {
538
- recordQuerysubCall();
539
537
  if (isBootstrapOnly()) throw new Error(`Cannot add calls to bootstrap only server`);
540
538
  if (Querysub.DEBUG_CALLS) {
541
539
  console.log(`[Querysub] addCall @${debugTime(call.runAtTime)}: ${call.DomainName}.${call.ModuleId}.${call.FunctionId}`);