querysub 0.520.0 → 0.522.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.
@@ -2,8 +2,6 @@
2
2
 
3
3
  process.argv.push("--nobreak");
4
4
  process.argv.push("--public");
5
- process.argv.push("--network");
6
- process.argv.push("public");
7
5
 
8
6
  require("typenode");
9
7
  require("../src/3-path-functions/PathFunctionRunnerMain");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.520.0",
3
+ "version": "0.522.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.11",
73
+ "socket-function": "^1.2.12",
74
74
  "terser": "^5.31.0",
75
75
  "typenode": "^6.6.1",
76
76
  "typesafecss": "^0.32.0",
@@ -2,9 +2,13 @@
2
2
  Per-server, in-memory traffic counters (never written to the database). Everything is just an integer increment
3
3
  into a 5-minute bucket; we keep the last 60 minutes (12 buckets) and drop older ones, so memory is bounded.
4
4
 
5
+ Reads return PER-SECOND RATES, not totals. Each bucket also records the timestamp it first received data, so the
6
+ rate window runs from the oldest retained bucket's first data up to now — which stays accurate right after startup
7
+ (when only a partial window of data exists) instead of dividing a small burst by the full 60-minute window.
8
+
5
9
  Byte traffic comes from SocketFunction.trackMessageSizes (upload/download per connection). The connection nodeId is
6
10
  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
11
+ path is just a Map increment keyed by the raw id. At read we split traffic into per-known-node rates and a single
8
12
  "outside the network" aggregate for anything not in our cached node list (clients, etc.).
9
13
  */
10
14
 
@@ -17,21 +21,28 @@ import { debugNodeId } from "../-c-identity/IdentityController";
17
21
 
18
22
  const BUCKET_MS = timeInMinute * 5;
19
23
  const BUCKET_COUNT = 12;
24
+ // Floor on the rate window so a burst in the first fraction of a second after startup can't divide out to an absurd rate.
25
+ const MIN_WINDOW_SECONDS = 1;
20
26
  // Sentinel key for traffic that isn't a network node (client connections, etc.). Aggregated so memory stays O(nodes),
21
27
  // not O(client connections) — clients churn through many raw ids per hour and we never want a bucket per client.
22
28
  const OUTSIDE_KEY = "";
23
29
 
24
30
  export type NodeDataTraffic = { sent: number; received: number; };
31
+ // All numbers are PER-SECOND rates (see file header).
25
32
  export type TrafficStats = {
26
33
  functionsExecuted: number;
27
34
  pathValuesSent: number;
28
35
  pathValuesReceived: number;
29
36
  querysubCalls: number;
30
- // Bytes to/from each known network node, plus one aggregate for everything outside the node list.
37
+ // Byte rates to/from each known network node, plus one aggregate for everything outside the node list.
31
38
  perNode: { [nodeId: string]: NodeDataTraffic };
32
39
  outside: NodeDataTraffic;
33
40
  };
34
41
 
42
+ // count = amount accumulated in the bucket; firstMs = when the bucket first received data (for the rate window).
43
+ type Bucket = { count: number; firstMs: number; };
44
+ type BucketMap = Map<number, Bucket>;
45
+
35
46
  function currentBucket() {
36
47
  return Math.floor(Date.now() / BUCKET_MS);
37
48
  }
@@ -39,20 +50,24 @@ function oldestBucket() {
39
50
  return currentBucket() - BUCKET_COUNT + 1;
40
51
  }
41
52
 
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>>();
53
+ let functionsExecuted: BucketMap = new Map();
54
+ let pathValuesSent: BucketMap = new Map();
55
+ let pathValuesReceived: BucketMap = new Map();
56
+ let querysubCalls: BucketMap = new Map();
57
+ // raw connection nodeId => buckets
58
+ let bytesSent = new Map<string, BucketMap>();
59
+ let bytesReceived = new Map<string, BucketMap>();
50
60
 
