querysub 0.611.0 → 0.613.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.613.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
76
76
|
"pako": "^2.1.0",
|
|
77
77
|
"peggy": "^5.0.6",
|
|
78
|
-
"sliftutils": "^1.7.
|
|
78
|
+
"sliftutils": "^1.7.86",
|
|
79
79
|
"socket-function": "^1.2.30",
|
|
80
80
|
"terser": "^5.31.0",
|
|
81
81
|
"typenode": "^6.6.1",
|
|
@@ -26,6 +26,8 @@ import { getGitRefLive, getGitURLLive } from "./git";
|
|
|
26
26
|
import { DeployProgress } from "./deployFunctions";
|
|
27
27
|
import { PromiseObj } from "../promise";
|
|
28
28
|
import type { FunctionRunnerIndex } from "../4-querysub/FunctionRunnerTracking";
|
|
29
|
+
import { isDefined } from "sliftutils/misc/types";
|
|
30
|
+
|
|
29
31
|
const UPDATE_POLL_INTERVAL = timeInMinute * 15;
|
|
30
32
|
const HEARTBEAT_INTERVAL = timeInMinute;
|
|
31
33
|
const EDGE_NODE_DELETE_THRESHOLD = timeInMinute * 15;
|
|
@@ -34,11 +36,8 @@ const EDGE_NODE_READ_PARALLEL = 64;
|
|
|
34
36
|
export const edgeNodeStorage = isNodeTrue() && getArchives2BufferedPublic("edgenodes2/");
|
|
35
37
|
export const edgeNodeIndexFile = "edge-nodes-index.json";
|
|
36
38
|
|
|
37
|
-
let registeredNodePaths = new Set<string>();
|
|
38
39
|
function getNextNodePath() {
|
|
39
|
-
|
|
40
|
-
registeredNodePaths.add(path);
|
|
41
|
-
return path;
|
|
40
|
+
return "node0_" + getOwnNodeId();
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
let edgeShutdown = false;
|
|
@@ -94,6 +93,7 @@ declare global {
|
|
|
94
93
|
var EDGE_NODE_STATS: EdgeNodeStats | undefined;
|
|
95
94
|
}
|
|
96
95
|
let registeredEdgeNode: { host: string; entryPaths: string[] } | boolean | undefined;
|
|
96
|
+
let lastEdgeNodeConfig: EdgeNodeConfig | undefined;
|
|
97
97
|
export async function registerEdgeNode(config: {
|
|
98
98
|
host: string;
|
|
99
99
|
entryPaths: string[];
|
|
@@ -114,9 +114,7 @@ export async function registerEdgeNode(config: {
|
|
|
114
114
|
registerShutdownHandler(async () => {
|
|
115
115
|
edgeShutdown = true;
|
|
116
116
|
console.log(magenta(`Removing node from edge node list due to shutdown`));
|
|
117
|
-
await
|
|
118
|
-
await edgeNodeStorage.del(nodePath);
|
|
119
|
-
}));
|
|
117
|
+
await edgeNodeStorage.del(getNextNodePath());
|
|
120
118
|
await updateEdgeNodesFile();
|
|
121
119
|
});
|
|
122
120
|
const { watchScheduledShutdown } = await import("../-g-core-values/scheduledShutdown");
|
|
@@ -128,9 +126,7 @@ export async function registerEdgeNode(config: {
|
|
|
128
126
|
// Wait a bit to fully remove ourself from the edge node list
|
|
129
127
|
await delay(Math.max(0, duration * 0.2));
|
|
130
128
|
console.log(magenta(`Removing node from edge node list due to scheduled shutdown at ${new Date(time).toLocaleString()}`));
|
|
131
|
-
await
|
|
132
|
-
await edgeNodeStorage.del(nodePath);
|
|
133
|
-
}));
|
|
129
|
+
await edgeNodeStorage.del(getNextNodePath());
|
|
134
130
|
await updateEdgeNodesFile();
|
|
135
131
|
})();
|
|
136
132
|
});
|
|
@@ -177,7 +173,7 @@ export async function registerEdgeNode(config: {
|
|
|
177
173
|
public: isPublic(),
|
|
178
174
|
functionRunnerIndex: await getInitialFunctionRunnerIndex(),
|
|
179
175
|
};
|
|
180
|
-
|
|
176
|
+
lastEdgeNodeConfig = edgeNodeConfig;
|
|
181
177
|
await edgeNodeStorage.set(getNextNodePath(), Buffer.from(JSON.stringify(edgeNodeConfig)));
|
|
182
178
|
}
|
|
183
179
|
registeredEdgeNode = {
|
|
@@ -229,6 +225,7 @@ const loadEntryPointsByHash = runInSerial(async function loadEntryPointsByHash(c
|
|
|
229
225
|
public: isPublic(),
|
|
230
226
|
functionRunnerIndex: await getInitialFunctionRunnerIndex(),
|
|
231
227
|
};
|
|
228
|
+
lastEdgeNodeConfig = edgeNodeConfig;
|
|
232
229
|
|
|
233
230
|
await edgeNodeStorage.set(getNextNodePath(), Buffer.from(JSON.stringify(edgeNodeConfig)));
|
|
234
231
|
console.log(magenta(`Registered edge node`), edgeNodeConfig);
|
|
@@ -245,14 +242,9 @@ async function getInitialFunctionRunnerIndex(): Promise<FunctionRunnerIndex | un
|
|
|
245
242
|
}
|
|
246
243
|
|
|
247
244
|
async function publishOwnScheduledShutdown(time: number) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
let config: EdgeNodeConfig = JSON.parse(buffer.toString());
|
|
252
|
-
if (config.scheduledShutdownTime === time) continue;
|
|
253
|
-
config.scheduledShutdownTime = time;
|
|
254
|
-
await edgeNodeStorage.set(nodePath, Buffer.from(JSON.stringify(config)));
|
|
255
|
-
}
|
|
245
|
+
if (!lastEdgeNodeConfig) return;
|
|
246
|
+
lastEdgeNodeConfig.scheduledShutdownTime = time;
|
|
247
|
+
await edgeNodeStorage.set(getNextNodePath(), Buffer.from(JSON.stringify(lastEdgeNodeConfig)));
|
|
256
248
|
await updateEdgeNodesFile();
|
|
257
249
|
}
|
|
258
250
|
|
|
@@ -271,12 +263,10 @@ const startUpdateLoop = lazy(async () => {
|
|
|
271
263
|
|
|
272
264
|
async function heartbeatLoop() {
|
|
273
265
|
if (edgeShutdown) return;
|
|
266
|
+
if (!lastEdgeNodeConfig) return;
|
|
274
267
|
let nodePath = getNextNodePath();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
let config: EdgeNodeConfig = JSON.parse(buffer.toString());
|
|
278
|
-
config.heartbeatTime = Date.now();
|
|
279
|
-
await edgeNodeStorage.set(nodePath, Buffer.from(JSON.stringify(config)));
|
|
268
|
+
lastEdgeNodeConfig.heartbeatTime = Date.now();
|
|
269
|
+
await edgeNodeStorage.set(nodePath, Buffer.from(JSON.stringify(lastEdgeNodeConfig)));
|
|
280
270
|
// Push the fresh heartbeat into the index so clients see it (they read only the index, not the per-node files).
|
|
281
271
|
await updateEdgeNodesFile();
|
|
282
272
|
}
|
|
@@ -287,7 +277,6 @@ async function updateLoop() {
|
|
|
287
277
|
}
|
|
288
278
|
|
|
289
279
|
async function updateEdgeNodesFile() {
|
|
290
|
-
// Snapshot the time BEFORE reading anything below. Staleness is judged against when we STARTED, so if the reads block we can never mass-evict live nodes just because wall-clock advanced while we were stuck.
|
|
291
280
|
let now = Date.now();
|
|
292
281
|
|
|
293
282
|
let prevEdgeNodeFile = await edgeNodeStorage.get(edgeNodeIndexFile);
|
|
@@ -318,7 +307,6 @@ async function updateEdgeNodesFile() {
|
|
|
318
307
|
let edgeNodeFiles = await edgeNodeStorage.find("node", { type: "files" });
|
|
319
308
|
// Reads are independent, so run them in parallel (bounded): with many files a serial pass would take files × read-latency, which could be minutes.
|
|
320
309
|
let readEdgeNodeFile = runInParallel({ parallelCount: EDGE_NODE_READ_PARALLEL }, async (nodeFile: string) => {
|
|
321
|
-
// Read directly, not through a cache: heartbeatTime mutates every minute, so a cached config would report a stale heartbeat and wrongly evict a live peer.
|
|
322
310
|
let buffer = await edgeNodeStorage.get(nodeFile);
|
|
323
311
|
if (!buffer) return undefined;
|
|
324
312
|
let config: EdgeNodeConfig = JSON.parse(buffer.toString());
|
|
@@ -330,19 +318,9 @@ async function updateEdgeNodesFile() {
|
|
|
330
318
|
}
|
|
331
319
|
return config;
|
|
332
320
|
});
|
|
333
|
-
let configs = await Promise.all(edgeNodeFiles.map(readEdgeNodeFile));
|
|
334
|
-
|
|
335
|
-
let bestByNodeId = new Map<string, EdgeNodeConfig>();
|
|
336
|
-
for (let config of configs) {
|
|
337
|
-
if (!config) continue;
|
|
338
|
-
// One entry per nodeId (re-registrations can leave more than one), keeping the freshest heartbeat.
|
|
339
|
-
let existing = bestByNodeId.get(config.nodeId);
|
|
340
|
-
if (!existing || (config.heartbeatTime || config.bootTime) > (existing.heartbeatTime || existing.bootTime)) {
|
|
341
|
-
bestByNodeId.set(config.nodeId, config);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
321
|
+
let configs = (await Promise.all(edgeNodeFiles.map(readEdgeNodeFile))).filter(isDefined);
|
|
344
322
|
|
|
345
|
-
let newEdgeNodes = sort(
|
|
323
|
+
let newEdgeNodes = sort(configs, x => -(x.heartbeatTime || x.bootTime));
|
|
346
324
|
let newEdgeNodeIndex: EdgeNodesIndex = {
|
|
347
325
|
edgeNodes: newEdgeNodes,
|
|
348
326
|
liveHash,
|
|
@@ -26,7 +26,7 @@ import { PromiseObj } from "../promise";
|
|
|
26
26
|
import path from "path";
|
|
27
27
|
import { fsExistsAsync } from "../fs";
|
|
28
28
|
import { ALIVE_WINDOW_FOREVER, ParametersTimelineEntry, syncParametersTimelineFiles } from "./parametersTimeline";
|
|
29
|
-
import { getScreenName, getRetiringKillTime, retireScreen, retireCurrentScreen, getTmuxPrefix, getScreenState, isScreenRunningProcess, getProcessStartTime, getScreenPanePid, ensureProcessRecord, runScreenCommand, killScreen, readServiceNodeId, removeServiceNode, killScreenNow, withScreenLock } from "./processManager";
|
|
29
|
+
import { getScreenName, getRetiringKillTime, getCanonicalScreenName, retireScreen, retireScreenWithMaxKillTime, retireCurrentScreen, getTmuxPrefix, getScreenState, isScreenRunningProcess, getProcessStartTime, getScreenPanePid, ensureProcessRecord, runScreenCommand, killScreen, readServiceNodeId, removeServiceNode, killScreenNow, withScreenLock } from "./processManager";
|
|
30
30
|
import { ProcessRecord, syncProcessRecords, listProcessRecords } from "./processLogs";
|
|
31
31
|
|
|
32
32
|
|
|
@@ -261,6 +261,17 @@ function getMachineInstances(configs: ServiceConfig[], machineId: string): { con
|
|
|
261
261
|
return instances;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// A version being replaced is renamed aside and given until this before it is taken down, which is the only thing an overlap is. Anchored to the release, not to whenever this resync happened to run, so every machine's old process goes at the same moment however staggered the resyncs are. Uses the NEWEST parameters' overlapTime, so shortening the overlap pulls the deadline in even for versions already retiring.
|
|
265
|
+
function getInstanceKillTime(parameters: ServiceParameters): number {
|
|
266
|
+
return (parameters.releaseTime || Date.now()) + (parameters.overlapTime ?? DEFAULT_OVERLAP_TIME);
|
|
267
|
+
}
|
|
268
|
+
/** The canonical screen name of one instance - the version that should be running there. Single source of truth for the name, so the retiring-screen sweep derives it the same way ensureScreen does. */
|
|
269
|
+
function getInstanceScreenName(config: ServiceConfig, index: number): string {
|
|
270
|
+
let live = getLiveServiceParameters(config);
|
|
271
|
+
let serviceKey = (live.deploy ? live : config.parameters).key;
|
|
272
|
+
return getScreenName({ serviceKey, index });
|
|
273
|
+
}
|
|
274
|
+
|
|
264
275
|
/** Puts one instance of a service into the state its configuration describes: the folder synced, the process running what it should be, and the records and timeline saying so. Returns the screen it is using, so anything nobody claimed can be killed. Everything about the screen happens under its lock, so two passes can never interleave. */
|
|
265
276
|
async function ensureScreen(config: {
|
|
266
277
|
config: ServiceConfig;
|
|
@@ -272,13 +283,12 @@ async function ensureScreen(config: {
|
|
|
272
283
|
let serviceId = config.config.serviceId;
|
|
273
284
|
let serviceConfig = config.config;
|
|
274
285
|
|
|
275
|
-
|
|
276
|
-
let killTime = (serviceConfig.parameters.releaseTime || Date.now()) + (serviceConfig.parameters.overlapTime ?? DEFAULT_OVERLAP_TIME);
|
|
286
|
+
let killTime = getInstanceKillTime(serviceConfig.parameters);
|
|
277
287
|
|
|
278
288
|
// The screen name means one thing: the version that should be running. There is no second name to reason about - anything it replaced is already renamed aside and carries its own deadline.
|
|
279
289
|
let parameters: ServiceParameters | undefined = getLiveServiceParameters(serviceConfig);
|
|
280
290
|
if (!parameters.deploy) parameters = undefined;
|
|
281
|
-
let screenName =
|
|
291
|
+
let screenName = getInstanceScreenName(serviceConfig, index);
|
|
282
292
|
let folder = os.homedir() + "/" + SERVICE_FOLDER + screenName + "/";
|
|
283
293
|
// Returned even when the work below throws, so a transient failure never makes the sweep kill a screen we still want
|
|
284
294
|
let usedScreenNames = [screenName];
|
|
@@ -500,6 +510,12 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
500
510
|
// The fast-path results are memoized per resync, so a folder shared by two instances is only traversed once
|
|
501
511
|
gitFastPathChecks.clear();
|
|
502
512
|
|
|
513
|
+
// The kill time each instance's overlap now wants, keyed by the canonical screen name its retiring predecessor was renamed from. Lets the sweep cap an already-retiring screen's deadline against the newest overlap instead of the (possibly longer) one baked into its name.
|
|
514
|
+
let killTimeByScreen = new Map<string, number>();
|
|
515
|
+
for (let { config, index } of instances) {
|
|
516
|
+
killTimeByScreen.set(getInstanceScreenName(config, index), getInstanceKillTime(config.parameters));
|
|
517
|
+
}
|
|
518
|
+
|
|
503
519
|
// Every instance converges concurrently. Each has its own folder, its own records and its own lock, and yarn install takes a network mutex, so there is nothing to serialize behind - one slow git sync or shutdown no longer delays everything after it.
|
|
504
520
|
let usedScreenNames = new Set((await Promise.all(instances.map(instance => ensureScreen({ ...instance, machineInfo })))).flat());
|
|
505
521
|
|
|
@@ -508,7 +524,14 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
508
524
|
if (usedScreenNames.has(screenName)) continue;
|
|
509
525
|
// A retiring screen is not unclaimed - it is a version that was replaced, and its name says when it is due to go. Cached per name, so seeing it on every resync only ever starts one, and a restart picks it up again from the name alone.
|
|
510
526
|
if (getRetiringKillTime(screenName) !== undefined) {
|
|
511
|
-
|
|
527
|
+
let canonicalScreenName = getCanonicalScreenName(screenName);
|
|
528
|
+
// Capped against the newest overlap when we still have that instance's config, so a shortened overlap pulls the deadline in (down to now, killing it) rather than letting it wait out the time baked into its name. Without a matching config we cannot know the new overlap, so we honor the name.
|
|
529
|
+
let maxKillTime = canonicalScreenName !== undefined ? killTimeByScreen.get(canonicalScreenName) : undefined;
|
|
530
|
+
if (maxKillTime !== undefined && canonicalScreenName !== undefined) {
|
|
531
|
+
void retireScreenWithMaxKillTime({ screenName, folder: root + canonicalScreenName + "/", maxKillTime });
|
|
532
|
+
} else {
|
|
533
|
+
void retireScreen(screenName);
|
|
534
|
+
}
|
|
512
535
|
continue;
|
|
513
536
|
}
|
|
514
537
|
// Detached, so a screen that takes its full grace period to go down doesn't hold up the end of the resync
|
|
@@ -356,6 +356,47 @@ export async function retireCurrentScreen(config: {
|
|
|
356
356
|
void retireScreen(retiringScreenName);
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
/** The canonical name a retiring screen was renamed from - the inverse of getRetiringScreenName. Undefined for a name that is not a retiring screen. */
|
|
360
|
+
export function getCanonicalScreenName(screenName: string): string | undefined {
|
|
361
|
+
let marker = screenName.lastIndexOf(RETIRING_MARKER);
|
|
362
|
+
if (marker === -1) return undefined;
|
|
363
|
+
return screenName.slice(0, marker) + SCREEN_SUFFIX;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Schedules a retiring screen's takedown, capped at maxKillTime: a shortened overlap can leave a screen scheduled to go later than it now should, and since the deadline lives in the name, the only way to move it is to rename the screen. When the cap is earlier than the time baked into the name, this renames it to carry the earlier time (down to now, which retireScreen then acts on immediately, killing it) and re-arms against the new name. When nothing needs pulling in, it just re-arms the existing timer, so it is safe to call every sweep. */
|
|
367
|
+
export async function retireScreenWithMaxKillTime(config: { screenName: string; folder: string; maxKillTime: number }): Promise<void> {
|
|
368
|
+
let { screenName, folder } = config;
|
|
369
|
+
let maxKillTime = Math.round(config.maxKillTime);
|
|
370
|
+
let currentKillTime = getRetiringKillTime(screenName);
|
|
371
|
+
if (currentKillTime === undefined) return;
|
|
372
|
+
let canonicalScreenName = getCanonicalScreenName(screenName);
|
|
373
|
+
if (canonicalScreenName !== undefined && maxKillTime < currentKillTime) {
|
|
374
|
+
let newScreenName = getRetiringScreenName(canonicalScreenName, maxKillTime);
|
|
375
|
+
if (newScreenName !== screenName) {
|
|
376
|
+
console.log(green(`Pulling in retiring ${screenName} to ${newScreenName} (kill time ${new Date(currentKillTime).toISOString()} -> ${new Date(maxKillTime).toISOString()})`));
|
|
377
|
+
let pid = await getScreenPanePid(screenName);
|
|
378
|
+
// The pending timer is waiting on the OLD, later deadline; drop it so a fresh one runs against the new name's earlier deadline.
|
|
379
|
+
retireScreen.clear(screenName);
|
|
380
|
+
try {
|
|
381
|
+
await runPromise(`${getTmuxPrefix()}tmux rename-session -t ${screenName} ${newScreenName}`);
|
|
382
|
+
} catch (e) {
|
|
383
|
+
// Another pass may have already renamed or killed it
|
|
384
|
+
console.log(`Could not rename retiring ${screenName} to ${newScreenName} (already renamed or gone?): ${(e as Error).stack ?? e}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (pid) {
|
|
388
|
+
let record = await findProcessRecordByPid(folder, pid);
|
|
389
|
+
if (record) {
|
|
390
|
+
await writeProcessRecord({ ...record, screenName: newScreenName });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
void retireScreen(newScreenName);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
void retireScreen(screenName);
|
|
398
|
+
}
|
|
399
|
+
|
|
359
400
|
/** Ends a session at once, with no notice - for a screen that should not exist at all, so there is nothing to wind down gracefully. Finding one is always a surprise, so the reason it was not supposed to be there is logged. */
|
|
360
401
|
export async function killScreenNow(screenName: string, reason: string): Promise<void> {
|
|
361
402
|
let panePid = await getScreenPanePid(screenName);
|