querysub 0.508.0 → 0.509.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/function-public.js +2 -0
- package/bin/server.js +0 -2
- package/package.json +3 -4
- package/spec.txt +5 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
- package/src/-g-core-values/NodeCapabilities.ts +4 -0
- package/src/-g-core-values/scheduledShutdown.ts +48 -0
- package/src/0-path-value-core/AuthorityLookup.ts +50 -9
- package/src/0-path-value-core/PathRouter.ts +10 -31
- package/src/0-path-value-core/PathRouterRouteOverride.ts +14 -98
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +1 -4
- package/src/0-path-value-core/PathValueCommitter.ts +1 -6
- package/src/0-path-value-core/PathValueController.ts +5 -7
- package/src/0-path-value-core/PathWatcher.ts +8 -11
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +8 -22
- package/src/0-path-value-core/pathValueCore.ts +4 -5
- package/src/1-path-client/RemoteWatcher.ts +47 -34
- package/src/2-proxy/archiveMoveHarness.ts +2 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +11 -10
- package/src/3-path-functions/PathFunctionRunner.ts +165 -24
- package/src/3-path-functions/functionReplay.ts +2 -0
- package/src/3-path-functions/syncSchema.ts +0 -2
- package/src/4-deploy/edgeBootstrap.ts +10 -0
- package/src/4-deploy/edgeClientWatcher.tsx +4 -6
- package/src/4-deploy/edgeNodes.ts +52 -12
- package/src/4-querysub/FunctionRunnerTracking.ts +290 -0
- package/src/4-querysub/Querysub.ts +11 -16
- package/src/4-querysub/QuerysubController.ts +13 -11
- package/src/4-querysub/permissions.ts +3 -0
- package/src/4-querysub/querysubPrediction.ts +2 -2
- package/src/config.ts +1 -3
- package/src/deployManager/components/ServiceDetailPage.tsx +127 -73
- package/src/deployManager/components/ServicesListPage.tsx +3 -3
- package/src/deployManager/machineApplyMainCode.ts +139 -153
- package/src/deployManager/machineController.ts +12 -61
- package/src/deployManager/machineSchema.ts +84 -3
- package/src/deployManager/setupMachineMain.ts +2 -1
- package/src/diagnostics/grossStats/GrossStatsController.ts +3 -2
- package/src/diagnostics/logs/errorTickets/autoFixer.ts +3 -2
- package/src/diagnostics/logs/logGitHashes.ts +1 -1
- package/src/diagnostics/managementPages.tsx +2 -0
- package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +36 -28
- package/src/diagnostics/misc-pages/FunctionCaptureAnalyzePage.tsx +1 -1
- package/src/library-components/NetworkSelector.tsx +77 -0
- package/src/misc.ts +19 -0
- package/src/user-implementation/userData.ts +1 -1
- package/src/zipThreaded.ts +3 -2
- package/bin/server-dev.js +0 -11
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { batchFunction, delay, runInfinitePoll } from "socket-function/src/batching";
|
|
2
|
-
import { cache } from "socket-function/src/caching";
|
|
2
|
+
import { cache, lazy } from "socket-function/src/caching";
|
|
3
3
|
import { blue, magenta, yellow } from "socket-function/src/formatting/logColors";
|
|
4
|
-
import { timeInHour, timeInSecond } from "socket-function/src/misc";
|
|
4
|
+
import { timeInHour, timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
5
5
|
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
6
6
|
import { registerDynamicResource } from "../diagnostics/trackResources";
|
|
7
7
|
import { errorToUndefined, logErrors } from "../errors";
|
|
@@ -16,6 +16,8 @@ import { parseArgs } from "./PathFunctionHelpers";
|
|
|
16
16
|
import { FunctionMetadata, PERMISSIONS_FUNCTION_ID, addRoutingPrefixForDeploy, getAllDevelopmentModulesIds, getDevelopmentModule, getExportPath, getModuleRelativePath, getSchemaObject } from "./syncSchema";
|
|
17
17
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
18
18
|
import { getControllerNodeIdList, set_debug_getFunctionRunnerShards } from "../-g-core-values/NodeCapabilities";
|
|
19
|
+
import { getOwnNodeId } from "../-f-node-discovery/NodeDiscovery";
|
|
20
|
+
import { getScheduledShutdownTime } from "../-g-core-values/scheduledShutdown";
|
|
19
21
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
20
22
|
import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
21
23
|
import { getDomain, isPublic, DEFAULT_NETWORK } from "../config";
|
|
@@ -37,8 +39,8 @@ export const functionSchema = rawSchema<{
|
|
|
37
39
|
[ModuleId: string]: {
|
|
38
40
|
Data: unknown;
|
|
39
41
|
Sources: { [FunctionId: string]: FunctionSpec | undefined; };
|
|
40
|
-
Calls: { [CallId: string]: CallSpec | undefined; };
|
|
41
|
-
Results: { [CallId: string]: FunctionResult | undefined; };
|
|
42
|
+
Calls: { [Network: string]: { [CallId: string]: CallSpec | undefined; }; };
|
|
43
|
+
Results: { [Network: string]: { [CallId: string]: FunctionResult | undefined; }; };
|
|
42
44
|
};
|
|
43
45
|
};
|
|
44
46
|
};
|
|
@@ -53,9 +55,14 @@ setImmediate(() => {
|
|
|
53
55
|
});
|
|
54
56
|
|
|
55
57
|
export function commitCall(call: CallSpec) {
|
|
58
|
+
if (!call.network) {
|
|
59
|
+
throw new Error(`Call has no network, so it would never run (no FunctionRunner would pick it up). Call: ${debugCallSpec(call)}`);
|
|
60
|
+
}
|
|
61
|
+
call.writeNodeId = getOwnNodeId();
|
|
56
62
|
let domainName = call.DomainName;
|
|
57
63
|
let moduleId = call.ModuleId;
|
|
58
64
|
let callId = call.CallId;
|
|
65
|
+
let network = call.network;
|
|
59
66
|
proxyWatcher.writeOnly({
|
|
60
67
|
canWrite: true,
|
|
61
68
|
eventWrite: true,
|
|
@@ -63,7 +70,7 @@ export function commitCall(call: CallSpec) {
|
|
|
63
70
|
// NOTE: We write the call in the present, it is callSpec.runAtTime that is in the past. No one cares
|
|
64
71
|
// when the call was added, only when it wants to run at.
|
|
65
72
|
watchFunction: function writeCall() {
|
|
66
|
-
functionSchema()[domainName].PathFunctionRunner[moduleId].Calls[callId] = atomicObjectWrite(call);
|
|
73
|
+
functionSchema()[domainName].PathFunctionRunner[moduleId].Calls[network][callId] = atomicObjectWrite(call);
|
|
67
74
|
},
|
|
68
75
|
});
|
|
69
76
|
}
|
|
@@ -107,8 +114,11 @@ export interface CallSpec {
|
|
|
107
114
|
// Not just used for debugging, also used to add special proxy-related warnings.
|
|
108
115
|
fromProxy?: string;
|
|
109
116
|
|
|
110
|
-
//
|
|
111
|
-
network
|
|
117
|
+
// Only FunctionRunners on this network will run the call. If no FunctionRunner is on this network, the call never runs.
|
|
118
|
+
network: string;
|
|
119
|
+
|
|
120
|
+
// The node that committed the call. FunctionRunners record call timings tagged with this, so the node that wrote a call can pull the timings of its own calls.
|
|
121
|
+
writeNodeId?: string;
|
|
112
122
|
}
|
|
113
123
|
export function debugCallSpec(spec: CallSpec): string {
|
|
114
124
|
return `${spec.DomainName}/${spec.ModuleId}/${spec.FunctionId}`;
|
|
@@ -126,6 +136,121 @@ export type FunctionResult = ({
|
|
|
126
136
|
totalTime: number;
|
|
127
137
|
};
|
|
128
138
|
|
|
139
|
+
export type FunctionStatsSummary = {
|
|
140
|
+
count: number;
|
|
141
|
+
totalTime: number;
|
|
142
|
+
minTime: number;
|
|
143
|
+
maxTime: number;
|
|
144
|
+
};
|
|
145
|
+
const TIMINGS_KEPT_TIME = timeInHour;
|
|
146
|
+
const TIMINGS_BUCKET_SIZE = timeInMinute * 5;
|
|
147
|
+
// Kept PER writing node: how long a call takes varies by the node it was called from, so each node only wants the timings of its own calls. Bucketed (bucket start time => stats) so old data can age out without keeping per-call data; only summaries ever leave this file.
|
|
148
|
+
let functionTimings = new Map<string, Map<string, Map<number, FunctionStatsSummary>>>();
|
|
149
|
+
// Writing nodes get new nodeIds when they restart, so without this their old timings would be kept forever.
|
|
150
|
+
const startTimingsSweep = lazy(() => {
|
|
151
|
+
runInfinitePoll(TIMINGS_KEPT_TIME, function sweepExpiredTimings() {
|
|
152
|
+
let expiry = Date.now() - TIMINGS_KEPT_TIME;
|
|
153
|
+
for (let [writeNodeId, networks] of functionTimings) {
|
|
154
|
+
for (let [network, buckets] of networks) {
|
|
155
|
+
for (let start of buckets.keys()) {
|
|
156
|
+
if (start < expiry) {
|
|
157
|
+
buckets.delete(start);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (buckets.size === 0) {
|
|
161
|
+
networks.delete(network);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (networks.size === 0) {
|
|
165
|
+
functionTimings.delete(writeNodeId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
function recordFunctionTiming(config: {
|
|
171
|
+
writeNodeId: string | undefined;
|
|
172
|
+
network: string;
|
|
173
|
+
durationMs: number;
|
|
174
|
+
}) {
|
|
175
|
+
if (!config.writeNodeId) return;
|
|
176
|
+
startTimingsSweep();
|
|
177
|
+
let networks = functionTimings.get(config.writeNodeId);
|
|
178
|
+
if (!networks) {
|
|
179
|
+
networks = new Map();
|
|
180
|
+
functionTimings.set(config.writeNodeId, networks);
|
|
181
|
+
}
|
|
182
|
+
let buckets = networks.get(config.network);
|
|
183
|
+
if (!buckets) {
|
|
184
|
+
buckets = new Map();
|
|
185
|
+
networks.set(config.network, buckets);
|
|
186
|
+
}
|
|
187
|
+
let now = Date.now();
|
|
188
|
+
let bucketStart = Math.floor(now / TIMINGS_BUCKET_SIZE) * TIMINGS_BUCKET_SIZE;
|
|
189
|
+
let bucket = buckets.get(bucketStart);
|
|
190
|
+
if (!bucket) {
|
|
191
|
+
bucket = { count: 0, totalTime: 0, minTime: config.durationMs, maxTime: config.durationMs };
|
|
192
|
+
buckets.set(bucketStart, bucket);
|
|
193
|
+
}
|
|
194
|
+
bucket.count++;
|
|
195
|
+
bucket.totalTime += config.durationMs;
|
|
196
|
+
bucket.minTime = Math.min(bucket.minTime, config.durationMs);
|
|
197
|
+
bucket.maxTime = Math.max(bucket.maxTime, config.durationMs);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const runnerStartupTime = Date.now();
|
|
201
|
+
export type FunctionRunnerInfo = {
|
|
202
|
+
entryPoint: string;
|
|
203
|
+
startupTime: number;
|
|
204
|
+
scheduledShutdownTime?: number;
|
|
205
|
+
shards: DebugFunctionShardInfo[];
|
|
206
|
+
// network => stats over the last hour of the calling node's own calls
|
|
207
|
+
timings: { [network: string]: FunctionStatsSummary };
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Everything the function runner tracking poll needs, in a single cheap call: what we run (networks/shards), our lifecycle (startup/shutdown), and how long the caller's own calls took. Only exposed by function runners, so a failed call doubles as "not a runner".
|
|
211
|
+
class FunctionRunnerInfoControllerBase {
|
|
212
|
+
public async getRunnerInfo(config: {
|
|
213
|
+
writeNodeId: string;
|
|
214
|
+
}): Promise<FunctionRunnerInfo> {
|
|
215
|
+
let expiry = Date.now() - TIMINGS_KEPT_TIME;
|
|
216
|
+
let timings: FunctionRunnerInfo["timings"] = {};
|
|
217
|
+
for (let [network, buckets] of functionTimings.get(config.writeNodeId) || []) {
|
|
218
|
+
let summary: FunctionStatsSummary = { count: 0, totalTime: 0, minTime: 0, maxTime: 0 };
|
|
219
|
+
for (let [bucketStart, bucket] of buckets) {
|
|
220
|
+
if (bucketStart < expiry) continue;
|
|
221
|
+
if (summary.count === 0) {
|
|
222
|
+
summary.minTime = bucket.minTime;
|
|
223
|
+
}
|
|
224
|
+
summary.count += bucket.count;
|
|
225
|
+
summary.totalTime += bucket.totalTime;
|
|
226
|
+
summary.minTime = Math.min(summary.minTime, bucket.minTime);
|
|
227
|
+
summary.maxTime = Math.max(summary.maxTime, bucket.maxTime);
|
|
228
|
+
}
|
|
229
|
+
timings[network] = summary;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
entryPoint: process.argv[1],
|
|
233
|
+
startupTime: runnerStartupTime,
|
|
234
|
+
scheduledShutdownTime: getScheduledShutdownTime(),
|
|
235
|
+
shards: debugFunctionRunnerShards,
|
|
236
|
+
timings,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export const FunctionRunnerInfoController = SocketFunction.register(
|
|
241
|
+
"FunctionRunnerInfoController-3f7a1c2e-8d94-4b6a-9e51-c0d72fa8b143",
|
|
242
|
+
new FunctionRunnerInfoControllerBase(),
|
|
243
|
+
() => ({
|
|
244
|
+
getRunnerInfo: {},
|
|
245
|
+
}),
|
|
246
|
+
() => ({
|
|
247
|
+
hooks: [requiresNetworkTrustHook],
|
|
248
|
+
}),
|
|
249
|
+
{
|
|
250
|
+
noAutoExpose: true,
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
|
|
129
254
|
let currentCallSpec: { spec: CallSpec; fnc: FunctionSpec; } | undefined;
|
|
130
255
|
export function getCurrentCall() {
|
|
131
256
|
if (!currentCallSpec) {
|
|
@@ -195,6 +320,9 @@ export type DebugFunctionShardInfo = {
|
|
|
195
320
|
domainName: string;
|
|
196
321
|
shardRange: { startFraction: number, endFraction: number };
|
|
197
322
|
secondaryShardRange?: { startFraction: number, endFraction: number };
|
|
323
|
+
// The networks this runner listens to calls on
|
|
324
|
+
networks: string[];
|
|
325
|
+
isPublic: boolean;
|
|
198
326
|
};
|
|
199
327
|
let debugFunctionRunnerShards: DebugFunctionShardInfo[] = [];
|
|
200
328
|
export function debug_getFunctionRunnerShards() {
|
|
@@ -232,10 +360,13 @@ export class PathFunctionRunner {
|
|
|
232
360
|
}) {
|
|
233
361
|
SocketFunction.expose(FunctionPreloadController);
|
|
234
362
|
SocketFunction.expose(FunctionCaptureController);
|
|
363
|
+
SocketFunction.expose(FunctionRunnerInfoController);
|
|
235
364
|
debugFunctionRunnerShards.push({
|
|
236
365
|
domainName: config.domainName,
|
|
237
366
|
shardRange: config.shardRange,
|
|
238
367
|
secondaryShardRange: config.secondaryShardRange,
|
|
368
|
+
networks: config.networks && config.networks.length > 0 && config.networks || [DEFAULT_NETWORK],
|
|
369
|
+
isPublic: isPublic(),
|
|
239
370
|
});
|
|
240
371
|
logErrors(this.startWatching());
|
|
241
372
|
}
|
|
@@ -269,9 +400,8 @@ export class PathFunctionRunner {
|
|
|
269
400
|
|
|
270
401
|
let watchModuleCalls = cache((moduleId: string) => {
|
|
271
402
|
for (let network of networks) {
|
|
272
|
-
|
|
273
|
-
Querysub.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].
|
|
274
|
-
Querysub.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Results, networkConfig);
|
|
403
|
+
Querysub.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Calls[network], fullFraction);
|
|
404
|
+
Querysub.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Results[network], fullFraction);
|
|
275
405
|
}
|
|
276
406
|
});
|
|
277
407
|
|
|
@@ -326,7 +456,10 @@ export class PathFunctionRunner {
|
|
|
326
456
|
if (getPathIndex(path, 1) !== "PathFunctionRunner") continue;
|
|
327
457
|
if (getPathIndex(path, 0) !== domainName) continue;
|
|
328
458
|
|
|
329
|
-
const
|
|
459
|
+
const network = getPathIndex(path, 4);
|
|
460
|
+
if (!network) continue;
|
|
461
|
+
if (!networks.includes(network)) continue;
|
|
462
|
+
const callId = getPathIndex(path, 5);
|
|
330
463
|
if (!callId) continue;
|
|
331
464
|
let moduleId = getPathIndex(path, 2);
|
|
332
465
|
if (!moduleId) continue;
|
|
@@ -334,10 +467,10 @@ export class PathFunctionRunner {
|
|
|
334
467
|
|
|
335
468
|
let moduleData = data()[moduleId];
|
|
336
469
|
|
|
337
|
-
let result = atomicObjectRead(moduleData.Results[callId]);
|
|
470
|
+
let result = atomicObjectRead(moduleData.Results[network][callId]);
|
|
338
471
|
if (result) continue;
|
|
339
472
|
|
|
340
|
-
let callData = atomicObjectRead(moduleData.Calls[callId]);
|
|
473
|
+
let callData = atomicObjectRead(moduleData.Calls[network][callId]);
|
|
341
474
|
if (!callData) continue;
|
|
342
475
|
|
|
343
476
|
// Ignore invalid data
|
|
@@ -363,7 +496,7 @@ export class PathFunctionRunner {
|
|
|
363
496
|
// If we haven't synced the result, we can't know if it is ready or not
|
|
364
497
|
// (we'll run anyways once it is synced). Most other parts are immutable,
|
|
365
498
|
// but the result is by definition not, so... it is one of the values we check!
|
|
366
|
-
if (!isSynced(moduleData.Results[callId])) continue;
|
|
499
|
+
if (!isSynced(moduleData.Results[network][callId])) continue;
|
|
367
500
|
|
|
368
501
|
if (runningCalls.has(callId)) {
|
|
369
502
|
console.log(`Skipping run of already running call ${callId} at ${debugTime(callData.runAtTime)}`);
|
|
@@ -401,7 +534,7 @@ export class PathFunctionRunner {
|
|
|
401
534
|
// NOTE: If this starts but never runs, it means the call's network didn't match ours, which either means it was a dev call and so not relevant for us, or we are dev and it's a production call, so not relevant for us.
|
|
402
535
|
// - The network is set by --network when running serverside, or, clientside, by --network set on the command line of the querysub server.
|
|
403
536
|
console.log(`QUEUING ${getDebugName(callData, functionSpec, true)}`);
|
|
404
|
-
let resultsPath = getProxyPath(() => moduleData.Results[callId]);
|
|
537
|
+
let resultsPath = getProxyPath(() => moduleData.Results[network][callId]);
|
|
405
538
|
let history = authorityStorage.getValuePlusHistory(resultsPath);
|
|
406
539
|
console.log(` History: ${history.length}`);
|
|
407
540
|
for (let { valid, time, canGCValue } of history) {
|
|
@@ -551,7 +684,7 @@ export class PathFunctionRunner {
|
|
|
551
684
|
skipPermissions = PermissionsChecker.skipPermissionsChecks.bind(PermissionsChecker);
|
|
552
685
|
}
|
|
553
686
|
|
|
554
|
-
let callNetwork = callSpec.network
|
|
687
|
+
let callNetwork = callSpec.network;
|
|
555
688
|
let ourNetworks = this.config.networks && this.config.networks.length > 0 && this.config.networks || [DEFAULT_NETWORK];
|
|
556
689
|
if (!ourNetworks.includes(callNetwork)) {
|
|
557
690
|
return;
|
|
@@ -577,7 +710,7 @@ export class PathFunctionRunner {
|
|
|
577
710
|
let syncedModule = skipPermissions(() =>
|
|
578
711
|
functionSchema()[callSpec.DomainName].PathFunctionRunner[callSpec.ModuleId]
|
|
579
712
|
);
|
|
580
|
-
return !!atomicObjectRead(syncedModule.Results[callSpec.CallId]);
|
|
713
|
+
return !!atomicObjectRead(syncedModule.Results[callNetwork][callSpec.CallId]);
|
|
581
714
|
},
|
|
582
715
|
});
|
|
583
716
|
}
|
|
@@ -740,7 +873,7 @@ export class PathFunctionRunner {
|
|
|
740
873
|
// know figure out the dependencies for the call, which makes function call prediction
|
|
741
874
|
// possible.
|
|
742
875
|
skipPermissions(() => {
|
|
743
|
-
let result = doProxyOptions({ forceReadLatest: true }, () => atomicObjectRead(syncedModule.Results[callSpec.CallId]));
|
|
876
|
+
let result = doProxyOptions({ forceReadLatest: true }, () => atomicObjectRead(syncedModule.Results[callNetwork][callSpec.CallId]));
|
|
744
877
|
if (result) {
|
|
745
878
|
nooped = true;
|
|
746
879
|
// It is important to NOOP if it has already been written. Otherwise there could be multiple
|
|
@@ -757,7 +890,7 @@ export class PathFunctionRunner {
|
|
|
757
890
|
let currentTimeTaken = Date.now() - startTime;
|
|
758
891
|
stats.timeTaken = currentTimeTaken;
|
|
759
892
|
let totalTime = Date.now() - stats.firstQueuedTime;
|
|
760
|
-
syncedModule.Results[callSpec.CallId] = atomicObjectWrite({
|
|
893
|
+
syncedModule.Results[callNetwork][callSpec.CallId] = atomicObjectWrite({
|
|
761
894
|
lastInternalLoopCount: runCount,
|
|
762
895
|
outerLoopCount: stats.outerLoopCount,
|
|
763
896
|
totalInternalLoopCount: stats.totalInternalLoopCount,
|
|
@@ -765,7 +898,7 @@ export class PathFunctionRunner {
|
|
|
765
898
|
evalTime,
|
|
766
899
|
totalTime,
|
|
767
900
|
});
|
|
768
|
-
proxyWatcher.setEventPath(() => syncedModule.Results[callSpec.CallId]);
|
|
901
|
+
proxyWatcher.setEventPath(() => syncedModule.Results[callNetwork][callSpec.CallId]);
|
|
769
902
|
});
|
|
770
903
|
|
|
771
904
|
let watcher = proxyWatcher.getTriggeredWatcher();
|
|
@@ -786,7 +919,7 @@ export class PathFunctionRunner {
|
|
|
786
919
|
// Capture only runs that actually authored the Result (not nooped/partial runs),
|
|
787
920
|
// so a replayed call maps to exactly one recorded output state.
|
|
788
921
|
if (isCapturing()) {
|
|
789
|
-
let resultsPathStr = getProxyPath(() => functionSchema()[callSpec.DomainName].PathFunctionRunner[callSpec.ModuleId].Results[callSpec.CallId]);
|
|
922
|
+
let resultsPathStr = getProxyPath(() => functionSchema()[callSpec.DomainName].PathFunctionRunner[callSpec.ModuleId].Results[callNetwork][callSpec.CallId]);
|
|
790
923
|
let resultWrite = writes.find(w => w.path === resultsPathStr);
|
|
791
924
|
if (resultWrite) {
|
|
792
925
|
recordCapturedCall({
|
|
@@ -871,6 +1004,13 @@ export class PathFunctionRunner {
|
|
|
871
1004
|
wallTime, syncTime, evalTime,
|
|
872
1005
|
loops: runCount,
|
|
873
1006
|
});
|
|
1007
|
+
if (!nooped) {
|
|
1008
|
+
recordFunctionTiming({
|
|
1009
|
+
writeNodeId: callSpec.writeNodeId,
|
|
1010
|
+
network: callSpec.network,
|
|
1011
|
+
durationMs: Date.now() - callSpec.runAtTime.time,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
874
1014
|
PathFunctionRunner.RUN_FINISH_COUNT++;
|
|
875
1015
|
}
|
|
876
1016
|
|
|
@@ -908,11 +1048,12 @@ export class PathFunctionRunner {
|
|
|
908
1048
|
eventWrite: true,
|
|
909
1049
|
doNotStoreWritesAsPredictions: true,
|
|
910
1050
|
watchFunction() {
|
|
1051
|
+
let network = callSpec.network;
|
|
911
1052
|
let syncedModule = functionSchema()[callSpec.DomainName].PathFunctionRunner[callSpec.ModuleId];
|
|
912
|
-
// Don't clobber the value. I'm pretty sure event writes still read values, so we should still do a synchronization to see if there is an existing result.
|
|
913
|
-
let result = doProxyOptions({ forceReadLatest: true }, () => atomicObjectRead(syncedModule.Results[callSpec.CallId]));
|
|
1053
|
+
// Don't clobber the value. I'm pretty sure event writes still read values, so we should still do a synchronization to see if there is an existing result.
|
|
1054
|
+
let result = doProxyOptions({ forceReadLatest: true }, () => atomicObjectRead(syncedModule.Results[network][callSpec.CallId]));
|
|
914
1055
|
if (result) return;
|
|
915
|
-
syncedModule.Results[callSpec.CallId] = atomicObjectWrite({
|
|
1056
|
+
syncedModule.Results[network][callSpec.CallId] = atomicObjectWrite({
|
|
916
1057
|
error,
|
|
917
1058
|
lastInternalLoopCount: runCount,
|
|
918
1059
|
outerLoopCount,
|
|
@@ -17,6 +17,8 @@ export function makeReplayCallSpec(record: CaptureRecord): CallSpec {
|
|
|
17
17
|
callerMachineId: record.callerMachineId,
|
|
18
18
|
callerIP: record.callerIP,
|
|
19
19
|
runAtTime: record.runAtTime,
|
|
20
|
+
// Replays run locally and are never committed, so the network is irrelevant
|
|
21
|
+
network: "",
|
|
20
22
|
};
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -579,7 +579,6 @@ export function getCallIdOverride(config: {
|
|
|
579
579
|
functionId: string;
|
|
580
580
|
callId: string;
|
|
581
581
|
args: unknown[];
|
|
582
|
-
network?: string;
|
|
583
582
|
}): string {
|
|
584
583
|
try {
|
|
585
584
|
if (querysub) {
|
|
@@ -594,7 +593,6 @@ export function getCallIdOverride(config: {
|
|
|
594
593
|
remappedPrefix: def.prefix,
|
|
595
594
|
originalKey: config.callId,
|
|
596
595
|
routeKey: def.getKey(...config.args),
|
|
597
|
-
network: config.network,
|
|
598
596
|
});
|
|
599
597
|
} catch (e: any) {
|
|
600
598
|
console.error(`Error getting call id override for ${config.moduleId}.${config.functionId}, falling back to original call id`, { error: e.stack });
|
|
@@ -396,6 +396,16 @@ async function edgeNodeFunction(config: {
|
|
|
396
396
|
// to pick a node with low utilization.
|
|
397
397
|
edgeNodes = shuffle(edgeNodes);
|
|
398
398
|
|
|
399
|
+
// Deprioritize (but don't exclude, in case there is nothing else) nodes that are scheduled to shut down, and nodes that just booted (they might still be warming up, and if they crash on startup we would follow them down).
|
|
400
|
+
const EDGE_MIN_UP_TIME = 60_000;
|
|
401
|
+
const edgePickPenalty = (node: EdgeNodeConfig) => {
|
|
402
|
+
let penalty = 0;
|
|
403
|
+
if (node.scheduledShutdownTime) penalty += 2;
|
|
404
|
+
if (Date.now() - node.bootTime < EDGE_MIN_UP_TIME) penalty += 1;
|
|
405
|
+
return penalty;
|
|
406
|
+
};
|
|
407
|
+
edgeNodes.sort((a, b) => edgePickPenalty(a) - edgePickPenalty(b));
|
|
408
|
+
|
|
399
409
|
// We check multiple at a time, so a node being unresponsive doesn't cause lag
|
|
400
410
|
let PARALLEL_FACTOR = 3;
|
|
401
411
|
for (let i = 0; i < edgeNodes.length; i += PARALLEL_FACTOR) {
|
|
@@ -42,11 +42,9 @@ export function startEdgeNotifier() {
|
|
|
42
42
|
});
|
|
43
43
|
void (async () => {
|
|
44
44
|
await delay(1);
|
|
45
|
-
let {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
void notifyClients(`forcedupdate-at-${time}`, time);
|
|
49
|
-
}
|
|
45
|
+
let { watchScheduledShutdown } = await import("../-g-core-values/scheduledShutdown");
|
|
46
|
+
watchScheduledShutdown((time) => {
|
|
47
|
+
void notifyClients(`forcedupdate-at-${time}`, time);
|
|
50
48
|
});
|
|
51
49
|
})();
|
|
52
50
|
}
|
|
@@ -191,7 +189,7 @@ const EdgeNotifierClientController = SocketFunction.register(
|
|
|
191
189
|
|
|
192
190
|
if (!isNode()) {
|
|
193
191
|
setImmediate(() => {
|
|
194
|
-
void Querysub.
|
|
192
|
+
void Querysub.startupWait().finally(async () => {
|
|
195
193
|
curHash = await EdgeNotifierController.nodes[SocketFunction.browserNodeId()].watchUpdates();
|
|
196
194
|
void runRefreshLoop();
|
|
197
195
|
});
|
|
@@ -26,6 +26,7 @@ import { startEdgeNotifier } from "./edgeClientWatcher";
|
|
|
26
26
|
import { getGitRefLive, getGitURLLive } from "./git";
|
|
27
27
|
import { DeployProgress } from "./deployFunctions";
|
|
28
28
|
import { PromiseObj } from "../promise";
|
|
29
|
+
import type { FunctionRunnerIndex } from "../4-querysub/FunctionRunnerTracking";
|
|
29
30
|
|
|
30
31
|
const UPDATE_POLL_INTERVAL = timeInMinute * 15;
|
|
31
32
|
const DEAD_NODE_COUNT_THRESHOLD = 15;
|
|
@@ -73,6 +74,10 @@ export type EdgeNodeConfig = {
|
|
|
73
74
|
bootTime: number;
|
|
74
75
|
entryPaths: string[];
|
|
75
76
|
public: boolean;
|
|
77
|
+
// Set once this node has been told it will shut down (scheduled switchover). Clients deprioritize these nodes when picking an edge node.
|
|
78
|
+
scheduledShutdownTime?: number;
|
|
79
|
+
// The function runner index at registration time, so clients have it immediately at startup (they refresh it by polling QuerysubController.getFunctionRunnerIndex).
|
|
80
|
+
functionRunnerIndex?: FunctionRunnerIndex;
|
|
76
81
|
};
|
|
77
82
|
let registeredEdgeNode: { host: string; entryPaths: string[] } | boolean | undefined;
|
|
78
83
|
export async function registerEdgeNode(config: {
|
|
@@ -100,19 +105,20 @@ export async function registerEdgeNode(config: {
|
|
|
100
105
|
}));
|
|
101
106
|
await updateEdgeNodesFile();
|
|
102
107
|
});
|
|
103
|
-
const {
|
|
104
|
-
|
|
105
|
-
|
|
108
|
+
const { watchScheduledShutdown } = await import("../-g-core-values/scheduledShutdown");
|
|
109
|
+
watchScheduledShutdown((time) => {
|
|
110
|
+
void (async () => {
|
|
111
|
+
// Immediately publish our shutdown time, so clients can deprioritize us when picking an edge node.
|
|
112
|
+
await publishOwnScheduledShutdown(time);
|
|
106
113
|
let duration = time - Date.now();
|
|
107
|
-
// Wait a bit to remove ourself from the edge node list
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
114
|
+
// Wait a bit to fully remove ourself from the edge node list
|
|
115
|
+
await delay(Math.max(0, duration * 0.2));
|
|
116
|
+
console.log(magenta(`Removing node from edge node list due to scheduled shutdown at ${new Date(time).toLocaleString()}`));
|
|
117
|
+
await Promise.all(Array.from(registeredNodePaths).map(async nodePath => {
|
|
118
|
+
await edgeNodeStorage.del(nodePath);
|
|
119
|
+
}));
|
|
120
|
+
await updateEdgeNodesFile();
|
|
121
|
+
})();
|
|
116
122
|
});
|
|
117
123
|
|
|
118
124
|
await waitForFirstTimeSync();
|
|
@@ -154,6 +160,7 @@ export async function registerEdgeNode(config: {
|
|
|
154
160
|
bootTime: Date.now(),
|
|
155
161
|
entryPaths: config.entryPaths,
|
|
156
162
|
public: isPublic(),
|
|
163
|
+
functionRunnerIndex: await getInitialFunctionRunnerIndex(),
|
|
157
164
|
};
|
|
158
165
|
|
|
159
166
|
await edgeNodeStorage.set(getNextNodePath(), Buffer.from(JSON.stringify(edgeNodeConfig)));
|
|
@@ -204,6 +211,7 @@ const loadEntryPointsByHash = runInSerial(async function loadEntryPointsByHash(c
|
|
|
204
211
|
bootTime: Date.now(),
|
|
205
212
|
entryPaths,
|
|
206
213
|
public: isPublic(),
|
|
214
|
+
functionRunnerIndex: await getInitialFunctionRunnerIndex(),
|
|
207
215
|
};
|
|
208
216
|
|
|
209
217
|
await edgeNodeStorage.set(getNextNodePath(), Buffer.from(JSON.stringify(edgeNodeConfig)));
|
|
@@ -214,6 +222,25 @@ const loadEntryPointsByHash = runInSerial(async function loadEntryPointsByHash(c
|
|
|
214
222
|
onEdgeNodesChanged();
|
|
215
223
|
});
|
|
216
224
|
|
|
225
|
+
async function getInitialFunctionRunnerIndex(): Promise<FunctionRunnerIndex | undefined> {
|
|
226
|
+
const { startFunctionRunnerTracking, getFunctionRunnerIndex } = await import("../4-querysub/FunctionRunnerTracking");
|
|
227
|
+
await startFunctionRunnerTracking();
|
|
228
|
+
return getFunctionRunnerIndex();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function publishOwnScheduledShutdown(time: number) {
|
|
232
|
+
for (let nodePath of Array.from(registeredNodePaths)) {
|
|
233
|
+
let buffer = await edgeNodeStorage.get(nodePath);
|
|
234
|
+
if (!buffer) continue;
|
|
235
|
+
let config: EdgeNodeConfig = JSON.parse(buffer.toString());
|
|
236
|
+
if (config.scheduledShutdownTime === time) continue;
|
|
237
|
+
config.scheduledShutdownTime = time;
|
|
238
|
+
await edgeNodeStorage.set(nodePath, Buffer.from(JSON.stringify(config)));
|
|
239
|
+
getEdgeNodeConfig.clearKey(nodePath);
|
|
240
|
+
}
|
|
241
|
+
await updateEdgeNodesFile();
|
|
242
|
+
}
|
|
243
|
+
|
|
217
244
|
export const getEdgeNodeConfigURL = lazy(async () => {
|
|
218
245
|
let { getURL } = await hostArchives({
|
|
219
246
|
archives: edgeNodeStorage,
|
|
@@ -291,6 +318,19 @@ async function updateEdgeNodesFile() {
|
|
|
291
318
|
diff = true;
|
|
292
319
|
console.log(`Detected live hash changed. Updating ${edgeNodeIndexFile}`, { liveHash, prevHash: prevEdgeNodeIndex.liveHash });
|
|
293
320
|
}
|
|
321
|
+
if (!diff) {
|
|
322
|
+
// Node configs are otherwise immutable, but the scheduled shutdown time is written into them after the fact, and clients need it to avoid picking dying edge nodes.
|
|
323
|
+
let prevShutdownTimes = new Map(prevEdgeNodeIndex.edgeNodes.map(x => [x.nodeId, x.scheduledShutdownTime]));
|
|
324
|
+
for (let nodeFile of edgeNodeFiles) {
|
|
325
|
+
let config = await getEdgeNodeConfig(nodeFile);
|
|
326
|
+
if (!config) continue;
|
|
327
|
+
if (prevShutdownTimes.get(config.nodeId) !== config.scheduledShutdownTime) {
|
|
328
|
+
diff = true;
|
|
329
|
+
console.log(`Detected scheduled shutdown time changed for ${config.nodeId}. Updating ${edgeNodeIndexFile}`);
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
294
334
|
if (!diff) return;
|
|
295
335
|
|
|
296
336
|
|