querysub 0.509.0 → 0.511.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.
@@ -48,9 +48,9 @@ export class ServicesListPage extends qreact.Component {
48
48
  const newServiceId = `service-${Math.round(Date.now())}`;
49
49
  const newConfig: ServiceConfig = {
50
50
  serviceId: newServiceId,
51
- machineIds: [],
52
51
  parameters: {
53
52
  key: newServiceId,
53
+ machineIds: [],
54
54
  repoUrl: gitInfo.repoUrl,
55
55
  gitRef: gitInfo.latestRef,
56
56
  command: "ping 1.1.1.1",
@@ -80,11 +80,12 @@ export class ServicesListPage extends qreact.Component {
80
80
  {services.map(([serviceId, config]) => {
81
81
  if (!config) return <div key={serviceId}>Config is broken? Missing value for service? Is the file corrupted?</div>;
82
82
 
83
- let disabledMachines = config.machineIds.filter(machineId => {
83
+ let machineIds = config.parameters.machineIds || [];
84
+ let disabledMachines = machineIds.filter(machineId => {
84
85
  let machineConfig = getMachineConfig(machineId);
85
86
  return machineConfig?.disabled;
86
87
  });
87
- let enabledMachineIds = config.machineIds.filter(machineId => {
88
+ let enabledMachineIds = machineIds.filter(machineId => {
88
89
  let machineConfig = getMachineConfig(machineId);
89
90
  return !machineConfig?.disabled;
90
91
  });
@@ -138,7 +139,7 @@ export class ServicesListPage extends qreact.Component {
138
139
  </div>
139
140
  <div className={css.vbox(4)}>
140
141
  <div>Updated {formatDateJSX(config.info.lastUpdatedTime)} AGO</div>
141
- {config.parameters.overlapTime && <div className={css.colorhsl(210, 50, 50).fontSize(14).fontWeight("bold")}>Switchover overlap: {formatTime(config.parameters.overlapTime)}</div> || undefined}
142
+ {config.parameters.overlapTime && <div className={css.colorhsl(210, 50, 50).fontSize(14).fontWeight("bold")}>Deploy overlap: {formatTime(config.parameters.overlapTime)}</div> || undefined}
142
143
  </div>
143
144
  </div>
144
145
  </Anchor>
@@ -125,11 +125,14 @@ class RenameMachine extends qreact.Component {
125
125
  );
126
126
  let toUpdate: ServiceConfig[] = [];
127
127
  for (let config of configs.filter(isDefined)) {
128
- if (!config.machineIds.includes(from)) continue;
129
- let newMachineIds = config.machineIds.map(m => m === from ? to : m);
128
+ let machineIds = config.parameters.machineIds || [];
129
+ if (!machineIds.includes(from)) continue;
130
130
  toUpdate.push({
131
131
  ...config,
132
- machineIds: newMachineIds,
132
+ parameters: {
133
+ ...config.parameters,
134
+ machineIds: machineIds.map(m => m === from ? to : m),
135
+ },
133
136
  });
134
137
  }
135
138
  if (toUpdate.length === 0) {
@@ -1,6 +1,6 @@
1
1
  import { SocketFunction } from "socket-function/SocketFunction";
2
2
  import { qreact } from "../../4-dom/qreact";
3
- import { MachineServiceController, MachineInfo, ServiceConfig } from "../machineSchema";
3
+ import { DEFAULT_OVERLAP_TIME, MachineServiceController, MachineInfo, ServiceConfig } from "../machineSchema";
4
4
  import { showFullscreenModal } from "../../5-diagnostics/FullscreenModal";
5
5
  import { css } from "../../4-dom/css";
6
6
  import { formatTime } from "socket-function/src/formatting/format";
@@ -97,6 +97,9 @@ export class UpdateButtons extends qreact.Component<{
97
97
  onClick={async () => {
98
98
  for (let service of outdatedServices) {
99
99
  service.parameters.gitRef = gitInfo.latestRef;
100
+ // Release with the configured overlap, so the new instances are running before the old ones shut down
101
+ let overlap = service.parameters.overlapTime || DEFAULT_OVERLAP_TIME;
102
+ service.parameters.releaseTime = Date.now() + overlap;
100
103
  }
101
104
  await controller.setServiceConfigs.promise(outdatedServices);
102
105
  }}
@@ -133,6 +136,9 @@ export class UpdateServiceButtons extends qreact.Component<{
133
136
  className={buttonStyle.hsl(120, 70, 90).alignSelf("stretch")}
134
137
  onClick={async () => {
135
138
  this.props.service.parameters.gitRef = gitInfo.latestRef;
139
+ // Release with the configured overlap, so the new instances are running before the old ones shut down
140
+ let overlap = this.props.service.parameters.overlapTime || DEFAULT_OVERLAP_TIME;
141
+ this.props.service.parameters.releaseTime = Date.now() + overlap;
136
142
  await controller.setServiceConfigs.promise([this.props.service]);
137
143
  }}
138
144
  >
@@ -4,14 +4,14 @@ 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, ServiceSwitchover, serviceConfigs, pendingServiceConfigs, serviceSwitchovers, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME, getEffectiveServiceConfigs, recordLaunch, DEFAULT_OVERLAP_TIME } from "./machineSchema";
7
+ import { MACHINE_RESYNC_INTERVAL, MachineServiceControllerBase, MachineInfo, ServiceConfig, ServiceParameters, SERVICE_FOLDER, machineInfos, SERVICE_NODE_FILE_NAME, getEffectiveServiceConfigs, getLiveServiceParameters, restartParametersKey, 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";
11
11
  import { getDomain } from "../config";
12
12
  import { formatTime } from "socket-function/src/formatting/format";
13
13
  import { sort, timeInMinute, timeInSecond } from "socket-function/src/misc";
14
- import { isDefined } from "../misc";
14
+ import { isDefined, setPreciseTimeout } from "../misc";
15
15
  import { logLoadTime } from "../logModuleLoadTimes";
16
16
  import { delay, retryFunctional, runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
17
17
  import os from "os";
@@ -132,7 +132,7 @@ 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.
135
+ // The new instances started during a release overlap. They cover the service while the canonical screens are restarted with the new config at release time, and are killed shortly after.
136
136
  export function getNextScreenName(config: { serviceKey: string; index: number }): string {
137
137
  return getScreenName({ serviceKey: config.serviceKey + "-next", index: config.index });
138
138
  }
@@ -585,56 +585,30 @@ const ensureGitSynced = measureWrap(async function ensureGitSynced(config: {
585
585
  let launchesPerService = new Map<string, number>();
586
586
  let lastLaunchedTimePerService = new Map<string, number>();
587
587
 
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;
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 });
610
- }
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.
588
+ // Starts (or verifies) the "-next" instance of a service during a release overlap, running the new (not yet live) parameters. It covers the service while the canonical screens restart with the new parameters at release time, and is killed by the unused-screen cleanup afterwards (once the release passes we stop ensuring it).
615
589
  async function ensureNextInstance(config: {
616
- pending: ServiceConfig;
590
+ next: ServiceParameters;
617
591
  index: number;
618
592
  screenNamesUsed: Set<string>;
619
593
  screenStateMap: Map<string, { screenName: string; isProcessRunning: boolean; pid: string }>;
620
594
  }) {
621
- let { pending, index } = config;
622
- let nextScreenName = getNextScreenName({ serviceKey: pending.parameters.key, index });
595
+ let { next, index } = config;
596
+ let nextScreenName = getNextScreenName({ serviceKey: next.key, index });
623
597
  config.screenNamesUsed.add(nextScreenName);
624
598
 
625
599
  let root = os.homedir() + "/" + SERVICE_FOLDER;
626
600
  let folder = root + nextScreenName + "/";
627
601
  let gitFolder = folder + "git/";
628
602
  await fs.promises.mkdir(gitFolder, { recursive: true });
629
- if (pending.parameters.gitRef) {
603
+ if (next.gitRef) {
630
604
  let prevGitRef = "";
631
605
  try {
632
606
  prevGitRef = await getGitRefLive(gitFolder);
633
607
  } catch { }
634
608
  await ensureGitSynced({
635
609
  gitFolder,
636
- repoUrl: pending.parameters.repoUrl,
637
- gitRef: pending.parameters.gitRef,
610
+ repoUrl: next.repoUrl,
611
+ gitRef: next.gitRef,
638
612
  });
639
613
  let afterGitRef = await getGitRefLive(gitFolder);
640
614
  let nodeModulesMissing = !await fsExistsAsync(gitFolder + "node_modules");
@@ -649,37 +623,76 @@ async function ensureNextInstance(config: {
649
623
  if (await fsExistsAsync(parameterPath)) {
650
624
  prevParameters = await fs.promises.readFile(parameterPath, "utf8");
651
625
  }
652
- let newParametersString = JSON.stringify(pending.parameters);
653
- let sameParameters = prevParameters === newParametersString;
626
+ let newParametersString = JSON.stringify(next);
627
+ let sameParameters = sameRestartParameters(prevParameters, next);
654
628
  let screenIsRunning = config.screenStateMap.get(nextScreenName)?.isProcessRunning;
655
629
  if (sameParameters && screenIsRunning) {
656
- console.log(green(`Verified switchover instance ${magenta(nextScreenName)} is running`));
630
+ if (prevParameters !== newParametersString) {
631
+ await fs.promises.writeFile(parameterPath, newParametersString);
632
+ }
633
+ console.log(green(`Verified release-overlap instance ${magenta(nextScreenName)} is running`));
657
634
  return;
658
635
  }
659
636
 
660
- console.log(`Starting switchover instance ${magenta(nextScreenName)}, with ${newParametersString}, isRunning = ${screenIsRunning}, sameParameters = ${sameParameters}`);
637
+ console.log(`Starting release-overlap instance ${magenta(nextScreenName)}, with ${newParametersString}, isRunning = ${screenIsRunning}, sameParameters = ${sameParameters}`);
661
638
  await fs.promises.writeFile(parameterPath, newParametersString);
662
639
  await runScreenCommand({
663
640
  screenName: nextScreenName,
664
- command: pending.parameters.command,
641
+ command: next.command,
665
642
  });
666
643
  }
667
644
 
645
+ function sameRestartParameters(prevParametersJSON: string, next: ServiceParameters): boolean {
646
+ if (!prevParametersJSON) return false;
647
+ try {
648
+ return restartParametersKey(JSON.parse(prevParametersJSON)) === restartParametersKey(next);
649
+ } catch {
650
+ return false;
651
+ }
652
+ }
653
+
668
654
  const resyncServicesBase = runInSerial(measureWrap(async function resyncServices() {
669
655
  console.log(magenta("Resyncing services"));
670
656
  let machineId = getOwnMachineId(getDomain());
671
- let activeSwitchovers = await processSwitchovers();
672
- let configs = await getEffectiveServiceConfigs();
673
- let relevantConfigs = configs.filter(config => config.machineIds.includes(machineId)).filter(x => x.parameters.deploy);
657
+ let allConfigs = await getEffectiveServiceConfigs();
658
+ let relevantConfigs = allConfigs
659
+ .filter(config => (getLiveServiceParameters(config).machineIds || []).includes(machineId) || (config.parameters.machineIds || []).includes(machineId))
660
+ .filter(x => getLiveServiceParameters(x).deploy || x.parameters.deploy);
674
661
 
675
662
  let machineInfo = await getLiveMachineInfo();
676
663
 
677
664
  let screenState = await getScreenState();
678
665
  let screenStateMap = new Map(screenState.map(s => [s.screenName, s]));
679
666
  let screenNamesUsed = new Set<string>();
667
+ let upcomingReleases: { releaseTime: number; overlapTime: number }[] = [];
680
668
  let root = os.homedir() + "/" + SERVICE_FOLDER;
681
- for (let config of relevantConfigs) {
682
- let matchedCount = config.machineIds.filter(id => id === machineId).length;
669
+ for (let record of relevantConfigs) {
670
+ let config = { ...record, parameters: getLiveServiceParameters(record) };
671
+ let releaseTime = record.parameters.releaseTime || 0;
672
+ let isUpcomingRelease = !!releaseTime && Date.now() < releaseTime && !!record.oldParameters;
673
+ let overlapTime = record.parameters.overlapTime || DEFAULT_OVERLAP_TIME;
674
+ if (isUpcomingRelease) {
675
+ upcomingReleases.push({ releaseTime, overlapTime });
676
+ console.log(magenta(`Release in flight for ${record.serviceId} (${record.parameters.key}): new instances start at ${new Date(releaseTime - overlapTime).toLocaleString()}${Date.now() >= releaseTime - overlapTime && " (overlap running)" || ""}, old instances shut down at ${new Date(releaseTime).toLocaleString()}`));
677
+ // The release-overlap instances follow the NEW parameters' machineIds, so machines that are only in the new parameters still start their instances at overlap time (their canonical instances start once the release passes)
678
+ if (Date.now() >= releaseTime - overlapTime && record.parameters.deploy) {
679
+ let nextMatchedCount = (record.parameters.machineIds || []).filter(id => id === machineId).length;
680
+ for (let i = 0; i < nextMatchedCount; i++) {
681
+ try {
682
+ await ensureNextInstance({
683
+ next: record.parameters,
684
+ index: i,
685
+ screenNamesUsed,
686
+ screenStateMap,
687
+ });
688
+ } catch (e: any) {
689
+ console.error(`Error starting release-overlap instance for ${record.serviceId} (${record.parameters.key}): ${e.stack}`);
690
+ }
691
+ }
692
+ }
693
+ }
694
+ if (!config.parameters.deploy) continue;
695
+ let matchedCount = (config.parameters.machineIds || []).filter(id => id === machineId).length;
683
696
  for (let i = 0; i < matchedCount; i++) {
684
697
  let screenName = getScreenName({
685
698
  serviceKey: config.parameters.key,
@@ -698,20 +711,9 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
698
711
  let folder = root + screenName + "/";
699
712
  screenNamesUsed.add(screenName);
700
713
 
701
- let switchoverObj = activeSwitchovers.get(config.serviceId);
702
- if (switchoverObj) {
714
+ if (isUpcomingRelease) {
703
715
  // 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
- }
716
+ await notifyServiceShutdown(screenName, releaseTime);
715
717
  }
716
718
 
717
719
  let gitFolder = folder + "git/";
@@ -741,8 +743,7 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
741
743
  }
742
744
  let newParametersString = JSON.stringify(config.parameters);
743
745
 
744
-
745
- let sameParameters = prevParameters === newParametersString;
746
+ let sameParameters = sameRestartParameters(prevParameters, config.parameters);
746
747
  let screenIsRunning = screenStateMap.get(screenName)?.isProcessRunning;
747
748
 
748
749
  let nodePathId = folder + SERVICE_NODE_FILE_NAME;
@@ -752,6 +753,9 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
752
753
  }
753
754
 
754
755
  if (sameParameters && screenIsRunning) {
756
+ if (prevParameters !== newParametersString) {
757
+ await fs.promises.writeFile(parameterPath, newParametersString);
758
+ }
755
759
  console.log(green(`Verified ${magenta(screenName)} is running`));
756
760
  continue;
757
761
  }
@@ -816,7 +820,7 @@ const resyncServicesBase = runInSerial(measureWrap(async function resyncServices
816
820
  });
817
821
  }
818
822
 
819
- scheduleSwitchoverResync(activeSwitchovers);
823
+ scheduleReleaseResync(upcomingReleases);
820
824
 
821
825
  await machineInfos.set(machineId, machineInfo);
822
826
  console.log(`${magenta(`Resynced ${relevantConfigs.length} services`)}:\n${JSON.stringify(machineInfo, null, 2)}`);
@@ -832,29 +836,32 @@ async function resyncServices() {
832
836
  }
833
837
  }
834
838
 
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 }>) {
839
+ // The regular resync poll is too coarse for release boundaries (the overlap start and the release itself), so schedule an extra resync at the next boundary.
840
+ let cancelReleaseResync: (() => void) | undefined;
841
+ function scheduleReleaseResync(upcomingReleases: { releaseTime: number; overlapTime: number }[]) {
838
842
  let now = Date.now();
839
843
  let next: number | undefined;
840
- for (let { switchover } of activeSwitchovers.values()) {
841
- for (let boundary of [switchover.switchTime - switchover.overlapTime, switchover.switchTime]) {
844
+ for (let release of upcomingReleases) {
845
+ for (let boundary of [release.releaseTime - release.overlapTime, release.releaseTime]) {
842
846
  if (boundary <= now) continue;
843
847
  if (next === undefined || boundary < next) {
844
848
  next = boundary;
845
849
  }
846
850
  }
847
851
  }
848
- if (switchoverResyncTimer) {
849
- clearTimeout(switchoverResyncTimer);
850
- switchoverResyncTimer = undefined;
852
+ if (cancelReleaseResync) {
853
+ cancelReleaseResync();
854
+ cancelReleaseResync = undefined;
851
855
  }
852
856
  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);
857
+ console.log(`Scheduling a resync for the next release boundary at ${new Date(next).toLocaleString()}`);
858
+ cancelReleaseResync = setPreciseTimeout({
859
+ time: next,
860
+ callback: () => {
861
+ cancelReleaseResync = undefined;
862
+ void resyncServices();
863
+ },
864
+ });
858
865
  }
859
866
 
860
867
 
@@ -38,7 +38,7 @@ export function onServiceConfigChange(callback: () => Promise<void>): () => void
38
38
  }
39
39
 
40
40
 
41
- /** Tells a service's node that it will be shut down at the given time (due to a scheduled switchover). */
41
+ /** Tells a service's node that it will be shut down at the given time (due to a scheduled deploy). */
42
42
  export async function scheduleNodeShutdown(config: {
43
43
  nodeId: string;
44
44
  time: number;
@@ -51,7 +51,7 @@ class OnServiceChangeBase {
51
51
  }
52
52
 
53
53
  public async scheduleShutdown(time: number) {
54
- console.log(`Received scheduled shutdown notification (service switchover): shutting down at ${new Date(time).toLocaleString()}`);
54
+ console.log(`Received scheduled shutdown notification (service deploy): shutting down at ${new Date(time).toLocaleString()}`);
55
55
  setScheduledShutdownTime(time);
56
56
  }
57
57
  }