querysub 0.403.0 → 0.405.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.
Files changed (108) hide show
  1. package/.cursorrules +2 -0
  2. package/bin/audit-imports.js +4 -0
  3. package/bin/join.js +1 -1
  4. package/package.json +7 -4
  5. package/spec.txt +77 -0
  6. package/src/-a-archives/archiveCache.ts +9 -4
  7. package/src/-a-archives/archivesBackBlaze.ts +1039 -1039
  8. package/src/-a-auth/certs.ts +0 -12
  9. package/src/-c-identity/IdentityController.ts +12 -3
  10. package/src/-f-node-discovery/NodeDiscovery.ts +32 -26
  11. package/src/-g-core-values/NodeCapabilities.ts +12 -2
  12. package/src/0-path-value-core/AuthorityLookup.ts +239 -0
  13. package/src/0-path-value-core/LockWatcher2.ts +150 -0
  14. package/src/0-path-value-core/PathRouter.ts +543 -0
  15. package/src/0-path-value-core/PathRouterRouteOverride.ts +72 -0
  16. package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +73 -0
  17. package/src/0-path-value-core/PathValueCommitter.ts +222 -488
  18. package/src/0-path-value-core/PathValueController.ts +277 -239
  19. package/src/0-path-value-core/PathWatcher.ts +534 -0
  20. package/src/0-path-value-core/ShardPrefixes.ts +31 -0
  21. package/src/0-path-value-core/ValidStateComputer.ts +303 -0
  22. package/src/0-path-value-core/archiveLocks/ArchiveLocks.ts +1 -1
  23. package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +80 -44
  24. package/src/0-path-value-core/archiveLocks/archiveSnapshots.ts +13 -16
  25. package/src/0-path-value-core/auditLogs.ts +2 -0
  26. package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +97 -0
  27. package/src/0-path-value-core/pathValueArchives.ts +491 -492
  28. package/src/0-path-value-core/pathValueCore.ts +195 -1496
  29. package/src/0-path-value-core/startupAuthority.ts +74 -0
  30. package/src/1-path-client/RemoteWatcher.ts +90 -82
  31. package/src/1-path-client/pathValueClientWatcher.ts +808 -815
  32. package/src/2-proxy/PathValueProxyWatcher.ts +10 -8
  33. package/src/2-proxy/archiveMoveHarness.ts +182 -214
  34. package/src/2-proxy/garbageCollection.ts +9 -8
  35. package/src/2-proxy/schema2.ts +21 -1
  36. package/src/3-path-functions/PathFunctionHelpers.ts +206 -180
  37. package/src/3-path-functions/PathFunctionRunner.ts +943 -766
  38. package/src/3-path-functions/PathFunctionRunnerMain.ts +5 -3
  39. package/src/3-path-functions/pathFunctionLoader.ts +2 -2
  40. package/src/3-path-functions/syncSchema.ts +596 -521
  41. package/src/4-deploy/deployFunctions.ts +19 -4
  42. package/src/4-deploy/deployGetFunctionsInner.ts +8 -2
  43. package/src/4-deploy/deployMain.ts +51 -68
  44. package/src/4-deploy/edgeClientWatcher.tsx +6 -1
  45. package/src/4-deploy/edgeNodes.ts +2 -2
  46. package/src/4-dom/qreact.tsx +2 -4
  47. package/src/4-dom/qreactTest.tsx +7 -13
  48. package/src/4-querysub/Querysub.ts +21 -8
  49. package/src/4-querysub/QuerysubController.ts +45 -29
  50. package/src/4-querysub/permissions.ts +2 -2
  51. package/src/4-querysub/querysubPrediction.ts +80 -70
  52. package/src/4-querysub/schemaHelpers.ts +5 -1
  53. package/src/5-diagnostics/GenericFormat.tsx +14 -9
  54. package/src/archiveapps/archiveGCEntry.tsx +9 -2
  55. package/src/archiveapps/archiveJoinEntry.ts +96 -84
  56. package/src/bits.ts +19 -0
  57. package/src/config.ts +21 -3
  58. package/src/config2.ts +23 -48
  59. package/src/deployManager/components/DeployPage.tsx +7 -3
  60. package/src/deployManager/machineSchema.ts +4 -1
  61. package/src/diagnostics/ActionsHistory.ts +3 -8
  62. package/src/diagnostics/AuditLogPage.tsx +2 -3
  63. package/src/diagnostics/FunctionCallInfo.tsx +141 -0
  64. package/src/diagnostics/FunctionCallInfoState.ts +162 -0
  65. package/src/diagnostics/MachineThreadInfo.tsx +1 -1
  66. package/src/diagnostics/NodeViewer.tsx +37 -48
  67. package/src/diagnostics/SyncTestPage.tsx +241 -0
  68. package/src/diagnostics/auditImportViolations.ts +185 -0
  69. package/src/diagnostics/listenOnDebugger.ts +3 -3
  70. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +10 -4
  71. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +2 -2
  72. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +24 -22
  73. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +1 -1
  74. package/src/diagnostics/logs/diskLogGlobalContext.ts +1 -0
  75. package/src/diagnostics/logs/errorNotifications2/logWatcher.ts +1 -3
  76. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryEditor.tsx +34 -16
  77. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleEntryReadMode.tsx +4 -6
  78. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleInstanceTableView.tsx +36 -5
  79. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePage.tsx +19 -5
  80. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCycleRenderer.tsx +15 -7
  81. package/src/diagnostics/logs/lifeCycleAnalysis/NestedLifeCycleInfo.tsx +28 -106
  82. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMatching.ts +2 -0
  83. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleMisc.ts +0 -0
  84. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycleSearch.tsx +18 -7
  85. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +3 -0
  86. package/src/diagnostics/managementPages.tsx +10 -3
  87. package/src/diagnostics/misc-pages/ArchiveViewer.tsx +20 -26
  88. package/src/diagnostics/misc-pages/ArchiveViewerTree.tsx +6 -4
  89. package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +2 -2
  90. package/src/diagnostics/misc-pages/LocalWatchViewer.tsx +7 -9
  91. package/src/diagnostics/misc-pages/SnapshotViewer.tsx +23 -12
  92. package/src/diagnostics/misc-pages/archiveViewerShared.tsx +1 -1
  93. package/src/diagnostics/pathAuditer.ts +486 -0
  94. package/src/diagnostics/pathAuditerCallback.ts +20 -0
  95. package/src/diagnostics/watchdog.ts +8 -1
  96. package/src/library-components/URLParam.ts +1 -1
  97. package/src/misc/hash.ts +1 -0
  98. package/src/path.ts +21 -7
  99. package/src/server.ts +54 -47
  100. package/src/user-implementation/loginEmail.tsx +1 -1
  101. package/tempnotes.txt +65 -0
  102. package/test.ts +298 -97
  103. package/src/0-path-value-core/NodePathAuthorities.ts +0 -1057
  104. package/src/0-path-value-core/PathController.ts +0 -1
  105. package/src/5-diagnostics/diskValueAudit.ts +0 -218
  106. package/src/5-diagnostics/memoryValueAudit.ts +0 -438
  107. package/src/archiveapps/archiveMergeEntry.tsx +0 -48
  108. package/src/archiveapps/lockTest.ts +0 -127
