blob: 7dd2f3ce9560db472f276c6679323d0e51bca6a6 [file] [log] [blame]
gioa1efbad2025-05-21 07:16:45 +00001import { z } from "zod";
2
gio78a22882025-07-01 18:56:01 +00003const LogItemSchema = z.object({
4 runId: z.string(),
5 timestampMilli: z.number(),
6 commit: z.string().optional(),
7 contents: z.preprocess((val) => {
8 if (typeof val === "string") {
9 return Buffer.from(val, "base64").toString("utf-8");
10 }
11 throw new Error("Log item contents is not a string");
12 }, z.string()),
13});
14
15export type LogItem = z.infer<typeof LogItemSchema>;
16
17const LogItemsSchema = z.array(LogItemSchema);
18
gioa1efbad2025-05-21 07:16:45 +000019export const WorkerSchema = z.object({
20 id: z.string(),
21 service: z.string(),
22 address: z.string().url(),
23 status: z.optional(
24 z.object({
gioa70535a2025-07-02 15:50:25 +000025 commit: z
26 .optional(
27 z.object({
28 hash: z.string(),
29 message: z.string(),
30 }),
31 )
32 .nullable(),
gioa1efbad2025-05-21 07:16:45 +000033 commands: z.optional(
34 z.array(
35 z.object({
36 command: z.string(),
37 state: z.string(),
38 }),
39 ),
40 ),
41 }),
42 ),
gio78a22882025-07-01 18:56:01 +000043 logs: LogItemsSchema.optional(),
gioa1efbad2025-05-21 07:16:45 +000044});
45
46export type Worker = z.infer<typeof WorkerSchema>;
47
48class ServiceMonitor {
49 private workers: Map<string, string> = new Map();
gio78a22882025-07-01 18:56:01 +000050 private logs: Map<string, LogItem[]> = new Map();
gioa1efbad2025-05-21 07:16:45 +000051 private statuses: Map<string, Worker["status"]> = new Map();
52
53 constructor(public readonly serviceName: string) {}
54
gio40c0c992025-07-02 13:18:05 +000055 registerWorker(workerId: string, workerAddress: string, workerStatus?: Worker["status"]): void {
gioa1efbad2025-05-21 07:16:45 +000056 this.workers.set(workerId, workerAddress);
gioa1efbad2025-05-21 07:16:45 +000057 if (workerStatus) {
58 this.statuses.set(workerId, workerStatus);
59 }
60 }
61
62 getWorkerAddress(workerId: string): string | undefined {
63 return this.workers.get(workerId);
64 }
65
gio78a22882025-07-01 18:56:01 +000066 getWorkerLog(workerId: string): LogItem[] | undefined {
gioa1efbad2025-05-21 07:16:45 +000067 return this.logs.get(workerId);
68 }
69
70 getWorkerStatus(workerId: string): Worker["status"] | undefined {
71 return this.statuses.get(workerId);
72 }
73
gio78a22882025-07-01 18:56:01 +000074 getAllLogs(): Map<string, LogItem[]> {
gioa1efbad2025-05-21 07:16:45 +000075 return new Map(this.logs);
76 }
77
78 getAllStatuses(): Map<string, Worker["status"]> {
79 return new Map(this.statuses);
80 }
81
82 getWorkerAddresses(): string[] {
83 return Array.from(this.workers.values());
84 }
85
86 getWorkerIds(): string[] {
87 return Array.from(this.workers.keys());
88 }
89
90 hasLogs(): boolean {
91 return this.logs.size > 0;
92 }
gio918780d2025-05-22 08:24:41 +000093
94 async reloadWorker(workerId: string): Promise<void> {
95 const workerAddress = this.workers.get(workerId);
96 if (!workerAddress) {
97 throw new Error(`Worker ${workerId} not found in service ${this.serviceName}`);
98 }
99 try {
100 const response = await fetch(`${workerAddress}/update`, { method: "POST" });
101 if (!response.ok) {
102 throw new Error(
103 `Failed to trigger reload for worker ${workerId} at ${workerAddress}: ${response.statusText}`,
104 );
105 }
106 console.log(`Reload triggered for worker ${workerId} in service ${this.serviceName}`);
107 } catch (error) {
108 console.error(`Error reloading worker ${workerId} in service ${this.serviceName}:`, error);
109 throw error; // Re-throw to be caught by ProjectMonitor
110 }
111 }
gio577d2342025-07-03 12:50:18 +0000112
113 async terminateWorker(workerId: string): Promise<void> {
114 const workerAddress = this.workers.get(workerId);
115 if (!workerAddress) {
116 throw new Error(`Worker ${workerId} not found in service ${this.serviceName}`);
117 }
118 try {
119 const response = await fetch(`${workerAddress}/quitquitquit`, { method: "POST" });
120 if (!response.ok) {
121 throw new Error(`Failed to terminate worker ${workerId} at ${workerAddress}: ${response.statusText}`);
122 }
123 console.log(`Terminated worker ${workerId} in service ${this.serviceName}`);
124 } catch (error) {
125 console.error(`Error terminating worker ${workerId} in service ${this.serviceName}:`, error);
126 throw error; // Re-throw to be caught by ProjectMonitor
127 }
128 }
gioa1efbad2025-05-21 07:16:45 +0000129}
130
131export class ProjectMonitor {
132 private serviceMonitors: Map<string, ServiceMonitor> = new Map();
133
134 constructor() {}
135
136 registerWorker(workerData: Worker): void {
137 let serviceMonitor = this.serviceMonitors.get(workerData.service);
138 if (!serviceMonitor) {
139 serviceMonitor = new ServiceMonitor(workerData.service);
140 this.serviceMonitors.set(workerData.service, serviceMonitor);
141 }
gio40c0c992025-07-02 13:18:05 +0000142 serviceMonitor.registerWorker(workerData.id, workerData.address, workerData.status);
gioa1efbad2025-05-21 07:16:45 +0000143 }
144
145 getWorkerAddresses(): string[] {
146 let allAddresses: string[] = [];
147 for (const serviceMonitor of this.serviceMonitors.values()) {
148 allAddresses = allAddresses.concat(serviceMonitor.getWorkerAddresses());
149 }
150 return Array.from(new Set(allAddresses));
151 }
152
gio78a22882025-07-01 18:56:01 +0000153 getWorkerLog(serviceName: string, workerId: string): LogItem[] | undefined {
gioa1efbad2025-05-21 07:16:45 +0000154 const serviceMonitor = this.serviceMonitors.get(serviceName);
155 if (serviceMonitor) {
156 return serviceMonitor.getWorkerLog(workerId);
157 }
158 return undefined;
159 }
160
161 getAllServiceNames(): string[] {
162 return Array.from(this.serviceMonitors.keys());
163 }
164
165 hasLogs(): boolean {
166 for (const serviceMonitor of this.serviceMonitors.values()) {
167 if (serviceMonitor.hasLogs()) {
168 return true;
169 }
170 }
171 return false;
172 }
173
174 getServiceMonitor(serviceName: string): ServiceMonitor | undefined {
175 return this.serviceMonitors.get(serviceName);
176 }
177
178 getWorkerStatusesForService(serviceName: string): Map<string, Worker["status"]> {
179 const serviceMonitor = this.serviceMonitors.get(serviceName);
180 if (serviceMonitor) {
181 return serviceMonitor.getAllStatuses();
182 }
183 return new Map();
184 }
gio918780d2025-05-22 08:24:41 +0000185
186 async reloadWorker(serviceName: string, workerId: string): Promise<void> {
187 const serviceMonitor = this.serviceMonitors.get(serviceName);
188 if (!serviceMonitor) {
189 throw new Error(`Service ${serviceName} not found`);
190 }
191 await serviceMonitor.reloadWorker(workerId);
192 }
gio577d2342025-07-03 12:50:18 +0000193
194 async terminateWorker(serviceName: string, workerId: string): Promise<void> {
195 const serviceMonitor = this.serviceMonitors.get(serviceName);
196 if (!serviceMonitor) {
197 throw new Error(`Service ${serviceName} not found`);
198 }
199 await serviceMonitor.terminateWorker(workerId);
200 }
gioa1efbad2025-05-21 07:16:45 +0000201}