querysub 0.508.0 → 0.510.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/function-public.js +2 -0
- package/bin/server.js +0 -2
- package/package.json +3 -4
- package/spec.txt +5 -0
- package/src/-f-node-discovery/NodeDiscovery.ts +1 -1
- package/src/-g-core-values/NodeCapabilities.ts +4 -0
- package/src/-g-core-values/scheduledShutdown.ts +48 -0
- package/src/0-path-value-core/AuthorityLookup.ts +50 -9
- package/src/0-path-value-core/PathRouter.ts +10 -31
- package/src/0-path-value-core/PathRouterRouteOverride.ts +14 -98
- package/src/0-path-value-core/PathRouterServerAuthoritySpec.tsx +1 -4
- package/src/0-path-value-core/PathValueCommitter.ts +1 -6
- package/src/0-path-value-core/PathValueController.ts +5 -7
- package/src/0-path-value-core/PathWatcher.ts +8 -11
- package/src/0-path-value-core/hackedPackedPathParentFiltering.ts +8 -22
- package/src/0-path-value-core/pathValueCore.ts +4 -5
- package/src/1-path-client/RemoteWatcher.ts +47 -34
- package/src/2-proxy/archiveMoveHarness.ts +2 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +11 -10
- package/src/3-path-functions/PathFunctionRunner.ts +165 -24
- package/src/3-path-functions/functionReplay.ts +2 -0
- package/src/3-path-functions/syncSchema.ts +0 -2
- package/src/4-deploy/edgeBootstrap.ts +10 -0
- package/src/4-deploy/edgeClientWatcher.tsx +4 -6
- package/src/4-deploy/edgeNodes.ts +52 -12
- package/src/4-querysub/FunctionRunnerTracking.ts +290 -0
- package/src/4-querysub/Querysub.ts +11 -16
- package/src/4-querysub/QuerysubController.ts +13 -11
- package/src/4-querysub/permissions.ts +3 -0
- package/src/4-querysub/querysubPrediction.ts +2 -2
- package/src/config.ts +1 -3
- package/src/deployManager/components/ServiceDetailPage.tsx +206 -77
- package/src/deployManager/components/ServicesListPage.tsx +3 -3
- package/src/deployManager/machineApplyMainCode.ts +139 -153
- package/src/deployManager/machineController.ts +12 -61
- package/src/deployManager/machineSchema.ts +84 -3
- package/src/deployManager/setupMachineMain.ts +2 -1
- package/src/diagnostics/grossStats/GrossStatsController.ts +3 -2
- package/src/diagnostics/logs/errorTickets/autoFixer.ts +3 -2
- package/src/diagnostics/logs/logGitHashes.ts +1 -1
- package/src/diagnostics/managementPages.tsx +2 -0
- package/src/diagnostics/misc-pages/AuthoritySpecPage.tsx +36 -28
- package/src/diagnostics/misc-pages/FunctionCaptureAnalyzePage.tsx +1 -1
- package/src/library-components/NetworkSelector.tsx +77 -0
- package/src/misc.ts +19 -0
- package/src/user-implementation/userData.ts +1 -1
- package/src/zipThreaded.ts +3 -2
- package/bin/server-dev.js +0 -11
|
@@ -4,7 +4,7 @@ import { measureWrap } from "socket-function/src/profiling/measure";
|
|
|
4
4
|
import { getOwnMachineId } from "sliftutils/misc/https/certs";
|
|
5
5
|
import { forceRemoveNode, getOurNodeId, getOurNodeIdAssert } from "../-f-node-discovery/NodeDiscovery";
|
|
6
6
|
import { Querysub } from "../4-querysub/Querysub";
|
|
7
|
-
import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, serviceConfigs, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME, getEffectiveServiceConfigs, recordLaunch } from "./machineSchema";
|
|
7
|
+
import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, ServiceSwitchover, serviceConfigs, pendingServiceConfigs, serviceSwitchovers, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME, getEffectiveServiceConfigs, recordLaunch, DEFAULT_OVERLAP_TIME } from "./machineSchema";
|
|
8
8
|
import { runPromise } from "../functional/runCommand";
|
|
9
9
|
import { getExternalIP } from "socket-function/src/networking";
|
|
10
10
|
import { errorToUndefined, errorToUndefinedSilent } from "../errors";
|
|
@@ -21,7 +21,7 @@ import { lazy } from "socket-function/src/caching";
|
|
|
21
21
|
import { getGitRefLive, getGitURLLive, setGitRef, rebuildGitFolder, nuclearReclone } from "../4-deploy/git";
|
|
22
22
|
import { blue, green, magenta, red } from "socket-function/src/formatting/logColors";
|
|
23
23
|
import { shutdown } from "../diagnostics/periodic";
|
|
24
|
-
import { onServiceConfigChange,
|
|
24
|
+
import { onServiceConfigChange, scheduleNodeShutdown } from "./machineController";
|
|
25
25
|
import { PromiseObj } from "../promise";
|
|
26
26
|
import path from "path";
|
|
27
27
|
import { fsExistsAsync } from "../fs";
|
|
@@ -132,6 +132,10 @@ const SCREEN_SUFFIX = "-dply";
|
|
|
132
132
|
export function getScreenName(config: { serviceKey: string; index: number }): string {
|
|
133
133
|
return `${config.serviceKey}-${config.index}${SCREEN_SUFFIX}`.replace(/[^a-zA-Z0-9\-_]/g, "_");
|
|
134
134
|
}
|
|
135
|
+
// The new instances started during a switchover overlap. They cover the service while the canonical screens are restarted with the new config at switch time, and are killed shortly after.
|
|
136
|
+
export function getNextScreenName(config: { serviceKey: string; index: number }): string {
|
|
137
|
+
return getScreenName({ serviceKey: config.serviceKey + "-next", index: config.index });
|
|
138
|
+
}
|
|
135
139
|
|
|
136
140
|
export async function streamScreenOutput(config: {
|
|
137
141
|
key: string;
|
|
@@ -286,42 +290,22 @@ done`);
|
|
|
286
290
|
})();
|
|
287
291
|
}
|
|
288
292
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
rollingScreenName: rolling.rollingScreenName,
|
|
298
|
-
originalScreenName: rolling.originalScreenName,
|
|
299
|
-
expiresAt: rolling.pinnedTime + rolling.pinnedDuration,
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return rollingInfo;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export async function killRollingServicesForService(config: {
|
|
308
|
-
serviceKey: string;
|
|
309
|
-
}): Promise<void> {
|
|
310
|
-
let toDelete: string[] = [];
|
|
311
|
-
|
|
312
|
-
for (let [screenName, rolling] of rollingKeepScreenAlive) {
|
|
313
|
-
if (screenName.startsWith(config.serviceKey + "-")) {
|
|
314
|
-
console.log(red(`Explicitly killing rolling screen ${rolling.rollingScreenName} for service ${config.serviceKey}`));
|
|
315
|
-
await killScreen({
|
|
316
|
-
screenName: rolling.rollingScreenName,
|
|
317
|
-
});
|
|
318
|
-
rollingScreens.delete(rolling.rollingScreenName);
|
|
319
|
-
toDelete.push(screenName);
|
|
320
|
-
}
|
|
293
|
+
// screenName => the shutdown time we last successfully told the service's node about
|
|
294
|
+
let notifiedShutdowns = new Map<string, number>();
|
|
295
|
+
async function notifyServiceShutdown(screenName: string, time: number) {
|
|
296
|
+
if (notifiedShutdowns.get(screenName) === time) return;
|
|
297
|
+
let nodeIdFile = os.homedir() + "/" + SERVICE_FOLDER + screenName + "/" + SERVICE_NODE_FILE_NAME;
|
|
298
|
+
if (!await fsExistsAsync(nodeIdFile)) {
|
|
299
|
+
console.log(`No nodeId file found for ${screenName}, cannot notify it of its scheduled shutdown yet (will retry on the next resync)`);
|
|
300
|
+
return;
|
|
321
301
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
302
|
+
let nodeId = await fs.promises.readFile(nodeIdFile, "utf8");
|
|
303
|
+
try {
|
|
304
|
+
console.log(green(`Notifying ${screenName} (${nodeId}) that it is scheduled to shut down at ${new Date(time).toLocaleString()}`));
|
|
305
|
+
await scheduleNodeShutdown({ nodeId, time });
|
|
306
|
+
notifiedShutdowns.set(screenName, time);
|
|
307
|
+
} catch (e: any) {
|
|
308
|
+
console.warn(`Error notifying ${screenName} (${nodeId}) of its scheduled shutdown (will retry on the next resync): ${e.stack}`);
|
|
325
309
|
}
|
|
326
310
|
}
|
|
327
311
|
|
|
@@ -451,71 +435,13 @@ async function removeOldNodeId(screenName: string) {
|
|
|
451
435
|
}
|
|
452
436
|
|
|
453
437
|
|
|
454
|
-
// originalName => info
|
|
455
|
-
let rollingKeepScreenAlive = new Map<string, {
|
|
456
|
-
rollingScreenName: string;
|
|
457
|
-
originalScreenName: string;
|
|
458
|
-
pinnedDuration: number;
|
|
459
|
-
pinnedTime: number;
|
|
460
|
-
}>();
|
|
461
|
-
// rollingScreenName => originalScreenName
|
|
462
|
-
let rollingScreens = new Map<string, string>();
|
|
463
|
-
|
|
464
438
|
const runScreenCommand = measureWrap(async function runScreenCommand(config: {
|
|
465
439
|
screenName: string;
|
|
466
440
|
command: string;
|
|
467
|
-
rollingWindow: number | undefined;
|
|
468
441
|
}) {
|
|
469
442
|
let prefix = getTmuxPrefix();
|
|
470
443
|
let screenName = config.screenName;
|
|
471
444
|
|
|
472
|
-
if (config.rollingWindow !== undefined) {
|
|
473
|
-
let screens = await getScreenState();
|
|
474
|
-
let rollingObj = rollingKeepScreenAlive.get(screenName);
|
|
475
|
-
if (rollingObj) {
|
|
476
|
-
rollingObj.pinnedDuration = config.rollingWindow;
|
|
477
|
-
}
|
|
478
|
-
let existingScreen = screens.find(x => x.screenName === screenName);
|
|
479
|
-
if (existingScreen && !rollingObj && await isScreenRunningProcess(existingScreen.pid)) {
|
|
480
|
-
let nodeIdPath = os.homedir() + "/" + SERVICE_FOLDER + screenName + "/" + SERVICE_NODE_FILE_NAME;
|
|
481
|
-
let rollingFinalTime = Date.now() + config.rollingWindow;
|
|
482
|
-
if (await fsExistsAsync(nodeIdPath)) {
|
|
483
|
-
let nodeId = await fs.promises.readFile(nodeIdPath, "utf8");
|
|
484
|
-
// REMOVE the nodeId file, so we the node isn't terminated!
|
|
485
|
-
await fs.promises.unlink(nodeIdPath);
|
|
486
|
-
try {
|
|
487
|
-
console.log(green(`Triggering rolling update notification to ${nodeId} for ${screenName} at ${new Date().toLocaleString()}`));
|
|
488
|
-
await triggerRollingUpdate({
|
|
489
|
-
nodeId,
|
|
490
|
-
time: rollingFinalTime,
|
|
491
|
-
});
|
|
492
|
-
} catch (e: any) {
|
|
493
|
-
console.warn(`Error triggering rolling update notification to ${nodeId} for ${screenName}: ${e.stack}`);
|
|
494
|
-
}
|
|
495
|
-
} else {
|
|
496
|
-
console.log(red(`No nodeId file found for ${screenName}, not triggering rolling update notification`));
|
|
497
|
-
}
|
|
498
|
-
// Stop the pipe-pane, and delete the pipe file
|
|
499
|
-
try {
|
|
500
|
-
await runPromise(`${prefix}tmux pipe-pane -O -t ${screenName}`);
|
|
501
|
-
await fs.promises.unlink(os.homedir() + "/" + SERVICE_FOLDER + screenName + "/pipe.txt");
|
|
502
|
-
} catch (e: any) {
|
|
503
|
-
console.warn(`Error stopping pipe-pane for ${screenName}: ${e.stack}`);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
let rollingScreenName = screenName + "-rolling" + SCREEN_SUFFIX;
|
|
507
|
-
console.log(green(`Renaming screen ${screenName} to ${rollingScreenName} for rolling interval ${config.rollingWindow} at ${new Date().toLocaleString()}`));
|
|
508
|
-
await runPromise(`${prefix}tmux rename-session -t ${screenName} ${rollingScreenName}`);
|
|
509
|
-
rollingScreens.set(rollingScreenName, screenName);
|
|
510
|
-
rollingKeepScreenAlive.set(screenName, {
|
|
511
|
-
rollingScreenName,
|
|
512
|
-
originalScreenName: screenName,
|
|
513
|
-
pinnedDuration: config.rollingWindow,
|
|
514
|
-
pinnedTime: Date.now(),
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
445
|
try {
|
|
520
446
|
// Throw if it already exists
|
|
521
447
|
await runPromise(`${prefix}tmux new -s ${screenName} -d`);
|
|
@@ -547,7 +473,6 @@ const runScreenCommand = measureWrap(async function runScreenCommand(config: {
|
|
|
547
473
|
await runScreenCommand({
|
|
548
474
|
screenName,
|
|
549
475
|
command: config.command,
|
|
550
|
-
rollingWindow: config.rollingWindow,
|
|
551
476
|
});
|
|
552
477
|
return;
|
|
553
478
|
}
|
|
@@ -660,44 +585,90 @@ const ensureGitSynced = measureWrap(async function ensureGitSynced(config: {
|
|
|
660
585
|
let launchesPerService = new Map<string, number>();
|
|
661
586
|
let lastLaunchedTimePerService = new Map<string, number>();
|
|
662
587
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
588
|
+
// Promotes any switchovers that have reached their switch time (the pending config becomes live), and returns the switchovers still in flight.
|
|
589
|
+
async function processSwitchovers(): Promise<Map<string, { switchover: ServiceSwitchover; pending: ServiceConfig }>> {
|
|
590
|
+
let active = new Map<string, { switchover: ServiceSwitchover; pending: ServiceConfig }>();
|
|
591
|
+
let switchovers = await serviceSwitchovers.values();
|
|
592
|
+
for (let switchover of switchovers) {
|
|
593
|
+
let pending = await pendingServiceConfigs.get(switchover.serviceId);
|
|
594
|
+
if (!pending) {
|
|
595
|
+
console.warn(`Switchover for ${switchover.serviceId} has no pending config, removing it`);
|
|
596
|
+
await serviceSwitchovers.delete(switchover.serviceId);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (Date.now() >= switchover.switchTime) {
|
|
600
|
+
// Multiple apply nodes may race to promote, which is fine, as they all write the same pending config.
|
|
601
|
+
console.log(green(`Promoting pending config for ${switchover.serviceId} (switchover scheduled for ${new Date(switchover.switchTime).toLocaleString()})`));
|
|
602
|
+
await serviceConfigs.set(switchover.serviceId, pending);
|
|
603
|
+
await pendingServiceConfigs.delete(switchover.serviceId);
|
|
604
|
+
await serviceSwitchovers.delete(switchover.serviceId);
|
|
605
|
+
continue;
|
|
670
606
|
}
|
|
607
|
+
let overlapStart = switchover.switchTime - switchover.overlapTime;
|
|
608
|
+
console.log(magenta(`Switchover in flight for ${switchover.serviceId} (${pending.parameters.key}): new instances start at ${new Date(overlapStart).toLocaleString()}${Date.now() >= overlapStart && " (overlap running)" || ""}, old instances shut down at ${new Date(switchover.switchTime).toLocaleString()}`));
|
|
609
|
+
active.set(switchover.serviceId, { switchover, pending });
|
|
671
610
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
611
|
+
return active;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Starts (or verifies) the "-next" instance of a service during a switchover overlap, running the pending config. It covers the service while the canonical screens restart with the new config at switch time, and is killed by the unused-screen cleanup once the switchover promotes.
|
|
615
|
+
async function ensureNextInstance(config: {
|
|
616
|
+
pending: ServiceConfig;
|
|
617
|
+
index: number;
|
|
618
|
+
screenNamesUsed: Set<string>;
|
|
619
|
+
screenStateMap: Map<string, { screenName: string; isProcessRunning: boolean; pid: string }>;
|
|
620
|
+
}) {
|
|
621
|
+
let { pending, index } = config;
|
|
622
|
+
let nextScreenName = getNextScreenName({ serviceKey: pending.parameters.key, index });
|
|
623
|
+
config.screenNamesUsed.add(nextScreenName);
|
|
624
|
+
|
|
675
625
|
let root = os.homedir() + "/" + SERVICE_FOLDER;
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
626
|
+
let folder = root + nextScreenName + "/";
|
|
627
|
+
let gitFolder = folder + "git/";
|
|
628
|
+
await fs.promises.mkdir(gitFolder, { recursive: true });
|
|
629
|
+
if (pending.parameters.gitRef) {
|
|
630
|
+
let prevGitRef = "";
|
|
631
|
+
try {
|
|
632
|
+
prevGitRef = await getGitRefLive(gitFolder);
|
|
633
|
+
} catch { }
|
|
634
|
+
await ensureGitSynced({
|
|
635
|
+
gitFolder,
|
|
636
|
+
repoUrl: pending.parameters.repoUrl,
|
|
637
|
+
gitRef: pending.parameters.gitRef,
|
|
638
|
+
});
|
|
639
|
+
let afterGitRef = await getGitRefLive(gitFolder);
|
|
640
|
+
let nodeModulesMissing = !await fsExistsAsync(gitFolder + "node_modules");
|
|
641
|
+
if (afterGitRef !== prevGitRef || nodeModulesMissing) {
|
|
642
|
+
console.log(green(`Yarn installing for ${magenta(nextScreenName)} (ref ${prevGitRef} -> ${afterGitRef}, nodeModulesMissing=${nodeModulesMissing})`));
|
|
643
|
+
await runPromise(`yarn install --mutex network`, { cwd: gitFolder });
|
|
693
644
|
}
|
|
694
645
|
}
|
|
695
|
-
|
|
646
|
+
|
|
647
|
+
let parameterPath = folder + "/parameters.json";
|
|
648
|
+
let prevParameters = "";
|
|
649
|
+
if (await fsExistsAsync(parameterPath)) {
|
|
650
|
+
prevParameters = await fs.promises.readFile(parameterPath, "utf8");
|
|
651
|
+
}
|
|
652
|
+
let newParametersString = JSON.stringify(pending.parameters);
|
|
653
|
+
let sameParameters = prevParameters === newParametersString;
|
|
654
|
+
let screenIsRunning = config.screenStateMap.get(nextScreenName)?.isProcessRunning;
|
|
655
|
+
if (sameParameters && screenIsRunning) {
|
|
656
|
+
console.log(green(`Verified switchover instance ${magenta(nextScreenName)} is running`));
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
console.log(`Starting switchover instance ${magenta(nextScreenName)}, with ${newParametersString}, isRunning = ${screenIsRunning}, sameParameters = ${sameParameters}`);
|
|
661
|
+
await fs.promises.writeFile(parameterPath, newParametersString);
|
|
662
|
+
await runScreenCommand({
|
|
663
|
+
screenName: nextScreenName,
|
|
664
|
+
command: pending.parameters.command,
|
|
665
|
+
});
|
|
696
666
|
}
|
|
697
667
|
|
|
698
668
|
const resyncServicesBase = runInSerial(measureWrap(async function resyncServices() {
|
|
699
669
|
console.log(magenta("Resyncing services"));
|
|
700
670
|
let machineId = getOwnMachineId(getDomain());
|
|
671
|
+
let activeSwitchovers = await processSwitchovers();
|
|
701
672
|
let configs = await getEffectiveServiceConfigs();
|
|
702
673
|
let relevantConfigs = configs.filter(config => config.machineIds.includes(machineId)).filter(x => x.parameters.deploy);
|
|
703
674
|
|
|
@@ -726,6 +697,23 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
726
697
|
try {
|
|
727
698
|
let folder = root + screenName + "/";
|
|
728
699
|
screenNamesUsed.add(screenName);
|
|
700
|
+
|
|
701
|
+
let switchoverObj = activeSwitchovers.get(config.serviceId);
|
|
702
|
+
if (switchoverObj) {
|
|
703
|
+
// Tell the running instance when it will be shut down, so it (and everything talking to it) can wind down gracefully.
|
|
704
|
+
await notifyServiceShutdown(screenName, switchoverObj.switchover.switchTime);
|
|
705
|
+
let overlapStart = switchoverObj.switchover.switchTime - switchoverObj.switchover.overlapTime;
|
|
706
|
+
let pendingMatchedCount = switchoverObj.pending.machineIds.filter(id => id === machineId).length;
|
|
707
|
+
if (Date.now() >= overlapStart && switchoverObj.pending.parameters.deploy && i < pendingMatchedCount) {
|
|
708
|
+
await ensureNextInstance({
|
|
709
|
+
pending: switchoverObj.pending,
|
|
710
|
+
index: i,
|
|
711
|
+
screenNamesUsed,
|
|
712
|
+
screenStateMap,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
729
717
|
let gitFolder = folder + "git/";
|
|
730
718
|
await fs.promises.mkdir(gitFolder, { recursive: true });
|
|
731
719
|
if (config.parameters.gitRef) {
|
|
@@ -790,7 +778,6 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
790
778
|
await runScreenCommand({
|
|
791
779
|
screenName,
|
|
792
780
|
command: config.parameters.command,
|
|
793
|
-
rollingWindow: config.parameters.rollingWindow,
|
|
794
781
|
});
|
|
795
782
|
await delay(2000);
|
|
796
783
|
let newScreens = await getScreenState(false);
|
|
@@ -824,39 +811,13 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
|
|
|
824
811
|
|
|
825
812
|
for (let { screenName } of screenState) {
|
|
826
813
|
if (screenNamesUsed.has(screenName)) continue;
|
|
827
|
-
let rollingScreenRemainder = rollingScreens.get(screenName);
|
|
828
|
-
if (rollingScreenRemainder) {
|
|
829
|
-
let rollingInfo = rollingKeepScreenAlive.get(rollingScreenRemainder);
|
|
830
|
-
if (!rollingInfo) {
|
|
831
|
-
rollingScreens.delete(screenName);
|
|
832
|
-
}
|
|
833
|
-
if (rollingInfo && rollingInfo.pinnedTime + rollingInfo.pinnedDuration < Date.now()) {
|
|
834
|
-
console.log(green(`Killing rolling screen ${screenName} because it's expired at ${new Date().toLocaleString()}`));
|
|
835
|
-
rollingScreens.delete(screenName);
|
|
836
|
-
rollingKeepScreenAlive.delete(rollingScreenRemainder);
|
|
837
|
-
rollingInfo = undefined;
|
|
838
|
-
}
|
|
839
|
-
if (rollingInfo) {
|
|
840
|
-
console.log(green(`Skipping killing rolling screen ${screenName} because it's still alive at ${new Date().toLocaleString()}. Keeping it alive until ${new Date(rollingInfo.pinnedTime + rollingInfo.pinnedDuration).toLocaleString()}. Was pinned at ${new Date(rollingInfo.pinnedTime).toLocaleString()}, rolling duration is ${formatTime(rollingInfo.pinnedDuration)}`));
|
|
841
|
-
continue;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
let rollingInfo = rollingKeepScreenAlive.get(screenName);
|
|
846
|
-
if (rollingInfo) {
|
|
847
|
-
console.log(red(`Service ${screenName} was disabled, killing associated rolling screen ${rollingInfo.rollingScreenName}`));
|
|
848
|
-
await killScreen({
|
|
849
|
-
screenName: rollingInfo.rollingScreenName,
|
|
850
|
-
});
|
|
851
|
-
rollingScreens.delete(rollingInfo.rollingScreenName);
|
|
852
|
-
rollingKeepScreenAlive.delete(screenName);
|
|
853
|
-
}
|
|
854
|
-
|
|
855
814
|
await killScreen({
|
|
856
815
|
screenName,
|
|
857
816
|
});
|
|
858
817
|
}
|
|
859
818
|
|
|
819
|
+
scheduleSwitchoverResync(activeSwitchovers);
|
|
820
|
+
|
|
860
821
|
await machineInfos.set(machineId, machineInfo);
|
|
861
822
|
console.log(`${magenta(`Resynced ${relevantConfigs.length} services`)}:\n${JSON.stringify(machineInfo, null, 2)}`);
|
|
862
823
|
|
|
@@ -871,6 +832,31 @@ async function resyncServices() {
|
|
|
871
832
|
}
|
|
872
833
|
}
|
|
873
834
|
|
|
835
|
+
// The regular resync poll is too coarse for switchover boundaries (the overlap start and the switch itself), so schedule an extra resync at the next boundary.
|
|
836
|
+
let switchoverResyncTimer: ReturnType<typeof setTimeout> | undefined;
|
|
837
|
+
function scheduleSwitchoverResync(activeSwitchovers: Map<string, { switchover: ServiceSwitchover; pending: ServiceConfig }>) {
|
|
838
|
+
let now = Date.now();
|
|
839
|
+
let next: number | undefined;
|
|
840
|
+
for (let { switchover } of activeSwitchovers.values()) {
|
|
841
|
+
for (let boundary of [switchover.switchTime - switchover.overlapTime, switchover.switchTime]) {
|
|
842
|
+
if (boundary <= now) continue;
|
|
843
|
+
if (next === undefined || boundary < next) {
|
|
844
|
+
next = boundary;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (switchoverResyncTimer) {
|
|
849
|
+
clearTimeout(switchoverResyncTimer);
|
|
850
|
+
switchoverResyncTimer = undefined;
|
|
851
|
+
}
|
|
852
|
+
if (next === undefined) return;
|
|
853
|
+
console.log(`Scheduling a resync for the next switchover boundary at ${new Date(next).toLocaleString()}`);
|
|
854
|
+
switchoverResyncTimer = setTimeout(() => {
|
|
855
|
+
switchoverResyncTimer = undefined;
|
|
856
|
+
void resyncServices();
|
|
857
|
+
}, next - now + 1000);
|
|
858
|
+
}
|
|
859
|
+
|
|
874
860
|
|
|
875
861
|
async function isPIDRunning(pid: string) {
|
|
876
862
|
try {
|
|
@@ -5,14 +5,14 @@ import { requiresNetworkTrustHook } from "../-d-trust/NetworkTrust2";
|
|
|
5
5
|
import { timeInMinute } from "socket-function/src/misc";
|
|
6
6
|
import { assertIsManagementUser } from "../diagnostics/managementPages";
|
|
7
7
|
import { isNode } from "typesafecss";
|
|
8
|
-
import { streamScreenOutput
|
|
8
|
+
import { streamScreenOutput } from "./machineApplyMainCode";
|
|
9
9
|
import { Querysub } from "../4-querysub/Querysub";
|
|
10
10
|
import { getPathStr2 } from "../path";
|
|
11
11
|
import { getGitURLLive, setGitRef } from "../4-deploy/git";
|
|
12
12
|
import os from "os";
|
|
13
13
|
import { runPromise } from "../functional/runCommand";
|
|
14
14
|
import { getSyncedController } from "../library-components/SyncedController";
|
|
15
|
-
import {
|
|
15
|
+
import { setScheduledShutdownTime } from "../-g-core-values/scheduledShutdown";
|
|
16
16
|
|
|
17
17
|
const POLL_INTERVAL = timeInMinute * 15;
|
|
18
18
|
|
|
@@ -38,33 +38,21 @@ export function onServiceConfigChange(callback: () => Promise<void>): () => void
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
export function
|
|
43
|
-
callback: (time: number) => void;
|
|
44
|
-
}) {
|
|
45
|
-
rollingUpdateWatchers.add(config.callback);
|
|
46
|
-
}
|
|
47
|
-
// TODO: PathValueServer should use this to remove itself from the node pool, and then after 50% of the time until time tell it's clients to find a new server to get values from
|
|
48
|
-
// TODO: FunctionRunner needs to reshard when it sees this (somehow?)
|
|
49
|
-
export async function triggerRollingUpdate(config: {
|
|
41
|
+
/** Tells a service's node that it will be shut down at the given time (due to a scheduled switchover). */
|
|
42
|
+
export async function scheduleNodeShutdown(config: {
|
|
50
43
|
nodeId: string;
|
|
51
44
|
time: number;
|
|
52
45
|
}) {
|
|
53
|
-
await OnServiceChange.nodes[config.nodeId].
|
|
46
|
+
await OnServiceChange.nodes[config.nodeId].scheduleShutdown(config.time);
|
|
54
47
|
}
|
|
55
48
|
class OnServiceChangeBase {
|
|
56
49
|
public async onServiceConfigChange() {
|
|
57
50
|
await triggerServiceConfigChangeCallbacks();
|
|
58
51
|
}
|
|
59
52
|
|
|
60
|
-
public async
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
callback(time);
|
|
64
|
-
} catch (e) {
|
|
65
|
-
logErrors(e);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
53
|
+
public async scheduleShutdown(time: number) {
|
|
54
|
+
console.log(`Received scheduled shutdown notification (service switchover): shutting down at ${new Date(time).toLocaleString()}`);
|
|
55
|
+
setScheduledShutdownTime(time);
|
|
68
56
|
}
|
|
69
57
|
}
|
|
70
58
|
export const OnServiceChange = SocketFunction.register(
|
|
@@ -72,7 +60,7 @@ export const OnServiceChange = SocketFunction.register(
|
|
|
72
60
|
() => new OnServiceChangeBase(),
|
|
73
61
|
() => ({
|
|
74
62
|
onServiceConfigChange: {},
|
|
75
|
-
|
|
63
|
+
scheduleShutdown: {},
|
|
76
64
|
}),
|
|
77
65
|
() => ({
|
|
78
66
|
hooks: [requiresNetworkTrustHook],
|
|
@@ -140,36 +128,6 @@ class MachineControllerBase {
|
|
|
140
128
|
await runPromise("bash machine-startup.sh", { cwd: os.homedir(), detach: true });
|
|
141
129
|
}
|
|
142
130
|
|
|
143
|
-
public async getRollingServiceInfo(config: {
|
|
144
|
-
serviceKey: string;
|
|
145
|
-
}): Promise<{ rollingScreenName: string; originalScreenName: string; expiresAt: number }[]> {
|
|
146
|
-
return await getRollingServiceInfo(config);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
public async getRollingServiceInfoFromBrowser(config: {
|
|
150
|
-
machineNodeId: string;
|
|
151
|
-
serviceKey: string;
|
|
152
|
-
}): Promise<{ rollingScreenName: string; originalScreenName: string; expiresAt: number }[]> {
|
|
153
|
-
return await MachineController(config.machineNodeId).getRollingServiceInfo.promise({
|
|
154
|
-
serviceKey: config.serviceKey,
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
public async killRollingServices(config: {
|
|
159
|
-
serviceKey: string;
|
|
160
|
-
}): Promise<void> {
|
|
161
|
-
await killRollingServicesForService(config);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
public async killRollingServicesFromBrowser(config: {
|
|
165
|
-
machineNodeId: string;
|
|
166
|
-
serviceKey: string;
|
|
167
|
-
}): Promise<void> {
|
|
168
|
-
await MachineController(config.machineNodeId).killRollingServices.promise({
|
|
169
|
-
serviceKey: config.serviceKey,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
131
|
}
|
|
174
132
|
|
|
175
133
|
let forwardedCallbacks = new Map<string, string>();
|
|
@@ -182,23 +140,16 @@ export const MachineController = getSyncedController(SocketFunction.register(
|
|
|
182
140
|
watchOtherScreenOutput: {},
|
|
183
141
|
deployMachineFromBrowser: {},
|
|
184
142
|
deployMachine: {},
|
|
185
|
-
getRollingServiceInfo: {},
|
|
186
|
-
getRollingServiceInfoFromBrowser: {},
|
|
187
|
-
killRollingServices: {},
|
|
188
|
-
killRollingServicesFromBrowser: {},
|
|
189
143
|
}),
|
|
190
144
|
() => ({
|
|
191
145
|
hooks: [assertIsManagementUser],
|
|
192
146
|
}),
|
|
193
147
|
), {
|
|
194
148
|
writes: {
|
|
195
|
-
deployMachineFromBrowser: ["MachineInfo"
|
|
196
|
-
deployMachine: ["MachineInfo"
|
|
197
|
-
killRollingServicesFromBrowser: ["RollingInfo"],
|
|
198
|
-
},
|
|
199
|
-
reads: {
|
|
200
|
-
getRollingServiceInfoFromBrowser: ["RollingInfo"],
|
|
149
|
+
deployMachineFromBrowser: ["MachineInfo"],
|
|
150
|
+
deployMachine: ["MachineInfo"],
|
|
201
151
|
},
|
|
152
|
+
reads: {},
|
|
202
153
|
});
|
|
203
154
|
|
|
204
155
|
let callbacks = new Map<string, (data: string) => Promise<void>>();
|
|
@@ -86,8 +86,8 @@ export type ServiceConfig = {
|
|
|
86
86
|
/** Not set by default, so we can setup the configuration before deploying it (or so we can undeploy easily without deleting it) */
|
|
87
87
|
deploy?: boolean;
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
89
|
+
/** How long the new and old instances run simultaneously during a switchover. The new instances start at switchTime - overlapTime, and the old instances are shut down at switchTime. This is the default; each switchover can override it. */
|
|
90
|
+
overlapTime?: number;
|
|
91
91
|
};
|
|
92
92
|
info: {
|
|
93
93
|
title: string;
|
|
@@ -95,12 +95,26 @@ export type ServiceConfig = {
|
|
|
95
95
|
lastUpdatedTime: number;
|
|
96
96
|
};
|
|
97
97
|
};
|
|
98
|
+
|
|
99
|
+
export const DEFAULT_OVERLAP_TIME = timeInMinute * 10;
|
|
100
|
+
|
|
101
|
+
export type ServiceSwitchover = {
|
|
102
|
+
serviceId: string;
|
|
103
|
+
/** When the old instances are shut down and the pending config becomes live */
|
|
104
|
+
switchTime: number;
|
|
105
|
+
/** The new instances start at switchTime - overlapTime */
|
|
106
|
+
overlapTime: number;
|
|
107
|
+
scheduledAt: number;
|
|
108
|
+
};
|
|
98
109
|
export type MachineConfig = {
|
|
99
110
|
machineId: string;
|
|
100
111
|
disabled: boolean;
|
|
101
112
|
};
|
|
102
113
|
export const machineInfos = archiveJSONT<MachineInfo>(() => nestArchives("machines/machine-heartbeats/", getArchivesBackblaze(getDomain())));
|
|
103
114
|
export const serviceConfigs = archiveJSONT<ServiceConfig>(() => nestArchives("machines/service-configs/", getArchivesBackblaze(getDomain())));
|
|
115
|
+
// Edits in the UI save here (still on disk), and only become live when a switchover is deployed / scheduled
|
|
116
|
+
export const pendingServiceConfigs = archiveJSONT<ServiceConfig>(() => nestArchives("machines/service-configs-pending/", getArchivesBackblaze(getDomain())));
|
|
117
|
+
export const serviceSwitchovers = archiveJSONT<ServiceSwitchover>(() => nestArchives("machines/service-switchovers/", getArchivesBackblaze(getDomain())));
|
|
104
118
|
export const machineConfigs = archiveJSONT<MachineConfig>(() => nestArchives("machines/machine-configs/", getArchivesBackblaze(getDomain())));
|
|
105
119
|
|
|
106
120
|
export type LaunchRecord = {
|
|
@@ -309,8 +323,63 @@ export class MachineServiceControllerBase {
|
|
|
309
323
|
|
|
310
324
|
// Remove the service config
|
|
311
325
|
await serviceConfigs.delete(serviceId);
|
|
326
|
+
await pendingServiceConfigs.delete(serviceId);
|
|
327
|
+
await serviceSwitchovers.delete(serviceId);
|
|
312
328
|
await this.notifyMachines([], serviceConfig.machineIds);
|
|
313
329
|
}
|
|
330
|
+
|
|
331
|
+
/** Saves edits WITHOUT deploying them. They only take effect once a switchover is scheduled (scheduleSwitchover). */
|
|
332
|
+
public async setPendingServiceConfigs(configs: ServiceConfig[]) {
|
|
333
|
+
for (let config of configs) {
|
|
334
|
+
let serviceId = config.serviceId;
|
|
335
|
+
config.info.lastUpdatedTime = Date.now();
|
|
336
|
+
let serviceConfig = await serviceConfigs.get(serviceId);
|
|
337
|
+
if (!serviceConfig) {
|
|
338
|
+
throw new Error(`Service ${serviceId} does not exist`);
|
|
339
|
+
}
|
|
340
|
+
await pendingServiceConfigs.set(serviceId, config);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
public async getPendingServiceConfig(serviceId: string): Promise<ServiceConfig | undefined> {
|
|
344
|
+
return await pendingServiceConfigs.get(serviceId);
|
|
345
|
+
}
|
|
346
|
+
public async discardPendingServiceConfig(serviceId: string) {
|
|
347
|
+
await pendingServiceConfigs.delete(serviceId);
|
|
348
|
+
await serviceSwitchovers.delete(serviceId);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Deploys the pending config: the new instances start at switchTime - overlapTime, the old instances are told they will shut down at switchTime, and at switchTime the pending config becomes live. */
|
|
352
|
+
public async scheduleSwitchover(config: {
|
|
353
|
+
serviceId: string;
|
|
354
|
+
switchTime: number;
|
|
355
|
+
overlapTime: number;
|
|
356
|
+
}) {
|
|
357
|
+
let pending = await pendingServiceConfigs.get(config.serviceId);
|
|
358
|
+
if (!pending) {
|
|
359
|
+
throw new Error(`No pending config for service ${config.serviceId}. Save changes first, then deploy them.`);
|
|
360
|
+
}
|
|
361
|
+
let live = await serviceConfigs.get(config.serviceId);
|
|
362
|
+
if (!live) {
|
|
363
|
+
throw new Error(`Service ${config.serviceId} does not exist`);
|
|
364
|
+
}
|
|
365
|
+
await serviceSwitchovers.set(config.serviceId, {
|
|
366
|
+
serviceId: config.serviceId,
|
|
367
|
+
switchTime: config.switchTime,
|
|
368
|
+
overlapTime: config.overlapTime,
|
|
369
|
+
scheduledAt: Date.now(),
|
|
370
|
+
});
|
|
371
|
+
await this.notifyMachines([...new Set([...live.machineIds, ...pending.machineIds])], []);
|
|
372
|
+
}
|
|
373
|
+
public async cancelSwitchover(serviceId: string) {
|
|
374
|
+
let live = await serviceConfigs.get(serviceId);
|
|
375
|
+
await serviceSwitchovers.delete(serviceId);
|
|
376
|
+
if (live) {
|
|
377
|
+
await this.notifyMachines(live.machineIds, []);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
public async getServiceSwitchover(serviceId: string): Promise<ServiceSwitchover | undefined> {
|
|
381
|
+
return await serviceSwitchovers.get(serviceId);
|
|
382
|
+
}
|
|
314
383
|
public async getServiceConfigType(): Promise<string> {
|
|
315
384
|
return extractType(module, "ServiceConfig");
|
|
316
385
|
}
|
|
@@ -538,6 +607,12 @@ export const MachineServiceController = getSyncedController(
|
|
|
538
607
|
setMachineConfig: {},
|
|
539
608
|
addServiceConfig: {},
|
|
540
609
|
setServiceConfigs: {},
|
|
610
|
+
setPendingServiceConfigs: {},
|
|
611
|
+
getPendingServiceConfig: {},
|
|
612
|
+
discardPendingServiceConfig: {},
|
|
613
|
+
scheduleSwitchover: {},
|
|
614
|
+
cancelSwitchover: {},
|
|
615
|
+
getServiceSwitchover: {},
|
|
541
616
|
getServiceConfigType: {},
|
|
542
617
|
deleteServiceConfig: {},
|
|
543
618
|
getGitInfo: {},
|
|
@@ -562,7 +637,11 @@ export const MachineServiceController = getSyncedController(
|
|
|
562
637
|
setMachineConfig: ["MachineConfig", "MachineConfigList"],
|
|
563
638
|
addServiceConfig: ["ServiceConfig", "ServiceConfigList"],
|
|
564
639
|
setServiceConfigs: ["ServiceConfig"],
|
|
565
|
-
|
|
640
|
+
setPendingServiceConfigs: ["PendingServiceConfig"],
|
|
641
|
+
discardPendingServiceConfig: ["PendingServiceConfig", "ServiceSwitchover"],
|
|
642
|
+
scheduleSwitchover: ["ServiceSwitchover"],
|
|
643
|
+
cancelSwitchover: ["ServiceSwitchover"],
|
|
644
|
+
deleteServiceConfig: ["ServiceConfig", "ServiceConfigList", "PendingServiceConfig", "ServiceSwitchover"],
|
|
566
645
|
commitPushService: ["gitInfo", "ServiceConfig"],
|
|
567
646
|
commitPushAndPublishQuerysub: ["gitInfo", "ServiceConfig"],
|
|
568
647
|
deployFunctions: ["gitInfo"],
|
|
@@ -574,6 +653,8 @@ export const MachineServiceController = getSyncedController(
|
|
|
574
653
|
getMachineConfig: ["MachineConfig"],
|
|
575
654
|
getServiceList: ["ServiceConfigList"],
|
|
576
655
|
getServiceConfig: ["ServiceConfig"],
|
|
656
|
+
getPendingServiceConfig: ["PendingServiceConfig"],
|
|
657
|
+
getServiceSwitchover: ["ServiceSwitchover"],
|
|
577
658
|
getGitInfo: ["gitInfo"],
|
|
578
659
|
getPendingFunctions: ["gitInfo"],
|
|
579
660
|
getLiveFunctions: ["gitInfo"],
|