@@ -1,766 +1,943 @@
1
- import { delay, runInfinitePoll } from "socket-function/src/batching";
2
- import { cache } from "socket-function/src/caching";
3
- import { blue, magenta, red, yellow } from "socket-function/src/formatting/logColors";
4
- import { sha256HashBuffer, timeInHour, timeInSecond } from "socket-function/src/misc";
5
- import { measureFnc } from "socket-function/src/profiling/measure";
6
- import { getLowUint32, getShortNumber } from "../bits";
7
- import { registerDynamicResource } from "../diagnostics/trackResources";
8
- import { errorToUndefined, logErrors } from "../errors";
9
- import { getPathDepth, getPathFromStr, getPathIndex, trimPathStrToDepth } from "../path";
10
- import { rawSchema } from "../2-proxy/pathDatabaseProxyBase";
11
- import { ClientWatcher } from "../1-path-client/pathValueClientWatcher";
12
- import { getProxyPath } from "../2-proxy/pathValueProxy";
13
- import { atomic, atomicObjectRead, atomicObjectWrite, doAtomicWrites, doProxyOptions, isSynced, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
14
- import { __getRoutingHash, authorityStorage, compareTime, debugTime, epochTime, getNextTime, isCoreQuiet, MAX_ACCEPTED_CHANGE_AGE, MAX_CHANGE_AGE, PathValue, Time } from "../0-path-value-core/pathValueCore";
15
- import { getModuleFromSpec, watchModuleHotreloads } from "./pathFunctionLoader";
16
- import debugbreak from "debugbreak";
17
- import { parseArgs } from "./PathFunctionHelpers";
18
- import { FunctionMetadata, PERMISSIONS_FUNCTION_ID, getAllDevelopmentModulesIds, getDevelopmentModule, getExportPath, getModuleRelativePath, getSchemaObject } from "./syncSchema";
19
- import { formatTime } from "socket-function/src/formatting/format";
20
- import { getControllerNodeIdList, set_debug_getFunctionRunnerShards } from "../-g-core-values/NodeCapabilities";
21
- import { FilterSelector, Filterable, doesMatch } from "../misc/filterable";
22
- import { SocketFunction } from "socket-function/SocketFunction";
23
- import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
24
- import { getDomain, isLocal } from "../config";
25
- import { getGitRefSync, getGitURLSync } from "../4-deploy/git";
26
- import { DeployProgress } from "../4-deploy/deployFunctions";
27
- import { logDisk } from "../diagnostics/logs/diskLogger";
28
-
29
- export const functionSchema = rawSchema<{
30
- [domainName: string]: {
31
- PathFunctionRunner: {
32
- [ModuleId: string]: {
33
- Data: unknown;
34
- Sources: { [FunctionId: string]: FunctionSpec | undefined; };
35
- Calls: { [CallId: string]: CallSpec | undefined; };
36
- Results: { [CallId: string]: FunctionResult | undefined; };
37
- };
38
- };
39
- };
40
- }>();
41
-
42
- export function commitCall(call: CallSpec) {
43
- let domainName = call.DomainName;
44
- let moduleId = call.ModuleId;
45
- let callId = call.CallId;
46
- proxyWatcher.writeOnly({
47
- canWrite: true,
48
- eventWrite: true,
49
- doNotStoreWritesAsPredictions: true,
50
- // NOTE: We write the call in the present, it is callSpec.runAtTime that is in the past. No one cares
51
- // when the call was added, only when it wants to run at.
52
- watchFunction: function writeCall() {
53
- functionSchema()[domainName].PathFunctionRunner[moduleId].Calls[callId] = atomicObjectWrite(call);
54
- },
55
- });
56
- }
57
-
58
- export const DEPTH_TO_DATA = 4;
59
- export const DOMAIN_INDEX = 0;
60
- export const MODULE_INDEX = 2;
61
-
62
- export function getSchemaPartsFromPath(path: string): { domainName: string; moduleId: string } | undefined {
63
- let parts = getPathFromStr(path);
64
- let domainName = parts[0];
65
- let moduleId = parts[2];
66
- if (!domainName || !moduleId) return undefined;
67
- return { domainName, moduleId };
68
- }
69
-
70
- export type FunctionSpec = {
71
- DomainName: string;
72
- ModuleId: string;
73
- FunctionId: string;
74
- exportPathStr: string;
75
- FilePath: string;
76
- gitURL: string;
77
- /** Ex, the hash */
78
- gitRef: string;
79
-
80
- maxLocksOverride?: number;
81
- };
82
-
83
- export interface CallSpec {
84
- DomainName: string;
85
- ModuleId: string;
86
- CallId: string;
87
- FunctionId: string;
88
- // NOTE: This is no longer JSON. Use parseArgs to decode it
89
- argsEncoded: string;
90
- callerMachineId: string;
91
- callerIP: string;
92
- runAtTime: Time;
93
-
94
- // Not just used for debugging, also used to add special proxy-related warnings.
95
- fromProxy?: string;
96
-
97
- filterable?: Filterable;
98
- }
99
- export function debugCallSpec(spec: CallSpec): string {
100
- return `${spec.DomainName}/${spec.ModuleId}/${spec.FunctionId}`;
101
- }
102
- export type FunctionResult = ({
103
-
104
- } | {
105
- error: string;
106
- }) & {
107
- timeTaken: number;
108
- evalTime: number;
109
- runCount: number;
110
- };
111
-
112
- let currentCallSpec: { spec: CallSpec; fnc: FunctionSpec; } | undefined;
113
- export function getCurrentCall() {
114
- if (!currentCallSpec) {
115
- debugbreak(2);
116
- debugger;
117
- throw new Error("Not presently in a call, and so cannot get call");
118
- }
119
- return currentCallSpec.spec;
120
- }
121
- export function getCurrentCallAllowUndefined() {
122
- return currentCallSpec?.spec;
123
- }
124
- export function getCurrentCallObj() {
125
- return currentCallSpec;
126
- }
127
- /** NOTE: We require the FunctionSpec so we know what has is loaded, to identify the
128
- * correct schema. This should be available, but if it isn't... we MIGHT be able
129
- * to work around it, if we can be assured the schema won't be used?
130
- * - This is so pre-loading of functions (which might happen without a deploy, if the deploy script
131
- * is killed part of the way through), doesn't change schemas until the deploy actually
132
- * updates the underlying functions.
133
- */
134
- export function overrideCurrentCall<T>(config: { spec: CallSpec; fnc: FunctionSpec; }, code: () => T) {
135
- let prev = currentCallSpec;
136
- currentCallSpec = config;
137
- try {
138
- return code();
139
- } finally {
140
- currentCallSpec = prev;
141
- }
142
- }
143
-
144
- function getDebugName(call: CallSpec, functionSpec: FunctionSpec, colored = false) {
145
- let pathName = getPathFromStr(functionSpec.exportPathStr).slice(1).join(".");
146
- let mainPart = `${call.DomainName}:${functionSpec.FilePath}|${pathName}|${debugTime(call.runAtTime)}`;
147
- if (colored) {
148
- return `${magenta(mainPart)}`;
149
- } else {
150
- return `${mainPart}`;
151
- }
152
- }
153
-
154
- type PermissionsCheckType = {
155
- new(callerMachineId: { callerMachineId: string; callerIP: string; }): {
156
- checkPermissions(path: string): { permissionsPath: string; allowed: boolean; };
157
- };
158
- skipPermissionsChecks: <T>(code: () => T) => T;
159
- }
160
-
161
- export type DebugFunctionShardInfo = {
162
- domainName: string;
163
- shardRange: { startFraction: number, endFraction: number };
164
- secondaryShardRange?: { startFraction: number, endFraction: number };
165
- };
166
- let debugFunctionRunnerShards: DebugFunctionShardInfo[] = [];
167
- export function debug_getFunctionRunnerShards() {
168
- return debugFunctionRunnerShards;
169
- }
170
- setImmediate(() => {
171
- set_debug_getFunctionRunnerShards(debug_getFunctionRunnerShards);
172
- });
173
-
174
- export class PathFunctionRunner {
175
- public static RUN_START_COUNT = 0;
176
- public static RUN_FINISH_COUNT = 0;
177
- public static DEBUG_CALLS = false;
178
- public static DEBUG_CALL_TRIGGERS = false;
179
- public static DEBUG_WATCHES_THRESHOLD = 5;
180
- public static MAX_WATCH_LOOPS = 1000;
181
-
182
- constructor(private config: {
183
- domainName: string;
184
- shardRange: { startFraction: number, endFraction: number };
185
- secondaryShardRange?: { startFraction: number, endFraction: number };
186
- PermissionsChecker: PermissionsCheckType | undefined;
187
- filterSelector?: FilterSelector;
188
- }) {
189
- SocketFunction.expose(FunctionPreloadController);
190
- debugFunctionRunnerShards.push({
191
- domainName: config.domainName,
192
- shardRange: config.shardRange,
193
- secondaryShardRange: config.secondaryShardRange,
194
- });
195
- logErrors(this.startWatching());
196
- }
197
-
198
- // Calls runCall on any calls that match our domain + shard range
199
- private async startWatching() {
200
- const { Querysub } = await import("../4-querysub/Querysub");
201
- let { shardRange, secondaryShardRange } = this.config;
202
-
203
- // We will use PathValueProxyWatcher to watch the paths
204
- // Maintain only one outstanding runCall per callId
205
- // - Once it finishes, we can trigger another call, if we notice the result is not set.
206
- // - AS we should get write prediction for the result, we can rerun our proxy check
207
- // loop immediately.
208
-
209
- let fullFraction = shardRange;
210
- if (secondaryShardRange) {
211
- fullFraction = {
212
- startFraction: Math.min(shardRange.startFraction, secondaryShardRange.startFraction),
213
- endFraction: Math.max(shardRange.endFraction, secondaryShardRange.endFraction),
214
- };
215
- }
216
-
217
- let config = this.config;
218
-
219
- let self = this;
220
-
221
- let outstandingCalls = 0;
222
-
223
- let watchModuleCalls = cache((moduleId: string) => {
224
- // Object.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Calls);
225
- // Object.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Results);
226
- Querysub.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Calls, fullFraction);
227
- Querysub.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Results, fullFraction);
228
- });
229
-
230
- let runningCalls = new Set<string>();
231
-
232
- // Rarely we might need to queue a function multiple times, when we are late to receive rejections.
233
- // HOWEVER, after enough times, we should stop, as we will probably just infinitely queue it.
234
- const MAX_QUEUE_COUNT = 100;
235
- let queueLimitCounts = new Map<string, number>();
236
- // Clear every hour, so we don't leave
237
- runInfinitePoll(timeInHour, () => queueLimitCounts.clear());
238
-
239
- const domainName = config.domainName;
240
- const watcher = proxyWatcher.createWatcher({
241
- trackTriggers: true,
242
- skipPermissionsCheck: true,
243
- hasSideEffects: true,
244
- // Maybe allow running with unsynced reads? This might be good, because we don't need all reads
245
- // to detect calls. BUT, it might also be bad, because it might cause us to rerun in a cascading
246
- // fashion? (As in, be really slow).
247
- // - Shouldn't be needed, because writes don't take that long
248
- // - Could fix bugs where we never get a read, and therefore all function calls fails to run?
249
- // allowUnsyncedReads: true
250
- watchFunction: function findFunctionsToCall() {
251
- // TODO: Maybe abstract the "preserve watchers" stuff,
252
- // so that it is automatically done at the start of our trigger.
253
- const watcher = proxyWatcher.getTriggeredWatcher();
254
-
255
- // Preserve previous watches explicitly (so we just do a diff, instead of accessing every
256
- // value every time).
257
- proxyWatcher.reuseLastWatches();
258
-
259
- let data = () => functionSchema()[domainName].PathFunctionRunner;
260
-
261
- let moduleIds = Object.keys(data());
262
- if (isLocal()) {
263
- moduleIds.push(...getAllDevelopmentModulesIds());
264
- moduleIds = Array.from(new Set(moduleIds));
265
- }
266
-
267
- for (let moduleId of moduleIds) {
268
- watchModuleCalls(moduleId);
269
- }
270
-
271
- let newCalls: { call: CallSpec; functionSpec: FunctionSpec }[] = [];
272
-
273
- if (watcher.triggeredByChanges) {
274
- for (let path of watcher.triggeredByChanges.paths) {
275
- let callsOrResult = getPathIndex(path, 3);
276
- if (callsOrResult !== "Calls" && callsOrResult !== "Results") continue;
277
- if (getPathIndex(path, 1) !== "PathFunctionRunner") continue;
278
- if (getPathIndex(path, 0) !== domainName) continue;
279
-
280
- const callId = getPathIndex(path, 4);
281
- if (!callId) continue;
282
- let moduleId = getPathIndex(path, 2);
283
- if (!moduleId) continue;
284
- if (!moduleIds.includes(moduleId)) continue;
285
-
286
- let moduleData = data()[moduleId];
287
-
288
- let result = atomicObjectRead(moduleData.Results[callId]);
289
- if (result) continue;
290
-
291
- let callData = atomicObjectRead(moduleData.Calls[callId]);
292
- if (!callData) continue;
293
-
294
- // Ignore invalid data
295
- if (callData.DomainName !== domainName) continue;
296
- if (callData.ModuleId !== moduleId) continue;
297
- if (callData.CallId !== callId) continue;
298
-
299
- const functionId = callData.FunctionId;
300
- let functionSpec = atomicObjectRead(moduleData.Sources[functionId]);
301
- if (!functionSpec) {
302
- if (!isSynced(moduleData.Sources[callData.FunctionId])) {
303
- continue;
304
- }
305
- if (isLocal()) {
306
- functionSpec = getDevFunctionSpecFromCall(callData);
307
- if (!functionSpec) continue;
308
- } else {
309
- console.warn(yellow(`Cannot call ${callData.DomainName}.${callData.ModuleId}.Sources.${callData.FunctionId} because it is not deployed`));
310
- continue;
311
- }
312
- }
313
-
314
- // If we haven't synced the result, we can't know if it is ready or not
315
- // (we'll run anyways once it is synced). Most other parts are immutable,
316
- // but the result is by definition not, so... it is one of the values we check!
317
- if (!isSynced(moduleData.Results[callId])) continue;
318
-
319
- if (runningCalls.has(callId)) continue;
320
- runningCalls.add(callId);
321
-
322
- let limitCount = queueLimitCounts.get(callId) || 0;
323
- limitCount++;
324
- queueLimitCounts.set(callId, limitCount);
325
- // NOTE: Calls are event writes, so... they should just clean themselves up, after we ignore them for long enough.
326
- if (limitCount >= MAX_QUEUE_COUNT) {
327
- // Only error the first time, as we don't need need that many errors
328
- if (limitCount === MAX_QUEUE_COUNT) {
329
- console.error(`Tried to requeue a function to run too many times (${limitCount}) for ${getDebugName(callData, functionSpec, true)}. This is NOT due to cascading reads. It might be due to repeated rejections?`);
330
- }
331
- continue;
332
- }
333
-
334
- if (PathFunctionRunner.DEBUG_CALLS) {
335
- console.log(`QUEUING ${getDebugName(callData, functionSpec, true)}`);
336
- let resultsPath = getProxyPath(() => moduleData.Results[callId]);
337
- let history = authorityStorage.getValuePlusHistory(resultsPath);
338
- console.log(` History: ${history.length}`);
339
- for (let { valid, time, canGCValue } of history) {
340
- console.log(` ${valid ? "✅" : "❌"} ${debugTime(time)} ${canGCValue ? "value is undefined" : ""}`);
341
- }
342
- }
343
- newCalls.push({ call: callData, functionSpec });
344
- }
345
- }
346
-
347
- // Better run first calls first, or else we will just pointlessly cause rejections.
348
- newCalls.sort((a, b) => compareTime(a.call.runAtTime, b.call.runAtTime));
349
- for (let { call, functionSpec } of newCalls) {
350
- // We don't need wait for all pending synced accesses to finish.
351
- // Worst case we have an old functionSpec, and then runCall has to loop again.
352
- // Otherwise the call request is (or should be) atomic, so it this should just work...
353
- // - Or if it is rejected, we will detect it has no result, and run it again.
354
- // NOTE: We run all calls in parallel, so we can load data in parallel. This is a bit dangerous, and might
355
- // result in out of order execution, but... we commit at runTimes, so, this should eventually
356
- // resolve itself due to rejections (which will change the call result, which SHOULD cause us to rerun again).
357
- // - This is a bit dangerous, as we might overwhelm ourself. We should address this one we support
358
- // FunctionRunner sharding.
359
- outstandingCalls++;
360
- let runCallPromise = self.runCall(call, functionSpec);
361
- void runCallPromise.finally(() => {
362
- outstandingCalls--;
363
- // IMPORTANT! We remove the call, NOT for GC, but! Because if the call writes are rejected (due to contention), then the Result will also be rejected, changing it back to undefined, so (as long as little enough time has passed), we will run the call again! Or rather, we will try to, as long as runningCalls doesn't have the value!
364
- runningCalls.delete(call.CallId);
365
- });
366
- logErrors(runCallPromise);
367
- }
368
- },
369
- });
370
- registerDynamicResource("paths|PathFunctionRunner.watches", () => watcher.lastWatches.paths.size);
371
- registerDynamicResource("paths|PathFunctionRunner.outstandingCalls", () => outstandingCalls);
372
-
373
- // If local, periodically check all modules
374
- // NOTE: `yarn deploy` commits the moduleIds to the database, but calls do not, as this would require either an extra
375
- // read, or an extra write. So this is somewhat a hack, but also fairly safe.
376
- if (isLocal()) {
377
- let prevModuleIds = new Set<string>();
378
- runInfinitePoll(timeInSecond * 15, () => {
379
- let moduleIds = getAllDevelopmentModulesIds();
380
- let changed = false;
381
- for (let moduleId of moduleIds) {
382
- if (!prevModuleIds.has(moduleId)) {
383
- changed = true;
384
- break;
385
- }
386
- }
387
- prevModuleIds = new Set(moduleIds);
388
- if (changed) {
389
- watcher.explicitlyTrigger();
390
- }
391
- });
392
- }
393
- }
394
-
395
-
396
- @measureFnc
397
- private async getExportsFromSpec(spec: FunctionSpec): Promise<unknown> {
398
- let module = await getModuleFromSpec(spec);
399
- return module.exports;
400
- }
401
-
402
- private getSecondaryShardDelay(callPath: CallSpec): number {
403
- let { shardRange, secondaryShardRange } = this.config;
404
- if (!secondaryShardRange) return 0;
405
- let fraction = __getRoutingHash(callPath.CallId);
406
- // It isn't secondary if it is primary
407
- if (shardRange.startFraction <= fraction && fraction < shardRange.endFraction) return 0;
408
- if (!(secondaryShardRange.startFraction <= fraction && fraction < secondaryShardRange.endFraction)) return 0;
409
- let fractionDistanceToPrimary = Math.min(
410
- Math.abs(fraction - shardRange.startFraction),
411
- Math.abs(fraction - shardRange.endFraction),
412
- );
413
- return Math.min(
414
- // No more than 50% of accepted change, otherwise it might not be accepted
415
- MAX_ACCEPTED_CHANGE_AGE * 0.5,
416
- // Default values in case MAX_ACCEPTED_CHANGE_AGE is VERY high (minutes, or even hours)
417
- 30_000 + 10_000 * fractionDistanceToPrimary,
418
- // Wait 5000 seconds, then start
419
- 5000 + MAX_ACCEPTED_CHANGE_AGE * fractionDistanceToPrimary * 0.05 / (shardRange.endFraction - shardRange.startFraction),
420
- );
421
- };
422
-
423
- private callsRemainingInTick = cache((wait: true) => {
424
- // NOTE: This gives us 100K calls per second, which is more than we can run anyways.
425
- // We just need to wait a bit, otherwise the IO loop can never run, and so our finished calls
426
- // can never actually finish, until eventually they are too late to finish and get rejected!
427
- let tickDone = delay(10);
428
- void tickDone.finally(() => {
429
- this.callsRemainingInTick.clear(true);
430
- });
431
- return { calls: 1000, tickDone };
432
- });
433
- private async letIOClear(): Promise<void> {
434
- while (true) {
435
- let callsObj = this.callsRemainingInTick(true);
436
- callsObj.calls--;
437
- if (callsObj.calls >= 0) return;
438
- await callsObj.tickDone;
439
- }
440
- }
441
-
442
- private async runCall(callPath: CallSpec, functionSpec: FunctionSpec): Promise<void> {
443
- const { Querysub } = await import("../4-querysub/Querysub");
444
-
445
- const PermissionsChecker = this.config.PermissionsChecker;
446
- let skipPermissions = <T>(code: () => T) => code();
447
- if (PermissionsChecker) {
448
- skipPermissions = PermissionsChecker.skipPermissionsChecks.bind(PermissionsChecker);
449
- }
450
-
451
- if (!doesMatch(callPath.filterable, this.config.filterSelector)) {
452
- return;
453
- }
454
-
455
- await this.letIOClear();
456
-
457
- let secondaryDelay = this.getSecondaryShardDelay(callPath);
458
- if (secondaryDelay) {
459
- async function getIsFinished() {
460
- return await proxyWatcher.commitFunction({
461
- watchFunction() {
462
- let syncedModule = skipPermissions(() =>
463
- functionSchema()[callPath.DomainName].PathFunctionRunner[callPath.ModuleId]
464
- );
465
- return !!atomicObjectRead(syncedModule.Results[callPath.CallId]);
466
- },
467
- });
468
- }
469
- // Call once, so we are already watching the
470
- await delay(secondaryDelay);
471
- let isFinished = await getIsFinished();
472
- if (isFinished) return;
473
- if (PathFunctionRunner.DEBUG_CALLS) {
474
- let fraction = __getRoutingHash(callPath.CallId);
475
- console.log(`${yellow("Function run fallback")} (primary servery failed to run function) after ${formatTime(secondaryDelay)}. Fraction ${fraction.toFixed(3)}. Primary ${this.config.shardRange.startFraction.toFixed(3)} to ${this.config.shardRange.endFraction.toFixed(3)}. Secondary ${this.config.secondaryShardRange?.startFraction.toFixed(3)} to ${this.config.secondaryShardRange?.endFraction.toFixed(3)}. for ${getDebugName(callPath, functionSpec, true)}`);
476
- }
477
- }
478
-
479
- PathFunctionRunner.RUN_START_COUNT++;
480
- if (PathFunctionRunner.DEBUG_CALLS) {
481
- console.log(`STARTING ${getDebugName(callPath, functionSpec, true)}`);
482
- }
483
- let startTime = Date.now();
484
- let runCount = 0;
485
-
486
- let finalWrites: PathValue[] | undefined;
487
-
488
- let evalTime = 0;
489
- let retries = 0;
490
-
491
- let nooped = false;
492
-
493
- while (true) {
494
- let isFunctionSpecOutdated = false;
495
- try {
496
-
497
- let moduleExports = await this.getExportsFromSpec(functionSpec);
498
-
499
- // ALWAYS get the permissions as well. This isn't perfect, as we could very well access data in other files, but... it helps
500
- // reduce extra iterations inside of commitFunction...
501
- await getModuleFromSpec({ ...functionSpec, exportPathStr: getExportPath(PERMISSIONS_FUNCTION_ID), FunctionId: PERMISSIONS_FUNCTION_ID, });
502
-
503
- let exportPath = getPathFromStr(functionSpec.exportPathStr);
504
- let exportObj = moduleExports as any;
505
- for (let path of exportPath) {
506
- exportObj = exportObj[path];
507
- }
508
- let baseFunction = exportObj as Function;
509
-
510
- if (typeof baseFunction !== "function") {
511
- throw new Error(`Export at ${JSON.stringify(exportPath.join("."))} was not a function (it was ${typeof baseFunction})`);
512
- }
513
-
514
- let devFunctionMetadata: FunctionMetadata | undefined;
515
-
516
- let devModule = getDevelopmentModule(functionSpec.ModuleId);
517
- if (devModule) {
518
- let schemaObj = getSchemaObject(devModule);
519
- devFunctionMetadata = schemaObj?.functionMetadata?.[functionSpec.FunctionId];
520
- }
521
-
522
- await proxyWatcher.commitFunction({
523
- canWrite: true,
524
- debugName: getDebugName(callPath, functionSpec),
525
- runAtTime: callPath.runAtTime,
526
- getPermissionsCheck: PermissionsChecker && (() => new PermissionsChecker(callPath)),
527
- nestedCalls: "inline",
528
- temporary: true,
529
- maxLocksOverride: devFunctionMetadata ? devFunctionMetadata.maxLocksOverride : functionSpec.maxLocksOverride,
530
- watchFunction: function runCallWatcher() {
531
- runCount++;
532
- if (PathFunctionRunner.DEBUG_CALLS) {
533
- console.log(`Evaluating (try count ${runCount}) ${getDebugName(callPath, functionSpec, true)}`);
534
- }
535
- if (PathFunctionRunner.DEBUG_CALL_TRIGGERS && runCount > PathFunctionRunner.DEBUG_WATCHES_THRESHOLD) {
536
- // NOTE: If this happens a few times during initial loading it is fine. If it happens a lot... and with values MUCH
537
- // greater than 2... it is likely a problem, and your functions might be doing cascading loading, which can take
538
- // a very long time (ex, `for(let i = 0; i < 1000; i++) if (!arr[i]) return`)
539
- console.log(`> 2 call evaluations (try count ${runCount}) ${getDebugName(callPath, functionSpec, true)}`);
540
- console.log(yellow(` Paths changed`));
541
- for (let path of proxyWatcher.getTriggeredWatcher().triggeredByChanges?.paths || []) {
542
- console.log(yellow(` ${path}`));
543
- }
544
- }
545
- if (runCount > PathFunctionRunner.MAX_WATCH_LOOPS) {
546
- throw new Error(`MAX_WATCH_LOOPS exceeded for ${getDebugName(callPath, functionSpec, true)}. All accesses have to be consistent. So Querysub.time() instead of Date.now() and Querysub.nextId() instead of nextId() / Math.random(). If you need multiple random numbers, keep track of an index, and pass it to Querysub.nextId() for the nth random number.`);
547
- }
548
-
549
- // We NEED to depend on the function spec, so spec updates can be atomic (which is needed to deploy an update
550
- // to many nodes at once, without having any period where multiple versions both commit values).
551
- let syncedModule = skipPermissions(() =>
552
- functionSchema()[callPath.DomainName].PathFunctionRunner[callPath.ModuleId]
553
- );
554
-
555
- // Make sure we are running the latest function (by checking it here, we also lock the values,
556
- // so if it is updated we atomically rerun / reject all calls which were racing with us).
557
- if (!isLocal()) {
558
- let syncedSpec = skipPermissions(() =>
559
- atomicObjectRead(syncedModule.Sources[callPath.FunctionId])
560
- );
561
- if (!syncedSpec) {
562
- throw new Error(`Function spec not found for ${getDebugName(callPath, functionSpec, true)}`);
563
- }
564
-
565
- // (We also need to depend on the RIGHT function spec).
566
- if (
567
- syncedSpec && (
568
- syncedSpec.DomainName !== functionSpec.DomainName
569
- || syncedSpec.ModuleId !== functionSpec.ModuleId
570
- || syncedSpec.FilePath !== functionSpec.FilePath
571
- || syncedSpec.FunctionId !== functionSpec.FunctionId
572
- || syncedSpec.exportPathStr !== functionSpec.exportPathStr
573
- || syncedSpec.FilePath !== functionSpec.FilePath
574
- || syncedSpec.gitRef !== functionSpec.gitRef
575
- || syncedSpec.gitURL !== functionSpec.gitURL
576
- || syncedSpec.maxLocksOverride !== functionSpec.maxLocksOverride
577
- )
578
- ) {
579
- isFunctionSpecOutdated = true;
580
- functionSpec = {
581
- DomainName: syncedSpec.DomainName,
582
- ModuleId: syncedSpec.ModuleId,
583
- FilePath: syncedSpec.FilePath,
584
- FunctionId: syncedSpec.FunctionId,
585
- exportPathStr: syncedSpec.exportPathStr,
586
- gitRef: syncedSpec.gitRef,
587
- gitURL: syncedSpec.gitURL,
588
- };
589
- return;
590
- }
591
- }
592
-
593
- let evalTimeStart = Date.now();
594
- try {
595
- let args = parseArgs(callPath);
596
- overrideCurrentCall({ spec: callPath, fnc: functionSpec, }, () => {
597
- baseFunction(...args);
598
- });
599
- } finally {
600
- evalTime += Date.now() - evalTimeStart;
601
- }
602
-
603
- // NOTE: The results are only temporarily stored, but serve as a way to let clients
604
- // know figure out the dependencies for the call, which makes function call prediction
605
- // possible.
606
- skipPermissions(() => {
607
- let result = doProxyOptions({ forceReadLatest: true }, () => atomicObjectRead(syncedModule.Results[callPath.CallId]));
608
- if (result) {
609
- nooped = true;
610
- // It is important to NOOP if it has already been written. Otherwise there could be multiple
611
- // runs of the same function, which could easily vary (if they use Math.random(), etc).
612
- // WHICH, as the write time is predetermined, would cause ValuePaths with equal times, but
613
- // different values (which breaks a lot of assumptions much of the code depends on! such
614
- // as locks, etc).
615
- if (!secondaryDelay && Querysub.isAllSynced()) {
616
- console.warn(`Skipping function write, as it has already been written to by another FunctionRunner.`);
617
- }
618
- return;
619
- }
620
- nooped = false;
621
- syncedModule.Results[callPath.CallId] = atomicObjectWrite({
622
- runCount: runCount,
623
- timeTaken: Date.now() - evalTimeStart,
624
- evalTime,
625
- });
626
- proxyWatcher.setEventPath(() => syncedModule.Results[callPath.CallId]);
627
- });
628
-
629
- },
630
- }, {
631
- onWritesCommitted(writes) {
632
- finalWrites = writes;
633
- },
634
- });
635
- } catch (e: any) {
636
- if (isFunctionSpecOutdated) continue;
637
-
638
- // NOTE: We are changing our paradigm to reject early, rather than retry. This is more stable. You can't really commit
639
- // important values and then just leave, unless those values have no locks. This helps prevent values from being
640
- // in different states on different machines, by just rejecting the call.
641
- // if (e.message.includes("MAX_CHANGE_AGE_EXCEEDED") && retries < 10 && evalTime < MAX_ACCEPTED_CHANGE_AGE) {
642
- // // NOTE: This should never really happen. It is only really if our PathValueServer goes down for a minute or so,
643
- // // so we don't just drop all of our function calls.
644
- // // - This is why we log every time, because no user actions should be able to result in this happening frequently.
645
- // console.error(red(`RETRYING due to MAX_CHANGE_AGE_EXCEEDED. Hopefully the server will be more responsive this time (retry ${retries}): ${getDebugName(callPath, functionSpec, true)}`));
646
- // // NOTE: Updating the time is fine, and won't break prediction rejections, as we reject based on path, not time.
647
- // // (It might result in the prediction being wrong, but it is better than the user action being completely removed).
648
- // callPath = { ...callPath, runAtTime: getNextTime() };
649
- // retries++;
650
- // continue;
651
- // }
652
- //if (PathFunctionRunner.DEBUG_CALLS)
653
- {
654
- console.log(`ERROR ${getDebugName(callPath, functionSpec, true)}`, e.stack);
655
- }
656
-
657
- // NOTE: commitFunction should never throw? If the server is dead, it still won't throw, so... we aren't try/catching it,
658
- // as I don't see how retrying would help (the same error would almost certainly happen again and again).
659
- await proxyWatcher.commitFunction({
660
- canWrite: true,
661
- // NOTE: We don't set runAtTime, because the rejection might be due to the call being too old
662
- debugName: `error (${getDebugName(callPath, functionSpec)})`,
663
- eventWrite: true,
664
- doNotStoreWritesAsPredictions: true,
665
- watchFunction() {
666
- let syncedModule = functionSchema()[callPath.DomainName].PathFunctionRunner[callPath.ModuleId];
667
- syncedModule.Results[callPath.CallId] = atomicObjectWrite({
668
- error: e.stack || "",
669
- runCount: runCount,
670
- timeTaken: Date.now() - startTime,
671
- evalTime,
672
- });
673
- },
674
- }, {
675
- onWritesCommitted(writes) {
676
- finalWrites = writes;
677
- },
678
- });
679
- }
680
- if (isFunctionSpecOutdated) continue;
681
- break;
682
- }
683
-
684
- if (PathFunctionRunner.DEBUG_CALLS) {
685
- console.log(`FINISHED${nooped ? " (skipped)" : ""} ${getDebugName(callPath, functionSpec, true)}, writes: ${finalWrites?.length}, took ${blue(formatTime(Date.now() - startTime))}`);
686
- }
687
-
688
- let wallTime = Date.now() - startTime;
689
- let syncTime = wallTime - evalTime;
690
-
691
-
692
- console.info("Finished FunctionRunner function", {
693
- ...callPath, argsEncoded: "", functionSpec,
694
- wallTime, syncTime, evalTime,
695
- loops: runCount,
696
- });
697
- PathFunctionRunner.RUN_FINISH_COUNT++;
698
- }
699
- }
700
-
701
- export async function preloadFunctions(specs: FunctionSpec[], progress?: DeployProgress) {
702
- progress?.({ section: "Finding FunctionPreloadControllers", progress: 0 });
703
- let nodeIds = await getControllerNodeIdList(FunctionPreloadController);
704
- progress?.({ section: "Finding FunctionPreloadControllers", progress: 1 });
705
- await Promise.allSettled(nodeIds.map(async nodeObj => {
706
- let nodeId = nodeObj.nodeId;
707
- let controller = FunctionPreloadController.nodes[nodeId];
708
- let section = `${nodeObj.entryPoint}:${nodeId}|Preloading Functions`;
709
- progress?.({ section, progress: 0 });
710
- console.log(blue(`Preloading functions on ${String(nodeId)}`));
711
- await errorToUndefined(controller.preloadFunctions(specs));
712
- progress?.({ section, progress: 1 });
713
- console.log(blue(`Finished preloading functions on ${String(nodeId)}`));
714
- }));
715
- }
716
-
717
- export function getDevFunctionSpecFromCall(call: {
718
- DomainName: string;
719
- ModuleId: string;
720
- FunctionId: string;
721
- }): FunctionSpec | undefined {
722
- let domainName = call.DomainName;
723
- let moduleId = call.ModuleId;
724
- let functionId = call.FunctionId;
725
-
726
- let devModule = getDevelopmentModule(moduleId);
727
- if (!devModule) {
728
- console.warn(yellow(`Cannot call ${domainName}.${moduleId}.Sources.${functionId} because the module is not deployed and not referenced in deploy.ts`));
729
- return undefined;
730
- }
731
- let filePath = getModuleRelativePath(devModule);
732
- let gitURL = getGitURLSync(undefined);
733
- let gitRef = getGitRefSync(undefined);
734
- return {
735
- DomainName: domainName,
736
- ModuleId: moduleId,
737
- FunctionId: functionId,
738
- exportPathStr: getExportPath(functionId),
739
- FilePath: filePath,
740
- gitURL,
741
- gitRef,
742
- };
743
- }
744
-
745
- class FunctionPreloaderBase {
746
- async preloadFunctions(specs: FunctionSpec[]) {
747
- if (isLocal()) return;
748
- for (let spec of specs) {
749
- await getModuleFromSpec(spec);
750
- }
751
- }
752
- }
753
-
754
- const FunctionPreloadController = SocketFunction.register(
755
- "FunctionPreloader-c966a1a6-5d3f-4453-bd03-ae84b574ec00",
756
- new FunctionPreloaderBase(),
757
- () => ({
758
- preloadFunctions: {},
759
- }),
760
- () => ({
761
- hooks: [requiresNetworkTrustHook],
762
- }),
763
- {
764
- noAutoExpose: true,
765
- }
766
- );
1
+ import { delay, runInfinitePoll } from "socket-function/src/batching";
2
+ import { cache } from "socket-function/src/caching";
3
+ import { blue, magenta, yellow } from "socket-function/src/formatting/logColors";
4
+ import { timeInHour, timeInSecond } from "socket-function/src/misc";
5
+ import { measureFnc } from "socket-function/src/profiling/measure";
6
+ import { registerDynamicResource } from "../diagnostics/trackResources";
7
+ import { errorToUndefined, logErrors } from "../errors";
8
+ import { getPathFromStr, getPathIndex } from "../path";
9
+ import { rawSchema } from "../2-proxy/pathDatabaseProxyBase";
10
+ import { getProxyPath } from "../2-proxy/pathValueProxy";
11
+ import { atomicObjectRead, atomicObjectWrite, doProxyOptions, isProxyBlockedByOrder, isSynced, proxyWatcher } from "../2-proxy/PathValueProxyWatcher";
12
+ import { authorityStorage, compareTime, debugTime, MAX_ACCEPTED_CHANGE_AGE, PathValue, Time } from "../0-path-value-core/pathValueCore";
13
+ import { getModuleFromSpec } from "./pathFunctionLoader";
14
+ import debugbreak from "debugbreak";
15
+ import { parseArgs } from "./PathFunctionHelpers";
16
+ import { FunctionMetadata, PERMISSIONS_FUNCTION_ID, addRoutingPrefixForDeploy, getAllDevelopmentModulesIds, getDevelopmentModule, getExportPath, getModuleRelativePath, getSchemaObject } from "./syncSchema";
17
+ import { formatTime } from "socket-function/src/formatting/format";
18
+ import { getControllerNodeIdList, set_debug_getFunctionRunnerShards } from "../-g-core-values/NodeCapabilities";
19
+ import { FilterSelector, Filterable, doesMatch } from "../misc/filterable";
20
+ import { SocketFunction } from "socket-function/SocketFunction";
21
+ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
22
+ import { getDomain, isPublic } from "../config";
23
+ import { getGitRefSync, getGitURLSync } from "../4-deploy/git";
24
+ import type { DeployProgress } from "../4-deploy/deployFunctions";
25
+ import { getRoutingOverride, getRoutingOverridePart } from "../0-path-value-core/PathRouterRouteOverride";
26
+ setImmediate(() => import("../4-querysub/Querysub"));
27
+
28
+ export const functionSchema = rawSchema<{
29
+ [domainName: string]: {
30
+ PathFunctionRunner: {
31
+ [ModuleId: string]: {
32
+ Data: unknown;
33
+ Sources: { [FunctionId: string]: FunctionSpec | undefined; };
34
+ Calls: { [CallId: string]: CallSpec | undefined; };
35
+ Results: { [CallId: string]: FunctionResult | undefined; };
36
+ };
37
+ };
38
+ };
39
+ }>();
40
+ void Promise.resolve().then(() => {
41
+ for (let moduleId of getAllDevelopmentModulesIds()) {
42
+ addRoutingPrefixForDeploy(getProxyPath(() => functionSchema()[getDomain()].PathFunctionRunner[moduleId].Calls));
43
+ addRoutingPrefixForDeploy(getProxyPath(() => functionSchema()[getDomain()].PathFunctionRunner[moduleId].Results));
44
+ }
45
+ });
46
+
47
+ export function commitCall(call: CallSpec) {
48
+ let domainName = call.DomainName;
49
+ let moduleId = call.ModuleId;
50
+ let callId = call.CallId;
51
+ proxyWatcher.writeOnly({
52
+ canWrite: true,
53
+ eventWrite: true,
54
+ doNotStoreWritesAsPredictions: true,
55
+ // NOTE: We write the call in the present, it is callSpec.runAtTime that is in the past. No one cares
56
+ // when the call was added, only when it wants to run at.
57
+ watchFunction: function writeCall() {
58
+ functionSchema()[domainName].PathFunctionRunner[moduleId].Calls[callId] = atomicObjectWrite(call);
59
+ },
60
+ });
61
+ }
62
+
63
+ export const DEPTH_TO_DATA = 4;
64
+ export const DOMAIN_INDEX = 0;
65
+ export const MODULE_INDEX = 2;
66
+
67
+ export function getSchemaPartsFromPath(path: string): { domainName: string; moduleId: string } | undefined {
68
+ let parts = getPathFromStr(path);
69
+ let domainName = parts[0];
70
+ let moduleId = parts[2];
71
+ if (!domainName || !moduleId) return undefined;
72
+ return { domainName, moduleId };
73
+ }
74
+
75
+ export type FunctionSpec = {
76
+ DomainName: string;
77
+ ModuleId: string;
78
+ FunctionId: string;
79
+ exportPathStr: string;
80
+ FilePath: string;
81
+ gitURL: string;
82
+ /** Ex, the hash */
83
+ gitRef: string;
84
+
85
+ maxLocksOverride?: number;
86
+ };
87
+
88
+ export interface CallSpec {
89
+ DomainName: string;
90
+ ModuleId: string;
91
+ CallId: string;
92
+ FunctionId: string;
93
+ // NOTE: This is no longer JSON. Use parseArgs to decode it
94
+ argsEncoded: string;
95
+ callerMachineId: string;
96
+ callerIP: string;
97
+ runAtTime: Time;
98
+
99
+ // Not just used for debugging, also used to add special proxy-related warnings.
100
+ fromProxy?: string;
101
+
102
+ filterable?: Filterable;
103
+ }
104
+ export function debugCallSpec(spec: CallSpec): string {
105
+ return `${spec.DomainName}/${spec.ModuleId}/${spec.FunctionId}`;
106
+ }
107
+ export type FunctionResult = ({
108
+
109
+ } | {
110
+ error: string;
111
+ }) & {
112
+ timeTaken: number;
113
+ evalTime: number;
114
+ lastInternalLoopCount: number;
115
+ outerLoopCount: number;
116
+ totalInternalLoopCount: number;
117
+ totalTime: number;
118
+ };
119
+
120
+ let currentCallSpec: { spec: CallSpec; fnc: FunctionSpec; } | undefined;
121
+ export function getCurrentCall() {
122
+ if (!currentCallSpec) {
123
+ debugbreak(2);
124
+ debugger;
125
+ throw new Error("Not presently in a call, and so cannot get call");
126
+ }
127
+ return currentCallSpec.spec;
128
+ }
129
+ export function getCurrentCallAllowUndefined() {
130
+ return currentCallSpec?.spec;
131
+ }
132
+ export function getCurrentCallObj() {
133
+ return currentCallSpec;
134
+ }
135
+ /** NOTE: We require the FunctionSpec so we know what has is loaded, to identify the
136
+ * correct schema. This should be available, but if it isn't... we MIGHT be able
137
+ * to work around it, if we can be assured the schema won't be used?
138
+ * - This is so pre-loading of functions (which might happen without a deploy, if the deploy script
139
+ * is killed part of the way through), doesn't change schemas until the deploy actually
140
+ * updates the underlying functions.
141
+ */
142
+ export function overrideCurrentCall<T>(config: { spec: CallSpec; fnc: FunctionSpec; }, code: () => T) {
143
+ let prev = currentCallSpec;
144
+ currentCallSpec = config;
145
+ try {
146
+ return code();
147
+ } finally {
148
+ currentCallSpec = prev;
149
+ }
150
+ }
151
+
152
+ function getDebugName(call: CallSpec, functionSpec: FunctionSpec | undefined, colored = false) {
153
+ if (!functionSpec) {
154
+ return `${call.DomainName}:${call.ModuleId}|${call.FunctionId}|${debugTime(call.runAtTime)}`;
155
+ }
156
+ let pathName = getPathFromStr(functionSpec.exportPathStr).slice(1).join(".");
157
+ let mainPart = `${call.DomainName}:${functionSpec.FilePath}|${pathName}|${debugTime(call.runAtTime)}`;
158
+ if (colored) {
159
+ return `${magenta(mainPart)}`;
160
+ } else {
161
+ return `${mainPart}`;
162
+ }
163
+ }
164
+
165
+ type PermissionsCheckType = {
166
+ new(callerMachineId: { callerMachineId: string; callerIP: string; }): {
167
+ checkPermissions(path: string): { permissionsPath: string; allowed: boolean; };
168
+ };
169
+ skipPermissionsChecks: <T>(code: () => T) => T;
170
+ }
171
+
172
+ export type DebugFunctionShardInfo = {
173
+ domainName: string;
174
+ shardRange: { startFraction: number, endFraction: number };
175
+ secondaryShardRange?: { startFraction: number, endFraction: number };
176
+ };
177
+ let debugFunctionRunnerShards: DebugFunctionShardInfo[] = [];
178
+ export function debug_getFunctionRunnerShards() {
179
+ return debugFunctionRunnerShards;
180
+ }
181
+ setImmediate(() => {
182
+ set_debug_getFunctionRunnerShards(debug_getFunctionRunnerShards);
183
+ });
184
+
185
+ type CallStats = {
186
+ outerLoopCount: number;
187
+ totalInternalLoopCount: number;
188
+ lastInternalLoopCount: number;
189
+ isInsideRunCall: boolean;
190
+ timeTaken: number;
191
+ evalTime: number;
192
+ firstQueuedTime: number;
193
+ };
194
+
195
+ export class PathFunctionRunner {
196
+ public static RUN_START_COUNT = 0;
197
+ public static RUN_FINISH_COUNT = 0;
198
+ public static DEBUG_CALLS = false;
199
+ public static DEBUG_CALL_TRIGGERS = false;
200
+ public static DEBUG_WATCHES_THRESHOLD = 5;
201
+ public static MAX_WATCH_LOOPS = 1000;
202
+
203
+ constructor(private config: {
204
+ domainName: string;
205
+ shardRange: { startFraction: number, endFraction: number };
206
+ secondaryShardRange?: { startFraction: number, endFraction: number };
207
+ PermissionsChecker: PermissionsCheckType | undefined;
208
+ filterSelector?: FilterSelector;
209
+ }) {
210
+ SocketFunction.expose(FunctionPreloadController);
211
+ debugFunctionRunnerShards.push({
212
+ domainName: config.domainName,
213
+ shardRange: config.shardRange,
214
+ secondaryShardRange: config.secondaryShardRange,
215
+ });
216
+ logErrors(this.startWatching());
217
+ }
218
+
219
+ // Calls runCall on any calls that match our domain + shard range
220
+ private async startWatching() {
221
+ const { Querysub } = await import("../4-querysub/Querysub");
222
+ let { shardRange, secondaryShardRange } = this.config;
223
+
224
+ // We will use PathValueProxyWatcher to watch the paths
225
+ // Maintain only one outstanding runCall per callId
226
+ // - Once it finishes, we can trigger another call, if we notice the result is not set.
227
+ // - AS we should get write prediction for the result, we can rerun our proxy check
228
+ // loop immediately.
229
+
230
+ let fullFraction = shardRange;
231
+ if (secondaryShardRange) {
232
+ fullFraction = {
233
+ startFraction: Math.min(shardRange.startFraction, secondaryShardRange.startFraction),
234
+ endFraction: Math.max(shardRange.endFraction, secondaryShardRange.endFraction),
235
+ };
236
+ }
237
+
238
+ let config = this.config;
239
+
240
+ let self = this;
241
+
242
+ let outstandingCalls = 0;
243
+
244
+ let watchModuleCalls = cache((moduleId: string) => {
245
+ // Object.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Calls);
246
+ // Object.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Results);
247
+ Querysub.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Calls, fullFraction);
248
+ Querysub.keys(functionSchema()[domainName].PathFunctionRunner[moduleId].Results, fullFraction);
249
+ });
250
+
251
+ let runningCalls = new Set<string>();
252
+
253
+ // Rarely we might need to queue a function multiple times, when we are late to receive rejections.
254
+ // HOWEVER, after enough times, we should stop, as we will probably just infinitely queue it.
255
+ // For debugging, we should never really be having to re-queue it. And if we requeue it a lot, it makes the logs larger, which makes it harder to debug it!
256
+ const MAX_QUEUE_COUNT = isPublic() ? 100 : 14;
257
+ let queueLimitCounts = new Map<string, number>();
258
+ // Clear every hour, so we don't leave
259
+ runInfinitePoll(timeInHour, () => queueLimitCounts.clear());
260
+
261
+ const domainName = config.domainName;
262
+ const watcher = proxyWatcher.createWatcher({
263
+ trackTriggers: true,
264
+ skipPermissionsCheck: true,
265
+ hasSideEffects: true,
266
+ // Maybe allow running with unsynced reads? This might be good, because we don't need all reads
267
+ // to detect calls. BUT, it might also be bad, because it might cause us to rerun in a cascading
268
+ // fashion? (As in, be really slow).
269
+ // - Shouldn't be needed, because writes don't take that long
270
+ // - Could fix bugs where we never get a read, and therefore all function calls fails to run?
271
+ // allowUnsyncedReads: true
272
+ watchFunction: function findFunctionsToCall() {
273
+ // TODO: Maybe abstract the "preserve watchers" stuff,
274
+ // so that it is automatically done at the start of our trigger.
275
+ const watcher = proxyWatcher.getTriggeredWatcher();
276
+
277
+ // Preserve previous watches explicitly (so we just do a diff, instead of accessing every
278
+ // value every time).
279
+ proxyWatcher.reuseLastWatches();
280
+
281
+ let data = () => functionSchema()[domainName].PathFunctionRunner;
282
+
283
+ let moduleIds = Object.keys(data());
284
+ if (!isPublic()) {
285
+ moduleIds.push(...getAllDevelopmentModulesIds());
286
+ moduleIds = Array.from(new Set(moduleIds));
287
+ }
288
+
289
+ for (let moduleId of moduleIds) {
290
+ watchModuleCalls(moduleId);
291
+ }
292
+
293
+ let newCalls: { call: CallSpec; functionSpec: FunctionSpec }[] = [];
294
+
295
+ if (watcher.triggeredByChanges) {
296
+ for (let path of watcher.triggeredByChanges.paths) {
297
+ let callsOrResult = getPathIndex(path, 3);
298
+ if (callsOrResult !== "Calls" && callsOrResult !== "Results") continue;
299
+ if (getPathIndex(path, 1) !== "PathFunctionRunner") continue;
300
+ if (getPathIndex(path, 0) !== domainName) continue;
301
+
302
+ const callId = getPathIndex(path, 4);
303
+ if (!callId) continue;
304
+ let moduleId = getPathIndex(path, 2);
305
+ if (!moduleId) continue;
306
+ if (!moduleIds.includes(moduleId)) continue;
307
+
308
+ let moduleData = data()[moduleId];
309
+
310
+ let result = atomicObjectRead(moduleData.Results[callId]);
311
+ if (result) continue;
312
+
313
+ let callData = atomicObjectRead(moduleData.Calls[callId]);
314
+ if (!callData) continue;
315
+
316
+ // Ignore invalid data
317
+ if (callData.DomainName !== domainName) continue;
318
+ if (callData.ModuleId !== moduleId) continue;
319
+ if (callData.CallId !== callId) continue;
320
+
321
+ const functionId = callData.FunctionId;
322
+ let functionSpec = atomicObjectRead(moduleData.Sources[functionId]);
323
+ if (!functionSpec) {
324
+ if (!isSynced(moduleData.Sources[callData.FunctionId])) {
325
+ continue;
326
+ }
327
+ if (!isPublic()) {
328
+ functionSpec = getDevFunctionSpecFromCall(callData);
329
+ if (!functionSpec) continue;
330
+ } else {
331
+ console.warn(yellow(`Cannot call ${callData.DomainName}.${callData.ModuleId}.Sources.${callData.FunctionId} because it is not deployed`));
332
+ continue;
333
+ }
334
+ }
335
+
336
+ // If we haven't synced the result, we can't know if it is ready or not
337
+ // (we'll run anyways once it is synced). Most other parts are immutable,
338
+ // but the result is by definition not, so... it is one of the values we check!
339
+ if (!isSynced(moduleData.Results[callId])) continue;
340
+
341
+ if (runningCalls.has(callId)) continue;
342
+ runningCalls.add(callId);
343
+
344
+ let limitCount = queueLimitCounts.get(callId) || 0;
345
+ limitCount++;
346
+ queueLimitCounts.set(callId, limitCount);
347
+ // NOTE: Calls are event writes, so... they should just clean themselves up, after we ignore them for long enough.
348
+ if (limitCount >= MAX_QUEUE_COUNT) {
349
+ // Only error the first time, as we don't need need that many errors
350
+ if (limitCount === MAX_QUEUE_COUNT) {
351
+ console.error(`(Likely a missing synced value) Tried to requeue a function to run too many times (${limitCount}) for ${getDebugName(callData, functionSpec, true)}. This is NOT due to cascading reads. It might be due to repeated rejections?`, {
352
+ callId
353
+ });
354
+
355
+ let stats = self.callStats.get(callId);
356
+ void self.finishCallWithError({
357
+ callSpec: callData,
358
+ functionSpec,
359
+ error: `Tried to requeue a function to run too many times (${limitCount}) for ${getDebugName(callData, functionSpec, true)}. This is NOT due to cascading reads. It might be due to repeated rejections?`,
360
+ runCount: stats?.lastInternalLoopCount || 0,
361
+ startTime: Date.now(),
362
+ evalTime: 0,
363
+ outerLoopCount: stats?.outerLoopCount || 0,
364
+ totalInternalLoopCount: stats?.totalInternalLoopCount || 0,
365
+ });
366
+ }
367
+ continue;
368
+ }
369
+
370
+ if (PathFunctionRunner.DEBUG_CALLS) {
371
+ console.log(`QUEUING ${getDebugName(callData, functionSpec, true)}`);
372
+ let resultsPath = getProxyPath(() => moduleData.Results[callId]);
373
+ let history = authorityStorage.getValuePlusHistory(resultsPath);
374
+ console.log(` History: ${history.length}`);
375
+ for (let { valid, time, canGCValue } of history) {
376
+ console.log(` ${valid ? "✅" : "❌"} ${debugTime(time)} ${canGCValue ? "value is undefined" : ""}`);
377
+ }
378
+ }
379
+ newCalls.push({ call: callData, functionSpec });
380
+ }
381
+ }
382
+
383
+ // Better run first calls first, or else we will just pointlessly cause rejections.
384
+ newCalls.sort((a, b) => compareTime(a.call.runAtTime, b.call.runAtTime));
385
+ for (let { call, functionSpec } of newCalls) {
386
+ // We don't need wait for all pending synced accesses to finish.
387
+ // Worst case we have an old functionSpec, and then runCall has to loop again.
388
+ // Otherwise the call request is (or should be) atomic, so it this should just work...
389
+ // - Or if it is rejected, we will detect it has no result, and run it again.
390
+ // NOTE: We run all calls in parallel, so we can load data in parallel. This is a bit dangerous, and might
391
+ // result in out of order execution, but... we commit at runTimes, so, this should eventually
392
+ // resolve itself due to rejections (which will change the call result, which SHOULD cause us to rerun again).
393
+ // - This is a bit dangerous, as we might overwhelm ourself. We should address this one we support
394
+ // FunctionRunner sharding.
395
+ outstandingCalls++;
396
+ let runCallPromise = self.runCall(call, functionSpec);
397
+ void runCallPromise.finally(() => {
398
+ outstandingCalls--;
399
+ // IMPORTANT! We remove the call, NOT for GC, but! Because if the call writes are rejected (due to contention), then the Result will also be rejected, changing it back to undefined, so (as long as little enough time has passed), we will run the call again! Or rather, we will try to, as long as runningCalls doesn't have the value!
400
+ runningCalls.delete(call.CallId);
401
+ });
402
+ logErrors(runCallPromise);
403
+ }
404
+ },
405
+ });
406
+ registerDynamicResource("paths|PathFunctionRunner.watches", () => watcher.lastWatches.paths.size);
407
+ registerDynamicResource("paths|PathFunctionRunner.outstandingCalls", () => outstandingCalls);
408
+
409
+ // If local, periodically check all modules
410
+ // NOTE: `yarn deploy` commits the moduleIds to the database, but calls do not, as this would require either an extra
411
+ // read, or an extra write. So this is somewhat a hack, but also fairly safe.
412
+ if (!isPublic()) {
413
+ let prevModuleIds = new Set<string>();
414
+ runInfinitePoll(timeInSecond * 15, () => {
415
+ let moduleIds = getAllDevelopmentModulesIds();
416
+ let changed = false;
417
+ for (let moduleId of moduleIds) {
418
+ if (!prevModuleIds.has(moduleId)) {
419
+ changed = true;
420
+ break;
421
+ }
422
+ }
423
+ prevModuleIds = new Set(moduleIds);
424
+ if (changed) {
425
+ watcher.explicitlyTrigger();
426
+ }
427
+ });
428
+ }
429
+ }
430
+
431
+
432
+ @measureFnc
433
+ private async getExportsFromSpec(spec: FunctionSpec): Promise<unknown> {
434
+ let module = await getModuleFromSpec(spec);
435
+ return module.exports;
436
+ }
437
+
438
+ private getSecondaryShardDelay(callPath: CallSpec): number {
439
+ let { shardRange, secondaryShardRange } = this.config;
440
+ if (!secondaryShardRange) return 0;
441
+ let fraction = getRoutingOverridePart(callPath.CallId)?.route;
442
+ if (fraction === undefined) throw new Error(`No routing override found for callId, so we can't determine if we should handle it. CallId: ${callPath.CallId}`);
443
+ // It isn't secondary if it is primary
444
+ if (shardRange.startFraction <= fraction && fraction < shardRange.endFraction) return 0;
445
+ if (!(secondaryShardRange.startFraction <= fraction && fraction < secondaryShardRange.endFraction)) return 0;
446
+ let fractionDistanceToPrimary = Math.min(
447
+ Math.abs(fraction - shardRange.startFraction),
448
+ Math.abs(fraction - shardRange.endFraction),
449
+ );
450
+ return Math.min(
451
+ // No more than 50% of accepted change, otherwise it might not be accepted
452
+ MAX_ACCEPTED_CHANGE_AGE * 0.5,
453
+ // Default values in case MAX_ACCEPTED_CHANGE_AGE is VERY high (minutes, or even hours)
454
+ 30_000 + 10_000 * fractionDistanceToPrimary,
455
+ // Wait 5000 seconds, then start
456
+ 5000 + MAX_ACCEPTED_CHANGE_AGE * fractionDistanceToPrimary * 0.05 / (shardRange.endFraction - shardRange.startFraction),
457
+ );
458
+ };
459
+
460
+ private callsRemainingInTick = cache((wait: true) => {
461
+ // NOTE: This gives us 100K calls per second, which is more than we can run anyways.
462
+ // We just need to wait a bit, otherwise the IO loop can never run, and so our finished calls
463
+ // can never actually finish, until eventually they are too late to finish and get rejected!
464
+ let tickDone = delay(10);
465
+ void tickDone.finally(() => {
466
+ this.callsRemainingInTick.clear(true);
467
+ });
468
+ return { calls: 1000, tickDone };
469
+ });
470
+ private async letIOClear(): Promise<void> {
471
+ while (true) {
472
+ let callsObj = this.callsRemainingInTick(true);
473
+ callsObj.calls--;
474
+ if (callsObj.calls >= 0) return;
475
+ await callsObj.tickDone;
476
+ }
477
+ }
478
+
479
+ private callStats = new Map<string, CallStats>();
480
+
481
+ private async runCall(callSpec: CallSpec, functionSpec: FunctionSpec): Promise<void> {
482
+ let callId = callSpec.CallId;
483
+ let stats = this.callStats.get(callId);
484
+ if (!stats) {
485
+ // HACK: Just completely reset the map after it gets big enough. This prevents us from crashing due to the map getting too large, as there's a limit to how large maps can get.
486
+ if (this.callStats.size > 1000 * 1000) {
487
+ this.callStats.clear();
488
+ }
489
+ stats = {
490
+ outerLoopCount: 0,
491
+ totalInternalLoopCount: 0,
492
+ lastInternalLoopCount: 0,
493
+ isInsideRunCall: false,
494
+ timeTaken: 0,
495
+ evalTime: 0,
496
+ firstQueuedTime: Date.now(),
497
+ };
498
+ this.callStats.set(callId, stats);
499
+ }
500
+
501
+ if (stats.isInsideRunCall) {
502
+ console.error(`Attempted to re-enter runCall for ${callId} while already inside runCall`);
503
+ return;
504
+ }
505
+
506
+ stats.isInsideRunCall = true;
507
+ stats.outerLoopCount++;
508
+ try {
509
+ await this.runCallBase(callSpec, functionSpec, stats);
510
+ } finally {
511
+ stats.isInsideRunCall = false;
512
+ }
513
+ }
514
+
515
+ private async runCallBase(callSpec: CallSpec, functionSpec: FunctionSpec, stats: CallStats): Promise<void> {
516
+ const { Querysub } = await import("../4-querysub/Querysub");
517
+
518
+ const PermissionsChecker = this.config.PermissionsChecker;
519
+ let skipPermissions = <T>(code: () => T) => code();
520
+ if (PermissionsChecker) {
521
+ skipPermissions = PermissionsChecker.skipPermissionsChecks.bind(PermissionsChecker);
522
+ }
523
+
524
+ if (!doesMatch(callSpec.filterable, this.config.filterSelector)) {
525
+ return;
526
+ }
527
+
528
+ console.info(`New function call: ${getDebugName(callSpec, functionSpec, true)}`, {
529
+ callId: callSpec.CallId,
530
+ timeId: callSpec.runAtTime.time,
531
+ functionId: callSpec.FunctionId,
532
+ moduleId: callSpec.ModuleId,
533
+ gitRef: functionSpec.gitRef,
534
+ });
535
+
536
+ await this.letIOClear();
537
+
538
+ let secondaryDelay = this.getSecondaryShardDelay(callSpec);
539
+ if (secondaryDelay) {
540
+ async function getIsFinished() {
541
+ return await proxyWatcher.commitFunction({
542
+ watchFunction() {
543
+ let syncedModule = skipPermissions(() =>
544
+ functionSchema()[callSpec.DomainName].PathFunctionRunner[callSpec.ModuleId]
545
+ );
546
+ return !!atomicObjectRead(syncedModule.Results[callSpec.CallId]);
547
+ },
548
+ });
549
+ }
550
+ // Call once, so we are already watching the
551
+ await delay(secondaryDelay);
552
+ let isFinished = await getIsFinished();
553
+ if (isFinished) {
554
+ console.info(`Function run finished before we could run it (another server must have ran it)`, {
555
+ callId: callSpec.CallId,
556
+ });
557
+ return;
558
+ }
559
+ if (PathFunctionRunner.DEBUG_CALLS) {
560
+ let fraction = getRoutingOverridePart(callSpec.CallId)?.route;
561
+ console.log(`${yellow("Function run fallback")} (primary servery failed to run function) after ${formatTime(secondaryDelay)}. Fraction ${fraction?.toFixed(3)}. Primary ${this.config.shardRange.startFraction.toFixed(3)} to ${this.config.shardRange.endFraction.toFixed(3)}. Secondary ${this.config.secondaryShardRange?.startFraction.toFixed(3)} to ${this.config.secondaryShardRange?.endFraction.toFixed(3)}. for ${getDebugName(callSpec, functionSpec, true)}`);
562
+ }
563
+ }
564
+
565
+ PathFunctionRunner.RUN_START_COUNT++;
566
+ if (PathFunctionRunner.DEBUG_CALLS) {
567
+ console.log(`STARTING ${getDebugName(callSpec, functionSpec, true)}`);
568
+ }
569
+ let startTime = Date.now();
570
+ let runCount = 0;
571
+ stats.lastInternalLoopCount = 0;
572
+
573
+ let finalWrites: PathValue[] | undefined;
574
+
575
+ let evalTime = 0;
576
+ let retries = 0;
577
+
578
+ let nooped = false;
579
+
580
+ while (true) {
581
+ let isFunctionSpecOutdated = false;
582
+ try {
583
+ if (retries > 10) {
584
+ console.error(`Could not sync stable function spec, aborting call. Retries: ${retries}`, {
585
+ callId: callSpec.CallId,
586
+ });
587
+ throw new Error(`Could not sync stable function spec, aborting call. Retries: ${retries}`);
588
+ }
589
+
590
+ let moduleExports = await this.getExportsFromSpec(functionSpec);
591
+
592
+ // ALWAYS get the permissions as well. This isn't perfect, as we could very well access data in other files, but... it helps
593
+ // reduce extra iterations inside of commitFunction...
594
+ await getModuleFromSpec({ ...functionSpec, exportPathStr: getExportPath(PERMISSIONS_FUNCTION_ID), FunctionId: PERMISSIONS_FUNCTION_ID, });
595
+
596
+ let exportPath = getPathFromStr(functionSpec.exportPathStr);
597
+ let exportObj = moduleExports as any;
598
+ for (let path of exportPath) {
599
+ exportObj = exportObj[path];
600
+ }
601
+ let baseFunction = exportObj as Function;
602
+
603
+ if (typeof baseFunction !== "function") {
604
+ throw new Error(`Export at ${JSON.stringify(exportPath.join("."))} was not a function (it was ${typeof baseFunction}). If in development, you must restart the development function runner server when you add new functions.`);
605
+ }
606
+
607
+ let devFunctionMetadata: FunctionMetadata | undefined;
608
+
609
+ let devModule = getDevelopmentModule(functionSpec.ModuleId);
610
+ if (devModule) {
611
+ let schemaObj = getSchemaObject(devModule);
612
+ devFunctionMetadata = schemaObj?.functionMetadata?.[functionSpec.FunctionId];
613
+ }
614
+
615
+ console.info(`Running function: ${getDebugName(callSpec, functionSpec, true)}`, {
616
+ callId: callSpec.CallId,
617
+ outerLoop: retries,
618
+ });
619
+
620
+ await proxyWatcher.commitFunction({
621
+ canWrite: true,
622
+ debugName: getDebugName(callSpec, functionSpec),
623
+ runAtTime: callSpec.runAtTime,
624
+ getPermissionsCheck: PermissionsChecker && (() => new PermissionsChecker(callSpec)),
625
+ nestedCalls: "inline",
626
+ temporary: true,
627
+ maxLocksOverride: devFunctionMetadata ? devFunctionMetadata.maxLocksOverride : functionSpec.maxLocksOverride,
628
+ source: callSpec.CallId,
629
+ watchFunction: function runCallWatcher() {
630
+ runCount++;
631
+ stats.lastInternalLoopCount = runCount;
632
+ stats.totalInternalLoopCount++;
633
+ if (PathFunctionRunner.DEBUG_CALLS) {
634
+ console.log(`Evaluating (try count ${runCount}) ${getDebugName(callSpec, functionSpec, true)}`);
635
+ }
636
+ if (runCount > PathFunctionRunner.MAX_WATCH_LOOPS) {
637
+ let errorMessage = `MAX_WATCH_LOOPS exceeded for ${getDebugName(callSpec, functionSpec, true)}. All accesses have to be consistent. So Querysub.time() instead of Date.now() and Querysub.nextId() instead of nextId() / Math.random(). If you need multiple random numbers, keep track of an index, and pass it to Querysub.nextId() for the nth random number.`;
638
+ console.error(errorMessage, {
639
+ callId: callSpec.CallId,
640
+ runCount,
641
+ limit: PathFunctionRunner.MAX_WATCH_LOOPS,
642
+ });
643
+ throw new Error(errorMessage);
644
+ }
645
+
646
+ // We NEED to depend on the function spec, so spec updates can be atomic (which is needed to deploy an update
647
+ // to many nodes at once, without having any period where multiple versions both commit values).
648
+ let syncedModule = skipPermissions(() =>
649
+ functionSchema()[callSpec.DomainName].PathFunctionRunner[callSpec.ModuleId]
650
+ );
651
+
652
+ // Make sure we are running the latest function (by checking it here, we also lock the values,
653
+ // so if it is updated we atomically rerun / reject all calls which were racing with us).
654
+ if (!!isPublic()) {
655
+ let syncedSpec = skipPermissions(() =>
656
+ atomicObjectRead(syncedModule.Sources[callSpec.FunctionId])
657
+ );
658
+ if (!syncedSpec) {
659
+ throw new Error(`Function spec not found for ${getDebugName(callSpec, functionSpec, true)}`);
660
+ }
661
+
662
+ // (We also need to depend on the RIGHT function spec).
663
+ if (
664
+ syncedSpec && (
665
+ syncedSpec.DomainName !== functionSpec.DomainName
666
+ || syncedSpec.ModuleId !== functionSpec.ModuleId
667
+ || syncedSpec.FilePath !== functionSpec.FilePath
668
+ || syncedSpec.FunctionId !== functionSpec.FunctionId
669
+ || syncedSpec.exportPathStr !== functionSpec.exportPathStr
670
+ || syncedSpec.FilePath !== functionSpec.FilePath
671
+ || syncedSpec.gitRef !== functionSpec.gitRef
672
+ || syncedSpec.gitURL !== functionSpec.gitURL
673
+ || syncedSpec.maxLocksOverride !== functionSpec.maxLocksOverride
674
+ )
675
+ ) {
676
+ isFunctionSpecOutdated = true;
677
+ functionSpec = {
678
+ DomainName: syncedSpec.DomainName,
679
+ ModuleId: syncedSpec.ModuleId,
680
+ FilePath: syncedSpec.FilePath,
681
+ FunctionId: syncedSpec.FunctionId,
682
+ exportPathStr: syncedSpec.exportPathStr,
683
+ gitRef: syncedSpec.gitRef,
684
+ gitURL: syncedSpec.gitURL,
685
+ };
686
+ return;
687
+ }
688
+ }
689
+
690
+ let evalTimeStart = Date.now();
691
+ try {
692
+ let args = parseArgs(callSpec);
693
+ overrideCurrentCall({ spec: callSpec, fnc: functionSpec, }, () => {
694
+ baseFunction(...args);
695
+ });
696
+ } finally {
697
+ let evalTimeDelta = Date.now() - evalTimeStart;
698
+ evalTime += evalTimeDelta;
699
+ stats.evalTime += evalTimeDelta;
700
+ }
701
+
702
+ // NOTE: The results are only temporarily stored, but serve as a way to let clients
703
+ // know figure out the dependencies for the call, which makes function call prediction
704
+ // possible.
705
+ skipPermissions(() => {
706
+ let result = doProxyOptions({ forceReadLatest: true }, () => atomicObjectRead(syncedModule.Results[callSpec.CallId]));
707
+ if (result) {
708
+ nooped = true;
709
+ // It is important to NOOP if it has already been written. Otherwise there could be multiple
710
+ // runs of the same function, which could easily vary (if they use Math.random(), etc).
711
+ // WHICH, as the write time is predetermined, would cause ValuePaths with equal times, but
712
+ // different values (which breaks a lot of assumptions much of the code depends on! such
713
+ // as locks, etc).
714
+ if (!secondaryDelay && Querysub.isAllSynced()) {
715
+ console.warn(`Skipping function write, as it has already been written to by another FunctionRunner.`);
716
+ }
717
+ return;
718
+ }
719
+ nooped = false;
720
+ let currentTimeTaken = Date.now() - startTime;
721
+ stats.timeTaken = currentTimeTaken;
722
+ let totalTime = Date.now() - stats.firstQueuedTime;
723
+ syncedModule.Results[callSpec.CallId] = atomicObjectWrite({
724
+ lastInternalLoopCount: runCount,
725
+ outerLoopCount: stats.outerLoopCount,
726
+ totalInternalLoopCount: stats.totalInternalLoopCount,
727
+ timeTaken: currentTimeTaken,
728
+ evalTime,
729
+ totalTime,
730
+ });
731
+ proxyWatcher.setEventPath(() => syncedModule.Results[callSpec.CallId]);
732
+ });
733
+
734
+ let watcher = proxyWatcher.getTriggeredWatcher();
735
+ if (watcher.hasAnyUnsyncedAccesses()) {
736
+ console.info(`Function run, unsynced values`, {
737
+ callId: callSpec.CallId,
738
+ count: watcher.pendingUnsyncedAccesses.size,
739
+ countParents: watcher.pendingUnsyncedParentAccesses.size,
740
+ promiseUnsynced: watcher.specialPromiseUnsynced,
741
+ blockedByOrder: isProxyBlockedByOrder(watcher),
742
+ path: Array.from(watcher.pendingUnsyncedAccesses)[0],
743
+ });
744
+ }
745
+ },
746
+ }, {
747
+ onWritesCommitted(writes) {
748
+ finalWrites = writes;
749
+ },
750
+ });
751
+ } catch (e: any) {
752
+
753
+ retries++;
754
+ if (isFunctionSpecOutdated) continue;
755
+
756
+ // NOTE: We are changing our paradigm to reject early, rather than retry. This is more stable. You can't really commit
757
+ // important values and then just leave, unless those values have no locks. This helps prevent values from being
758
+ // in different states on different machines, by just rejecting the call.
759
+ // if (e.message.includes("MAX_CHANGE_AGE_EXCEEDED") && retries < 10 && evalTime < MAX_ACCEPTED_CHANGE_AGE) {
760
+ // // NOTE: This should never really happen. It is only really if our PathValueServer goes down for a minute or so,
761
+ // // so we don't just drop all of our function calls.
762
+ // // - This is why we log every time, because no user actions should be able to result in this happening frequently.
763
+ // console.error(red(`RETRYING due to MAX_CHANGE_AGE_EXCEEDED. Hopefully the server will be more responsive this time (retry ${retries}): ${getDebugName(callPath, functionSpec, true)}`));
764
+ // // NOTE: Updating the time is fine, and won't break prediction rejections, as we reject based on path, not time.
765
+ // // (It might result in the prediction being wrong, but it is better than the user action being completely removed).
766
+ // callPath = { ...callPath, runAtTime: getNextTime() };
767
+ // retries++;
768
+ // continue;
769
+ // }
770
+ //if (PathFunctionRunner.DEBUG_CALLS)
771
+ {
772
+ console.log(`Function run error ${getDebugName(callSpec, functionSpec, true)}`, {
773
+ callId: callSpec.CallId,
774
+ error: e.stack,
775
+ });
776
+ }
777
+
778
+ finalWrites = await this.finishCallWithError({
779
+ callSpec,
780
+ functionSpec,
781
+ error: e.stack || "",
782
+ runCount,
783
+ startTime,
784
+ evalTime,
785
+ outerLoopCount: stats.outerLoopCount,
786
+ totalInternalLoopCount: stats.totalInternalLoopCount,
787
+ });
788
+ }
789
+
790
+ retries++;
791
+ if (isFunctionSpecOutdated) continue;
792
+ break;
793
+ }
794
+
795
+ if (PathFunctionRunner.DEBUG_CALLS) {
796
+ console.log(`FINISHED${nooped ? " (skipped)" : ""} ${getDebugName(callSpec, functionSpec, true)}, writes: ${finalWrites?.length}, took ${blue(formatTime(Date.now() - startTime))}`);
797
+ }
798
+
799
+ let wallTime = Date.now() - startTime;
800
+ let syncTime = wallTime - evalTime;
801
+
802
+ for (let write of finalWrites || []) {
803
+ console.info(`Function path write`, {
804
+ callId: callSpec.CallId,
805
+ path: write.path,
806
+ timeId: write.time.time,
807
+ isTransparent: write.isTransparent,
808
+ });
809
+ }
810
+
811
+ console.info(`Finished function evaluation: ${getDebugName(callSpec, functionSpec, true)}`, {
812
+ callId: callSpec.CallId,
813
+ outerLoop: retries,
814
+ finalWrites: finalWrites?.length,
815
+ argsEncoded: "", functionSpec,
816
+ wallTime, syncTime, evalTime,
817
+ loops: runCount,
818
+ });
819
+ PathFunctionRunner.RUN_FINISH_COUNT++;
820
+ }
821
+
822
+ // NOTE: This does and a
823
+ public async finishCallWithError(config: {
824
+ callSpec: CallSpec;
825
+ functionSpec: FunctionSpec | undefined;
826
+ error: string;
827
+ runCount: number;
828
+ startTime: number;
829
+ evalTime: number;
830
+ outerLoopCount: number;
831
+ totalInternalLoopCount: number;
832
+ }): Promise<PathValue[] | undefined> {
833
+
834
+ const { Querysub } = await import("../4-querysub/Querysub");
835
+ if (Querysub.isInSyncedCall()) {
836
+ Querysub.onCommitFinished(() => {
837
+ void this.finishCallWithError(config);
838
+ });
839
+ }
840
+
841
+ let { callSpec, functionSpec, error, runCount, startTime, evalTime, outerLoopCount, totalInternalLoopCount } = config;
842
+
843
+ let finalWrites: PathValue[] | undefined;
844
+
845
+ let callStats = this.callStats.get(callSpec.CallId);
846
+ let totalTime = callStats ? Date.now() - callStats.firstQueuedTime : Date.now() - startTime;
847
+
848
+ await proxyWatcher.commitFunction({
849
+ canWrite: true,
850
+ // NOTE: We don't set runAtTime, because the rejection might be due to the call being too old
851
+ debugName: `error (${getDebugName(callSpec, functionSpec)})`,
852
+ eventWrite: true,
853
+ doNotStoreWritesAsPredictions: true,
854
+ watchFunction() {
855
+ let syncedModule = functionSchema()[callSpec.DomainName].PathFunctionRunner[callSpec.ModuleId];
856
+ // 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.
857
+ let result = doProxyOptions({ forceReadLatest: true }, () => atomicObjectRead(syncedModule.Results[callSpec.CallId]));
858
+ if (result) return;
859
+ syncedModule.Results[callSpec.CallId] = atomicObjectWrite({
860
+ error,
861
+ lastInternalLoopCount: runCount,
862
+ outerLoopCount,
863
+ totalInternalLoopCount,
864
+ timeTaken: Date.now() - startTime,
865
+ evalTime,
866
+ totalTime,
867
+ });
868
+ },
869
+ }, {
870
+ onWritesCommitted(writes) {
871
+ finalWrites = writes;
872
+ },
873
+ });
874
+ return finalWrites;
875
+ }
876
+ }
877
+
878
+ export async function preloadFunctions(specs: FunctionSpec[], progress?: DeployProgress) {
879
+ progress?.({ section: "Finding FunctionPreloadControllers", progress: 0 });
880
+ let nodeIds = await getControllerNodeIdList(FunctionPreloadController);
881
+ progress?.({ section: "Finding FunctionPreloadControllers", progress: 1 });
882
+ await Promise.allSettled(nodeIds.map(async nodeObj => {
883
+ let nodeId = nodeObj.nodeId;
884
+ let controller = FunctionPreloadController.nodes[nodeId];
885
+ let section = `${nodeObj.entryPoint}:${nodeId}|Preloading Functions`;
886
+ progress?.({ section, progress: 0 });
887
+ console.log(blue(`Preloading functions on ${String(nodeId)}`));
888
+ await errorToUndefined(controller.preloadFunctions(specs));
889
+ progress?.({ section, progress: 1 });
890
+ console.log(blue(`Finished preloading functions on ${String(nodeId)}`));
891
+ }));
892
+ }
893
+
894
+ export function getDevFunctionSpecFromCall(call: {
895
+ DomainName: string;
896
+ ModuleId: string;
897
+ FunctionId: string;
898
+ }): FunctionSpec | undefined {
899
+ let domainName = call.DomainName;
900
+ let moduleId = call.ModuleId;
901
+ let functionId = call.FunctionId;
902
+
903
+ let devModule = getDevelopmentModule(moduleId);
904
+ if (!devModule) {
905
+ console.warn(yellow(`Cannot call ${domainName}.${moduleId}.Sources.${functionId} because the module is not deployed and not referenced in deploy.ts`));
906
+ return undefined;
907
+ }
908
+ let filePath = getModuleRelativePath(devModule);
909
+ let gitURL = getGitURLSync(undefined);
910
+ let gitRef = getGitRefSync(undefined);
911
+ return {
912
+ DomainName: domainName,
913
+ ModuleId: moduleId,
914
+ FunctionId: functionId,
915
+ exportPathStr: getExportPath(functionId),
916
+ FilePath: filePath,
917
+ gitURL,
918
+ gitRef,
919
+ };
920
+ }
921
+
922
+ class FunctionPreloaderBase {
923
+ async preloadFunctions(specs: FunctionSpec[]) {
924
+ if (!isPublic()) return;
925
+ for (let spec of specs) {
926
+ await getModuleFromSpec(spec);
927
+ }
928
+ }
929
+ }
930
+
931
+ const FunctionPreloadController = SocketFunction.register(
932
+ "FunctionPreloader-c966a1a6-5d3f-4453-bd03-ae84b574ec00",
933
+ new FunctionPreloaderBase(),
934
+ () => ({
935
+ preloadFunctions: {},
936
+ }),
937
+ () => ({
938
+ hooks: [requiresNetworkTrustHook],
939
+ }),
940
+ {
941
+ noAutoExpose: true,
942
+ }
943
+ );