querysub 0.528.0 → 0.530.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.
@@ -89,7 +89,7 @@ export type ServiceParameters = {
89
89
  /** Not set by default, so we can setup the configuration before deploying it (or so we can undeploy easily without deleting it) */
90
90
  deploy?: boolean;
91
91
 
92
- /** How long the new and old instances run simultaneously during a release. The new instances start at releaseTime - overlapTime, and the old instances are shut down at releaseTime. */
92
+ /** How long the old instances keep running after a release. The new instances start at releaseTime, and the old instances are shut down at releaseTime + overlapTime (using the NEWEST parameters' overlapTime). */
93
93
  overlapTime?: number;
94
94
 
95
95
  /** When these parameters go live. Until then machines keep running oldParameters. Absent (or in the past) means live now. */
@@ -378,19 +378,8 @@ export class MachineServiceControllerBase {
378
378
  }
379
379
  normalizeServiceConfig(serviceConfig);
380
380
 
381
- // The old parameters are whatever is live right now: the previous parameters if their release time passed, else the previous old parameters (an unreleased update was updated again, and machines just keep running what they were running).
382
- if (config.parameters.releaseTime && Date.now() < config.parameters.releaseTime) {
383
- let liveParameters = getLiveServiceParameters(serviceConfig);
384
- if (JSON.stringify({ ...liveParameters, releaseTime: undefined }) === JSON.stringify({ ...config.parameters, releaseTime: undefined })) {
385
- // The parameters aren't changing, so there is nothing to release (machineIds / info changes apply immediately, only parameters wait for the release time)
386
- config.parameters.releaseTime = undefined;
387
- config.oldParameters = undefined;
388
- } else {
389
- config.oldParameters = liveParameters;
390
- }
391
- } else {
392
- config.oldParameters = undefined;
393
- }
381
+ // NOTE: This is correct. This is obviously correct. For some reason, the AI wrote some really big, complicated thing here. I don't know why. This is the only way it should be done, obviously.
382
+ config.oldParameters = getLiveServiceParameters(serviceConfig);
394
383
 
395
384
  await serviceConfigs.set(serviceId, config);
396
385
  // Only notify we were or are deployed. If it's not deployed, this will be ignored anyways.
@@ -0,0 +1,62 @@
1
+ import fs from "fs";
2
+ import type { ServiceParameters } from "./machineSchema";
3
+
4
+ /*
5
+ Each service instance folder holds one <n>-parameters.json per parameters version that is running or scheduled to run there, written by the machine apply code and cleaned up once a version is neither. A process can find which version it is by matching pid against its own ancestor pids (the recorded pid is the screen's shell, an ancestor of the service process), and read the other files to see what deploys next and when it will be shut down. See parametersTimelineMain.ts for a template reader.
6
+ */
7
+
8
+ export const PARAMETERS_TIMELINE_FILE_REGEX = /^(\d+)-parameters\.json$/;
9
+ export const ALIVE_WINDOW_FOREVER = Number.MAX_SAFE_INTEGER;
10
+
11
+ export type ParametersTimelineEntry = {
12
+ /** Absent until the instance has actually been started (future versions have no pid yet). */
13
+ pid?: number;
14
+ /** [start, end] of when these parameters run. end is ALIVE_WINDOW_FOREVER when no shutdown is scheduled. */
15
+ aliveWindow: [number, number];
16
+ parameters: ServiceParameters;
17
+ };
18
+
19
+ /** Makes the folder's <n>-parameters.json files exactly match `entries`: versions keep their file (and number) across updates, matched by their parameters JSON, and files for versions no longer present are deleted. */
20
+ export async function syncParametersTimelineFiles(folder: string, entries: ParametersTimelineEntry[]): Promise<void> {
21
+ let files: string[] = [];
22
+ try {
23
+ files = await fs.promises.readdir(folder);
24
+ } catch {
25
+ return;
26
+ }
27
+ let existing = new Map<string, { n: number; entry: ParametersTimelineEntry }>();
28
+ let timelineFiles: { file: string; n: number }[] = [];
29
+ let maxN = -1;
30
+ for (let file of files) {
31
+ let match = PARAMETERS_TIMELINE_FILE_REGEX.exec(file);
32
+ if (!match) continue;
33
+ let n = parseInt(match[1]);
34
+ timelineFiles.push({ file, n });
35
+ maxN = Math.max(maxN, n);
36
+ try {
37
+ let entry = JSON.parse(await fs.promises.readFile(folder + file, "utf8")) as ParametersTimelineEntry;
38
+ existing.set(JSON.stringify(entry.parameters), { n, entry });
39
+ } catch {
40
+ // Unreadable file stays unmatched, so it is cleaned up below
41
+ }
42
+ }
43
+ let usedNs = new Set<number>();
44
+ for (let entry of entries) {
45
+ let prev = existing.get(JSON.stringify(entry.parameters));
46
+ let n = prev ? prev.n : ++maxN;
47
+ usedNs.add(n);
48
+ // Keep a previously discovered pid, as the caller's screen state may lag behind a recent start
49
+ let newEntry: ParametersTimelineEntry = { pid: entry.pid ?? prev?.entry.pid, aliveWindow: entry.aliveWindow, parameters: entry.parameters };
50
+ if (!prev || JSON.stringify(prev.entry) !== JSON.stringify(newEntry)) {
51
+ await fs.promises.writeFile(folder + n + "-parameters.json", JSON.stringify(newEntry));
52
+ }
53
+ }
54
+ for (let { file, n } of timelineFiles) {
55
+ if (usedNs.has(n)) continue;
56
+ try {
57
+ await fs.promises.unlink(folder + file);
58
+ } catch {
59
+ // Concurrent removal is fine
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,123 @@
1
+ /*
2
+ Debug utility that shows the deployment timeline for the service instance it runs inside. It is also the TEMPLATE for how services themselves can read the <n>-parameters.json files (written by the machine apply code into each instance folder) to learn which parameters version they are, what deploys next, and when they will be shut down.
3
+
4
+ The discovery steps a service should copy:
5
+ 1. Walk up from cwd until a folder contains <n>-parameters.json files (services run in <instance folder>/git/, so the instance folder is an ancestor).
6
+ 2. Walk up your own pid chain (ps -o ppid=) — the entry whose pid is one of your ancestors is YOU (the recorded pid is the screen's shell, which spawned your process).
7
+ 3. Your entry's aliveWindow[1] is your scheduled shutdown time (ALIVE_WINDOW_FOREVER means none); an entry with a future aliveWindow[0] and no pid is the next deploy.
8
+
9
+ Run from inside a service folder (or as a service command): yarn parameters-timeline
10
+ */
11
+ import fs from "fs";
12
+ import path from "path";
13
+ import { runPromise } from "../functional/runCommand";
14
+ import { delay } from "socket-function/src/batching";
15
+ import { formatDateTimeDetailed, formatTime } from "socket-function/src/formatting/format";
16
+ import { ALIVE_WINDOW_FOREVER, PARAMETERS_TIMELINE_FILE_REGEX, ParametersTimelineEntry } from "./parametersTimeline";
17
+
18
+ const POLL_INTERVAL = 30_000;
19
+
20
+ async function getAncestorPids(): Promise<Set<number>> {
21
+ let pids = new Set<number>([process.pid]);
22
+ let current = process.pid;
23
+ while (true) {
24
+ let ppid: number;
25
+ if (current === process.pid && process.ppid) {
26
+ ppid = process.ppid;
27
+ } else {
28
+ try {
29
+ ppid = parseInt((await runPromise(`ps -o ppid= -p ${current}`)).trim());
30
+ } catch {
31
+ break;
32
+ }
33
+ }
34
+ if (!ppid || Number.isNaN(ppid) || ppid <= 1 || pids.has(ppid)) break;
35
+ pids.add(ppid);
36
+ current = ppid;
37
+ }
38
+ return pids;
39
+ }
40
+
41
+ async function findTimelineFolder(): Promise<string | undefined> {
42
+ let dir = process.cwd();
43
+ while (true) {
44
+ let files: string[] = [];
45
+ try {
46
+ files = await fs.promises.readdir(dir);
47
+ } catch { }
48
+ if (files.some(file => PARAMETERS_TIMELINE_FILE_REGEX.test(file))) {
49
+ return dir.replaceAll("\\", "/") + "/";
50
+ }
51
+ let parent = path.dirname(dir);
52
+ if (parent === dir) return undefined;
53
+ dir = parent;
54
+ }
55
+ }
56
+
57
+ function formatBoundary(time: number): string {
58
+ if (time === ALIVE_WINDOW_FOREVER) return "∞";
59
+ if (!time) return "start";
60
+ return formatDateTimeDetailed(time);
61
+ }
62
+
63
+ async function showTimeline() {
64
+ let folder = await findTimelineFolder();
65
+ if (!folder) {
66
+ console.log(`No <n>-parameters.json files found in ${process.cwd()} or any ancestor folder`);
67
+ return;
68
+ }
69
+ let ancestorPids = await getAncestorPids();
70
+ let entries: { n: number; entry: ParametersTimelineEntry }[] = [];
71
+ for (let file of await fs.promises.readdir(folder)) {
72
+ let match = PARAMETERS_TIMELINE_FILE_REGEX.exec(file);
73
+ if (!match) continue;
74
+ try {
75
+ entries.push({ n: parseInt(match[1]), entry: JSON.parse(await fs.promises.readFile(folder + file, "utf8")) as ParametersTimelineEntry });
76
+ } catch (e) {
77
+ console.warn(`Could not read ${folder + file}: ${(e as Error).stack ?? e}`);
78
+ }
79
+ }
80
+ entries.sort((a, b) => a.entry.aliveWindow[0] - b.entry.aliveWindow[0] || a.n - b.n);
81
+
82
+ let now = Date.now();
83
+ const isUs = (e: { entry: ParametersTimelineEntry }) => !!e.entry.pid && ancestorPids.has(e.entry.pid);
84
+
85
+ console.log(`\n${formatDateTimeDetailed(now)} — deployment timeline in ${folder} (our pid chain: ${[...ancestorPids].join(" → ")})`);
86
+ console.log(entries.map(({ n, entry }) => {
87
+ let [start, end] = entry.aliveWindow;
88
+ let marker = end <= now && "✓ done" || start > now && "… future" || "▶ live";
89
+ return `[#${n} ${marker} ${formatBoundary(start)} → ${formatBoundary(end)}${isUs({ entry }) && " (us)" || ""}]`;
90
+ }).join(" ") || "(no entries)");
91
+ for (let { n, entry } of entries) {
92
+ console.log(`#${n}${isUs({ entry }) && " (us)" || ""} pid=${entry.pid ?? "not started"} parameters=${JSON.stringify(entry.parameters)}`);
93
+ }
94
+
95
+ let us = entries.find(isUs);
96
+ if (us) {
97
+ let end = us.entry.aliveWindow[1];
98
+ if (end === ALIVE_WINDOW_FOREVER) {
99
+ console.log(`We are #${us.n}, with no scheduled shutdown`);
100
+ } else {
101
+ console.log(`We are #${us.n}, scheduled to shut down at ${formatDateTimeDetailed(end)} (in ${formatTime(end - now)})`);
102
+ }
103
+ } else {
104
+ console.log(`No entry's pid matches our pid chain — we are either a future version that hasn't been assigned a pid yet, or not a descendant of a deployed instance`);
105
+ }
106
+ let nextDeploy = entries.find(e => e.entry.aliveWindow[0] > now);
107
+ if (nextDeploy) {
108
+ console.log(`Next deploy (#${nextDeploy.n}) releases at ${formatDateTimeDetailed(nextDeploy.entry.aliveWindow[0])} (in ${formatTime(nextDeploy.entry.aliveWindow[0] - now)})`);
109
+ }
110
+ }
111
+
112
+ async function main() {
113
+ while (true) {
114
+ try {
115
+ await showTimeline();
116
+ } catch (e) {
117
+ console.error((e as Error).stack ?? e);
118
+ }
119
+ await delay(POLL_INTERVAL);
120
+ }
121
+ }
122
+
123
+ main().catch(e => console.error((e as Error).stack ?? e));
@@ -264,6 +264,33 @@ async function main() {
264
264
  console.log(`✅ Swap configured: ${swapCheck.split(/\s+/)[1]}MB total vs ${ramMB}MB REAL MEMORY`);
265
265
  }
266
266
 
267
+ // Enable TCP BBR congestion control (with fq qdisc, which BBR is designed to pair with)
268
+ console.log("Checking TCP congestion control...");
269
+ const currentCongestion = (await runPromise(`ssh ${sshRemote} "sysctl -n net.ipv4.tcp_congestion_control"`)).trim();
270
+ if (currentCongestion === "bbr") {
271
+ console.log("✅ TCP BBR already enabled");
272
+ } else {
273
+ await runPromise(`ssh ${sshRemote} "sudo modprobe tcp_bbr"`, { nothrow: true });
274
+ const availableCongestion = await runPromise(`ssh ${sshRemote} "sysctl -n net.ipv4.tcp_available_congestion_control"`);
275
+ if (!availableCongestion.split(/\s+/).includes("bbr")) {
276
+ console.warn(`⚠️ TCP BBR not available on this kernel (current: ${currentCongestion}, available: ${availableCongestion.trim()}). Skipping.`);
277
+ } else {
278
+ console.log(`Enabling TCP BBR (current: ${currentCongestion})...`);
279
+ // Persist across reboots (modules-load.d so tcp_bbr is loaded before sysctl.d is applied)
280
+ await runPromise(`ssh ${sshRemote} "echo 'tcp_bbr' | sudo tee /etc/modules-load.d/bbr.conf"`);
281
+ await runPromise(`ssh ${sshRemote} "printf 'net.core.default_qdisc=fq\\nnet.ipv4.tcp_congestion_control=bbr\\n' | sudo tee /etc/sysctl.d/99-bbr.conf"`);
282
+ // Enable immediately
283
+ await runPromise(`ssh ${sshRemote} "sudo sysctl -w net.core.default_qdisc=fq"`);
284
+ await runPromise(`ssh ${sshRemote} "sudo sysctl -w net.ipv4.tcp_congestion_control=bbr"`);
285
+ const newCongestion = (await runPromise(`ssh ${sshRemote} "sysctl -n net.ipv4.tcp_congestion_control"`)).trim();
286
+ if (newCongestion === "bbr") {
287
+ console.log("✅ TCP BBR enabled");
288
+ } else {
289
+ console.warn(`⚠️ Tried to enable TCP BBR, but congestion control is still: ${newCongestion}`);
290
+ }
291
+ }
292
+ }
293
+
267
294
  let backblazePath = getBackblazePath();
268
295
 
269
296
  console.log("Setting up machine:", sshRemote);
@@ -78,8 +78,8 @@ if (isNode()) {
78
78
  }
79
79
 
80
80
  if (data.toString().includes("\r")) {
81
- import("./watchdog").then(m => m.logUnfiltered());
82
- //logAll();
81
+ //import("./watchdog").then(m => m.logUnfiltered());
82
+ logAll();
83
83
  }
84
84
  });
85
85
 
@@ -0,0 +1,58 @@
1
+ import { qreact } from "../4-dom/qreact";
2
+ import { css } from "typesafecss";
3
+ import { formatNumber } from "socket-function/src/formatting/format";
4
+
5
+ const FLASH_CLASS_NAME = "UsageBar-flash";
6
+
7
+ export const MEMORY_WARNING_THRESHOLD = 0.7;
8
+ export const MEMORY_ERROR_THRESHOLD = 0.85;
9
+ export const DISK_WARNING_THRESHOLD = 0.8;
10
+ export const DISK_ERROR_THRESHOLD = 0.92;
11
+
12
+ /** The standard thresholds for the machine resource bar types ("MEMORY" / "DISK"), so every page showing these bars warns identically. */
13
+ export function getUsageThresholds(type: string): { warningThreshold?: number; errorThreshold?: number } {
14
+ if (type === "MEMORY") {
15
+ return { warningThreshold: MEMORY_WARNING_THRESHOLD, errorThreshold: MEMORY_ERROR_THRESHOLD };
16
+ }
17
+ if (type === "DISK") {
18
+ return { warningThreshold: DISK_WARNING_THRESHOLD, errorThreshold: DISK_ERROR_THRESHOLD };
19
+ }
20
+ return {};
21
+ }
22
+
23
+ export class UsageBar extends qreact.Component<{
24
+ label: string;
25
+ value: number;
26
+ max: number;
27
+ /** Fraction of max at which the bar fill turns yellow. */
28
+ warningThreshold?: number;
29
+ /** Fraction of max at which the bar fill turns red and flashes. */
30
+ errorThreshold?: number;
31
+ }> {
32
+ render() {
33
+ let { label, value, max, warningThreshold, errorThreshold } = this.props;
34
+ let fraction = max && value / max || 0;
35
+ let isError = errorThreshold !== undefined && fraction >= errorThreshold;
36
+ let isWarning = !isError && warningThreshold !== undefined && fraction >= warningThreshold;
37
+ let fillClass = isError && css.hsl(0, 80, 60) || isWarning && css.hsl(45, 90, 60) || css.hsl(0, 0, 70);
38
+ return <div className={css.pad2(6, 2).hsla(0, 0, 0, 0.1).relative}>
39
+ <div className={
40
+ css.absolute.pos(0, 0).size(`${Math.min(fraction, 1) * 100}%`, "100%")
41
+ + fillClass
42
+ + (isError && (" " + FLASH_CLASS_NAME) || "")
43
+ } />
44
+ <div className={css.relative}>
45
+ {label} ({formatNumber(value)} / {formatNumber(max)})
46
+ </div>
47
+ {isError && <style>{`
48
+ @keyframes ${FLASH_CLASS_NAME}-anim {
49
+ 0%, 100% { background-color: hsl(0, 80%, 60%); }
50
+ 50% { background-color: hsl(0, 80%, 30%); }
51
+ }
52
+ .${FLASH_CLASS_NAME} {
53
+ animation: ${FLASH_CLASS_NAME}-anim 0.6s infinite;
54
+ }
55
+ `}</style>}
56
+ </div>;
57
+ }
58
+ }
package/tsconfig.json CHANGED
@@ -24,6 +24,13 @@
24
24
  ],
25
25
  "socket-function/*": [
26
26
  "./node_modules/socket-function/*"
27
+ ],
28
+ // Resolve to the emitted declarations instead of the linked sources: sliftutils compiles its JSX with preact.createElement, so its .tsx files cannot compile under our qreact jsxFactory.
29
+ "sliftutils": [
30
+ "./node_modules/sliftutils/index.d.ts"
31
+ ],
32
+ "sliftutils/*": [
33
+ "./node_modules/sliftutils/*.d.ts"
27
34
  ]
28
35
  },
29
36
  "experimentalDecorators": true,