51
- function addSimple(buckets: Map<number, number>, amount: number) {
61
+ function addSimple(buckets: BucketMap, amount: number) {
52
62
  let bucket = currentBucket();
53
- buckets.set(bucket, (buckets.get(bucket) || 0) + amount);
63
+ let entry = buckets.get(bucket);
64
+ if (!entry) {
65
+ entry = { count: 0, firstMs: Date.now() };
66
+ buckets.set(bucket, entry);
67
+ }
68
+ entry.count += amount;
54
69
  }
55
- function addKeyed(map: Map<string, Map<number, number>>, key: string, amount: number) {
70
+ function addKeyed(map: Map<string, BucketMap>, key: string, amount: number) {
56
71
  let buckets = map.get(key);
57
72
  if (!buckets) {
58
73
  buckets = new Map();
@@ -84,17 +99,23 @@ function trafficKey(nodeId: string): string {
84
99
  SocketFunction.trackMessageSizes.upload.push((size, nodeId) => addKeyed(bytesSent, trafficKey(nodeId), size));
85
100
  SocketFunction.trackMessageSizes.download.push((size, nodeId) => addKeyed(bytesReceived, trafficKey(nodeId), size));
86
101
 
87
- function sumRecent(buckets: Map<number, number>): number {
102
+ // Per-second rate over the retained window: total accumulated / (now - oldest retained bucket's first-data time).
103
+ // Also prunes expired buckets as it goes.
104
+ function bucketRate(buckets: BucketMap): number {
88
105
  let oldest = oldestBucket();
89
106
  let total = 0;
90
- for (let [bucket, count] of Array.from(buckets)) {
107
+ let firstMs = Infinity;
108
+ for (let [bucket, entry] of Array.from(buckets)) {
91
109
  if (bucket < oldest) {
92
110
  buckets.delete(bucket);
93
111
  continue;
94
112
  }
95
- total += count;
113
+ total += entry.count;
114
+ if (entry.firstMs < firstMs) firstMs = entry.firstMs;
96
115
  }
97
- return total;
116
+ if (total <= 0 || !Number.isFinite(firstMs)) return 0;
117
+ let windowSeconds = Math.max(MIN_WINDOW_SECONDS, (Date.now() - firstMs) / 1000);
118
+ return total / windowSeconds;
98
119
  }
99
120
 
100
121
  function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outside: NodeDataTraffic; } {
@@ -103,10 +124,10 @@ function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outs
103
124
  let outside: NodeDataTraffic = { sent: 0, received: 0 };
104
125
  // At most (node count + 1) keys, so this whole pass is O(nodes). debugNodeId (a linear lookup) is only used for
105
126
  // 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;
127
+ let attribute = (rawNodeId: string, field: "sent" | "received", rate: number) => {
128
+ if (rate <= 0) return;
108
129
  if (rawNodeId === OUTSIDE_KEY) {
109
- outside[field] += amount;
130
+ outside[field] += rate;
110
131
  return;
111
132
  }
112
133
  let nice = known.has(rawNodeId) ? rawNodeId : debugNodeId(rawNodeId);
@@ -116,17 +137,17 @@ function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outs
116
137
  entry = { sent: 0, received: 0 };
117
138
  perNode[nice] = entry;
118
139
  }
119
- entry[field] += amount;
140
+ entry[field] += rate;
120
141
  } else {
121
- outside[field] += amount;
142
+ outside[field] += rate;
122
143
  }
123
144
  };
124
145
  for (let [rawNodeId, buckets] of Array.from(bytesSent)) {
125
- attribute(rawNodeId, "sent", sumRecent(buckets));
146
+ attribute(rawNodeId, "sent", bucketRate(buckets));
126
147
  if (buckets.size === 0) bytesSent.delete(rawNodeId);
127
148
  }
128
149
  for (let [rawNodeId, buckets] of Array.from(bytesReceived)) {
129
- attribute(rawNodeId, "received", sumRecent(buckets));
150
+ attribute(rawNodeId, "received", bucketRate(buckets));
130
151
  if (buckets.size === 0) bytesReceived.delete(rawNodeId);
131
152
  }
132
153
  return { perNode, outside };
@@ -135,10 +156,10 @@ function aggregateData(): { perNode: { [nodeId: string]: NodeDataTraffic }; outs
135
156
  export function getTrafficStats(): TrafficStats {
136
157
  let data = aggregateData();
137
158
  return {
138
- functionsExecuted: sumRecent(functionsExecuted),
139
- pathValuesSent: sumRecent(pathValuesSent),
140
- pathValuesReceived: sumRecent(pathValuesReceived),
141
- querysubCalls: sumRecent(querysubCalls),
159
+ functionsExecuted: bucketRate(functionsExecuted),
160
+ pathValuesSent: bucketRate(pathValuesSent),
161
+ pathValuesReceived: bucketRate(pathValuesReceived),
162
+ querysubCalls: bucketRate(querysubCalls),
142
163
  perNode: data.perNode,
143
164
  outside: data.outside,
144
165
  };
@@ -21,7 +21,7 @@ import { getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
21
21
  import { getScheduledShutdownTime } from "../-g-core-values/scheduledShutdown";
22
22
  import { SocketFunction } from "socket-function/SocketFunction";
23
23
  import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
24
- import { getDomain, isPublic, DEFAULT_NETWORK } from "../config";
24
+ import { getDomain, isPublic } from "../config";
25
25
  import { getGitRefSync, getGitURLSync } from "../4-deploy/git";
26
26
  import type { DeployProgress } from "../4-deploy/deployFunctions";
27
27
  import { getRoutingOverride, getRoutingOverridePart } from "../0-path-value-core/PathRouterRouteOverride";
@@ -363,14 +363,15 @@ export class PathFunctionRunner {
363
363
  secondaryShardRange?: { startFraction: number, endFraction: number };
364
364
  PermissionsChecker: PermissionsCheckType | undefined;
365
365
  // The networks we listen to calls on. Unset is equivalent to ["default"].
366
- networks?: string[];
366
+ networks: string[];
367
367
  }) {
368
+ if (!config.networks.length) throw new Error(`networks is required`);
368
369
  PathFunctionRunner.hostControllers();
369
370
  debugFunctionRunnerShards.push({
370
371
  domainName: config.domainName,
371
372
  shardRange: config.shardRange,
372
373
  secondaryShardRange: config.secondaryShardRange,
373
- networks: config.networks && config.networks.length > 0 && config.networks || [DEFAULT_NETWORK],
374
+ networks: config.networks,
374
375
  isPublic: isPublic(),
375
376
  });
376
377
  logErrors(this.startWatching());
@@ -401,7 +402,7 @@ export class PathFunctionRunner {
401
402
 
402
403
  let outstandingCalls = 0;
403
404
 
404
- let networks = this.config.networks && this.config.networks.length > 0 && this.config.networks || [DEFAULT_NETWORK];
405
+ let networks = this.config.networks;
405
406
 
406
407
  let watchModuleCalls = cache((moduleId: string) => {
407
408
  for (let network of networks) {
@@ -691,7 +692,7 @@ export class PathFunctionRunner {
691
692
  }
692
693
 
693
694
  let callNetwork = callSpec.network;
694
- let ourNetworks = this.config.networks && this.config.networks.length > 0 && this.config.networks || [DEFAULT_NETWORK];
695
+ let ourNetworks = this.config.networks;
695
696
  if (!ourNetworks.includes(callNetwork)) {
696
697
  return;
697
698
  }
@@ -15,7 +15,7 @@ import { SocketFunction } from "socket-function/SocketFunction";
15
15
  import { getThreadKeyCert } from "sliftutils/misc/https/certs";
16
16
  import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
17
17
  import { timeInMinute } from "socket-function/src/misc";
18
- import { getDomain, getNetworks, isLocal, isPublic, DEFAULT_NETWORK } from "../config";
18
+ import { getDomain, getNetworks, isLocal, isPublic } from "../config";
19
19
  import { green, magenta } from "socket-function/src/formatting/logColors";
20
20
  import path from "path";
21
21
  import { IndexedLogs } from "../diagnostics/logs/IndexedLogs/IndexedLogs";
@@ -32,6 +32,9 @@ async function main() {
32
32
  // ClientWatcher.DEBUG_TRIGGERS = "heavy";
33
33
  // authorityStorage.DEBUG_UNWATCH = true;
34
34
 
35
+ let networks = getNetworks();
36
+ if (!networks.length) throw new Error(`No networks found. Use --network to specify the networks this function runner should listen on.`);
37
+
35
38
  PathFunctionRunner.DEBUG_CALLS = true;
36
39
  // debugCoreMode();
37
40
 
@@ -60,8 +63,6 @@ async function main() {
60
63
  console.log(green(`Sharding from ${shardStart} to ${shardEnd}`));
61
64
  }
62
65
 
63
- let networks = getNetworks();
64
-
65
66
  new PathFunctionRunner({
66
67
  domainName: getDomain(),
67
68
  shardRange: { startFraction: shardStart, endFraction: shardEnd },
@@ -76,9 +77,5 @@ async function main() {
76
77
  let deployPath = path.resolve("./deploy.ts");
77
78
  await import(deployPath);
78
79
  }
79
-
80
- if (networks.length !== 1 || networks[0] !== DEFAULT_NETWORK) {
81
- console.log(magenta(`Only running functions on the network(s): ${networks.join(", ")}. Use --network ${networks[0]} in your http server to route calls here.`));
82
- }
83
80
  }
84
81
  logErrors(main());
package/src/config.ts CHANGED
@@ -44,8 +44,6 @@ if (isNode()) {
44
44
  }
45
45
  }
46
46
 
47
- export const DEFAULT_NETWORK = "default";
48
-
49
47
  export function expandHomePath(path: string): string {
50
48
  if (path === "~" || path.startsWith("~/") || path.startsWith("~\\")) {
51
49
  return os.homedir() + path.slice(1);
@@ -76,7 +74,7 @@ export function getNetworks(): string[] {
76
74
  result = [...result, ...networkFileNetworks()];
77
75
  }
78
76
  result = Array.from(new Set(result));
79
- if (result.length === 0) return [DEFAULT_NETWORK];
77
+ if (result.length === 0) return [];
80
78
  return result;
81
79
  }
82
80
 
@@ -169,20 +169,24 @@ export class ServiceDetailPage extends qreact.Component {
169
169
  private renderTemplateVariables(config: ServiceConfig) {
170
170
  let templateVariables = getCommandTemplateVariables(config.parameters.command);
171
171
  let targets = getMachineTargets(config.parameters);
172
- let anyVariablesSet = targets.some(target => Object.keys(target.variables).length > 0);
173
- if (templateVariables.length === 0 && !anyVariablesSet) return undefined;
172
+ if (targets.length === 0) return undefined;
174
173
 
175
- const setVariable = (entryIndex: number, name: string, value: string) => {
174
+ const updateTargets = (change: (targets: ReturnType<typeof getMachineTargets>) => void) => {
176
175
  let updated = deepCloneJSON(config);
177
176
  let updatedTargets = getMachineTargets(updated.parameters);
178
- if (value) {
179
- updatedTargets[entryIndex].variables[name] = value;
180
- } else {
181
- delete updatedTargets[entryIndex].variables[name];
182
- }
177
+ change(updatedTargets);
183
178
  setMachineTargets(updated.parameters, updatedTargets);
184
179
  this.updateEditorState(updated);
185
180
  };
181
+ const setVariable = (entryIndex: number, name: string, value: string) => {
182
+ updateTargets(targets => {
183
+ if (value) {
184
+ targets[entryIndex].variables[name] = value;
185
+ } else {
186
+ delete targets[entryIndex].variables[name];
187
+ }
188
+ });
189
+ };
186
190
 
187
191
  let dupIndexes = new Map<string, number>();
188
192
  return <div className={css.vbox(10).fillWidth.pad2(12).bord2(0, 0, 20)}>
@@ -201,6 +205,26 @@ export class ServiceDetailPage extends qreact.Component {
201
205
  onChangeValue={value => setVariable(entryIndex, name, value)}
202
206
  />;
203
207
  })}
208
+ <button
209
+ className={css.pad2(10, 4).button.bord2(0, 0, 20).hsl(120, 70, 90)}
210
+ title="Duplicate this entry (same machine and variables)"
211
+ onClick={() => {
212
+ updateTargets(targets => {
213
+ targets.splice(entryIndex + 1, 0, deepCloneJSON(targets[entryIndex]));
214
+ });
215
+ }}>
216
+ +
217
+ </button>
218
+ <button
219
+ className={css.pad2(10, 4).button.bord2(0, 0, 20).hsl(0, 70, 90)}
220
+ title="Remove this entry"
221
+ onClick={() => {
222
+ updateTargets(targets => {
223
+ targets.splice(entryIndex, 1);
224
+ });
225
+ }}>
226
+
227
+ </button>
204
228
  </div>;
205
229
  })}
206
230
  </div>;