@ronkovic/aad 0.3.0 → 0.3.2

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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Memory Check Utility
3
+ * Provides memory status checking for AAD pipeline execution
4
+ */
5
+
6
+ import type pino from "pino";
7
+ import { MemoryError } from "./errors";
8
+
9
+ export interface MemoryStatus {
10
+ totalGB: number;
11
+ usedGB: number;
12
+ freeGB: number;
13
+ usedPercent: number;
14
+ isLowMemory: boolean;
15
+ recommendedAction?: string;
16
+ }
17
+
18
+ /**
19
+ * Get current system memory status (macOS only)
20
+ */
21
+ export async function getMemoryStatus(): Promise<MemoryStatus> {
22
+ const { stdout } = await Bun.spawn(["vm_stat"], {
23
+ stdout: "pipe",
24
+ }).exited.then(async (code) => {
25
+ if (code !== 0) throw new Error("vm_stat failed");
26
+ return {
27
+ stdout: await new Response(Bun.spawn(["vm_stat"], { stdout: "pipe" }).stdout).text(),
28
+ };
29
+ });
30
+
31
+ // Parse vm_stat output
32
+ const pageSizeMatch = stdout.match(/page size of (\d+) bytes/);
33
+ const freeMatch = stdout.match(/Pages free:\s+(\d+)/);
34
+ const activeMatch = stdout.match(/Pages active:\s+(\d+)/);
35
+ const inactiveMatch = stdout.match(/Pages inactive:\s+(\d+)/);
36
+ const speculativeMatch = stdout.match(/Pages speculative:\s+(\d+)/);
37
+ const wiredMatch = stdout.match(/Pages wired down:\s+(\d+)/);
38
+
39
+ if (!pageSizeMatch || !freeMatch || !activeMatch || !inactiveMatch || !wiredMatch) {
40
+ throw new Error("Failed to parse vm_stat output");
41
+ }
42
+
43
+ const pageSize = parseInt(pageSizeMatch[1]!, 10);
44
+ const freePages = parseInt(freeMatch[1]!, 10);
45
+ const activePages = parseInt(activeMatch[1]!, 10);
46
+ const inactivePages = parseInt(inactiveMatch[1]!, 10);
47
+ const speculativePages = parseInt(speculativeMatch?.[1] ?? "0", 10);
48
+ const wiredPages = parseInt(wiredMatch[1]!, 10);
49
+
50
+ // Calculate memory in GB
51
+ const bytesToGB = (bytes: number) => bytes / (1024 * 1024 * 1024);
52
+ const pagesToGB = (pages: number) => bytesToGB(pages * pageSize);
53
+
54
+ const freeGB = pagesToGB(freePages + speculativePages);
55
+ const usedGB = pagesToGB(activePages + inactivePages + wiredPages);
56
+ const totalGB = freeGB + usedGB;
57
+ const usedPercent = (usedGB / totalGB) * 100;
58
+
59
+ // Determine if memory is low
60
+ const isLowMemory = freeGB < 2.0; // Less than 2GB free
61
+ let recommendedAction: string | undefined;
62
+
63
+ if (freeGB < 1.5) {
64
+ recommendedAction = "Critical: Free memory < 1.5GB. Close heavy applications (Chrome, Docker) before running AAD.";
65
+ } else if (freeGB < 2.5) {
66
+ recommendedAction = "Warning: Free memory < 2.5GB. Consider closing unnecessary applications.";
67
+ }
68
+
69
+ return {
70
+ totalGB: Math.round(totalGB * 10) / 10,
71
+ usedGB: Math.round(usedGB * 10) / 10,
72
+ freeGB: Math.round(freeGB * 10) / 10,
73
+ usedPercent: Math.round(usedPercent),
74
+ isLowMemory,
75
+ recommendedAction,
76
+ };
77
+ }
78
+
79
+ export interface WaitForMemoryOptions {
80
+ minFreeGB?: number;
81
+ retryIntervalMs?: number;
82
+ maxRetries?: number;
83
+ }
84
+
85
+ export interface WaitForMemoryResult {
86
+ shouldReduceWorkers: boolean;
87
+ freeGB: number;
88
+ }
89
+
90
+ /**
91
+ * Wait for sufficient memory before dispatching a task.
92
+ * - freeGB < 1.5GB → wait and retry (default 30s, max 5 retries)
93
+ * - freeGB < 1.0GB after all retries → throw MemoryError
94
+ * - freeGB < 3.0GB → return { shouldReduceWorkers: true }
95
+ */
96
+ export async function waitForMemory(
97
+ logger: pino.Logger,
98
+ options?: WaitForMemoryOptions,
99
+ _getMemoryStatus?: () => Promise<MemoryStatus>,
100
+ _sleep?: (ms: number) => Promise<void>,
101
+ ): Promise<WaitForMemoryResult> {
102
+ const minFreeGB = options?.minFreeGB ?? 1.5;
103
+ const retryIntervalMs = options?.retryIntervalMs ?? 30_000;
104
+ const maxRetries = options?.maxRetries ?? 5;
105
+ const getStatus = _getMemoryStatus ?? getMemoryStatus;
106
+ const sleep = _sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
107
+
108
+ let status = await getStatus();
109
+ let retries = 0;
110
+
111
+ while (status.freeGB < minFreeGB && retries < maxRetries) {
112
+ retries++;
113
+ logger.warn(
114
+ { freeGB: status.freeGB, retry: retries, maxRetries },
115
+ `Low memory (${status.freeGB}GB free). Waiting ${retryIntervalMs / 1000}s before retry ${retries}/${maxRetries}...`,
116
+ );
117
+ await sleep(retryIntervalMs);
118
+ status = await getStatus();
119
+ }
120
+
121
+ if (status.freeGB < 1.0) {
122
+ throw new MemoryError(`Insufficient memory: ${status.freeGB}GB free (need at least 1.0GB)`, {
123
+ freeGB: status.freeGB,
124
+ retries,
125
+ });
126
+ }
127
+
128
+ return {
129
+ shouldReduceWorkers: status.freeGB < 3.0,
130
+ freeGB: status.freeGB,
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Check memory and log warning if low
136
+ */
137
+ export async function checkMemoryAndWarn(logger: pino.Logger): Promise<void> {
138
+ try {
139
+ const status = await getMemoryStatus();
140
+
141
+ logger.info({
142
+ totalGB: status.totalGB,
143
+ usedGB: status.usedGB,
144
+ freeGB: status.freeGB,
145
+ usedPercent: status.usedPercent,
146
+ }, "Memory status");
147
+
148
+ if (status.isLowMemory && status.recommendedAction) {
149
+ logger.warn({ freeGB: status.freeGB }, status.recommendedAction);
150
+ console.warn(`\n⚠️ ${status.recommendedAction}\n`);
151
+ }
152
+ } catch (error) {
153
+ logger.debug({ error }, "Memory check failed (non-critical)");
154
+ }
155
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Memory Monitor
3
+ * Polls system memory during SDK execution and fires callback when critical.
4
+ */
5
+
6
+ import type pino from "pino";
7
+ import { getMemoryStatus, type MemoryStatus } from "./memory-check";
8
+
9
+ export interface MemoryMonitorOptions {
10
+ logger: pino.Logger;
11
+ checkIntervalMs?: number;
12
+ criticalFreeGB?: number;
13
+ /** For testing: inject a custom getMemoryStatus */
14
+ _getMemoryStatus?: () => Promise<MemoryStatus>;
15
+ }
16
+
17
+ export interface MemoryMonitor {
18
+ start(): void;
19
+ stop(): void;
20
+ onCritical(callback: () => void): void;
21
+ }
22
+
23
+ export function createMemoryMonitor(options: MemoryMonitorOptions): MemoryMonitor {
24
+ const {
25
+ logger,
26
+ checkIntervalMs = 2000,
27
+ criticalFreeGB = 0.8,
28
+ _getMemoryStatus = getMemoryStatus,
29
+ } = options;
30
+
31
+ let intervalId: ReturnType<typeof setInterval> | null = null;
32
+ let criticalCallback: (() => void) | null = null;
33
+ let fired = false;
34
+
35
+ return {
36
+ start() {
37
+ if (intervalId) return;
38
+ fired = false;
39
+ intervalId = setInterval(() => {
40
+ void (async () => {
41
+ try {
42
+ const status = await _getMemoryStatus();
43
+ if (status.freeGB < criticalFreeGB && !fired) {
44
+ fired = true;
45
+ logger.error(
46
+ { freeGB: status.freeGB, criticalFreeGB },
47
+ "Critical memory threshold reached"
48
+ );
49
+ criticalCallback?.();
50
+ }
51
+ } catch (err) {
52
+ logger.debug({ err }, "Memory monitor check failed");
53
+ }
54
+ })();
55
+ }, checkIntervalMs);
56
+ },
57
+
58
+ stop() {
59
+ if (intervalId) {
60
+ clearInterval(intervalId);
61
+ intervalId = null;
62
+ }
63
+ },
64
+
65
+ onCritical(callback: () => void) {
66
+ criticalCallback = callback;
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Graceful Shutdown Handler
3
+ * Saves run state on SIGTERM/SIGINT/uncaughtException so --resume can pick up.
4
+ */
5
+
6
+ import type pino from "pino";
7
+ import type { RunStore, TaskStore } from "../modules/persistence/stores.port";
8
+ import type { RunId } from "./types";
9
+
10
+ export interface ShutdownHandlerOptions {
11
+ runId: RunId;
12
+ stores: {
13
+ runStore: RunStore;
14
+ taskStore: TaskStore;
15
+ };
16
+ logger: pino.Logger;
17
+ exitFn?: (code: number) => void;
18
+ }
19
+
20
+ let _shuttingDown = false;
21
+ let _lastSignalTime = 0;
22
+ let _installed = false;
23
+ let _cleanup: (() => void) | null = null;
24
+
25
+ /**
26
+ * Check if the process is shutting down.
27
+ */
28
+ export function isShuttingDown(): boolean {
29
+ return _shuttingDown;
30
+ }
31
+
32
+ /**
33
+ * Reset internal state (for testing only).
34
+ */
35
+ export function _resetShutdownState(): void {
36
+ _shuttingDown = false;
37
+ _lastSignalTime = 0;
38
+ if (_cleanup) {
39
+ _cleanup();
40
+ _cleanup = null;
41
+ }
42
+ _installed = false;
43
+ }
44
+
45
+ /**
46
+ * Install signal handlers for graceful shutdown with state persistence.
47
+ */
48
+ export function installShutdownHandler(options: ShutdownHandlerOptions): void {
49
+ if (_installed) return;
50
+ _installed = true;
51
+
52
+ const { runId, stores, logger, exitFn = (code: number) => process.exit(code) } = options;
53
+ let saving = false;
54
+
55
+ const handleShutdown = async (reason: string) => {
56
+ const now = Date.now();
57
+ // Debounce: ignore signals within 1s
58
+ if (now - _lastSignalTime < 1000) return;
59
+ _lastSignalTime = now;
60
+
61
+ if (saving) return;
62
+ saving = true;
63
+ _shuttingDown = true;
64
+
65
+ logger.warn({ runId, reason }, "Graceful shutdown initiated, saving state...");
66
+
67
+ try {
68
+ // Update running tasks to pending so resume can re-dispatch them
69
+ const tasks = await stores.taskStore.getAll();
70
+ for (const task of tasks) {
71
+ if (task.status === "running") {
72
+ await stores.taskStore.save({ ...task, status: "pending", workerId: undefined });
73
+ }
74
+ }
75
+
76
+ // Update run state counts
77
+ const allTasks = await stores.taskStore.getAll();
78
+ const pending = allTasks.filter((t) => t.status === "pending").length;
79
+ const completed = allTasks.filter((t) => t.status === "completed").length;
80
+ const failed = allTasks.filter((t) => t.status === "failed").length;
81
+
82
+ const runState = await stores.runStore.get(runId);
83
+ if (runState) {
84
+ await stores.runStore.save({
85
+ ...runState,
86
+ pending,
87
+ running: 0,
88
+ completed,
89
+ failed,
90
+ endTime: new Date().toISOString(),
91
+ });
92
+ }
93
+
94
+ logger.info({ runId, pending, completed, failed }, "State saved. Use --resume to continue.");
95
+ } catch (err) {
96
+ logger.error({ err }, "Failed to save state during shutdown");
97
+ }
98
+
99
+ exitFn(1);
100
+ };
101
+
102
+ const onSigterm = () => { void handleShutdown("SIGTERM"); };
103
+ const onSigint = () => { void handleShutdown("SIGINT"); };
104
+ const onUncaught = (err: Error) => { void handleShutdown(`uncaughtException: ${err.message}`); };
105
+
106
+ process.on("SIGTERM", onSigterm);
107
+ process.on("SIGINT", onSigint);
108
+ process.on("uncaughtException", onUncaught);
109
+
110
+ _cleanup = () => {
111
+ process.removeListener("SIGTERM", onSigterm);
112
+ process.removeListener("SIGINT", onSigint);
113
+ process.removeListener("uncaughtException", onUncaught);
114
+ };
115
+ }
@@ -0,0 +1,49 @@
1
+ // Utility functions for shared functionality
2
+
3
+ /**
4
+ * Capitalizes the first character of a string.
5
+ * @param str - The string to capitalize
6
+ * @returns The string with the first character capitalized
7
+ */
8
+ export function capitalize(str: string): string {
9
+ if (str.length === 0) {
10
+ return str;
11
+ }
12
+ return str.charAt(0).toUpperCase() + str.slice(1);
13
+ }
14
+
15
+ /**
16
+ * Converts all characters in a string to lowercase.
17
+ * @param str - The string to convert
18
+ * @returns The string with all characters in lowercase
19
+ */
20
+ export function toLowerCase(str: string): string {
21
+ return str.toLowerCase();
22
+ }
23
+
24
+ /**
25
+ * Removes leading and trailing whitespace from a string.
26
+ * @param str - The string to trim
27
+ * @returns The string with leading and trailing whitespace removed
28
+ */
29
+ export function trim(str: string): string {
30
+ return str.trim();
31
+ }
32
+
33
+ /**
34
+ * Reverses a string.
35
+ * @param str - The string to reverse
36
+ * @returns The reversed string
37
+ */
38
+ export function reverse(str: string): string {
39
+ return str.split("").reverse().join("");
40
+ }
41
+
42
+ /**
43
+ * Converts all characters in a string to uppercase.
44
+ * @param str - The string to convert
45
+ * @returns The string with all characters in uppercase
46
+ */
47
+ export function toUpperCase(str: string): string {
48
+ return str.toUpperCase();
49
+ }