blob: 4b234a531462234d76c8379c640e87cb12972f59 [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({
gio0afbaee2025-05-22 04:34:33 +000025 commit: z.optional(
26 z.object({
27 hash: z.string(),
28 message: z.string(),
29 }),
30 ),
gioa1efbad2025-05-21 07:16:45 +000031 commands: z.optional(
32 z.array(
33 z.object({
34 command: z.string(),
35 state: z.string(),
36 }),
37 ),
38 ),
39 }),
40 ),
gio78a22882025-07-01 18:56:01 +000041 logs: LogItemsSchema.optional(),
gioa1efbad2025-05-21 07:16:45 +000042});
43
44export type Worker = z.infer<typeof WorkerSchema>;
45
46class ServiceMonitor {
47 private workers: Map<string, string> = new Map();
gio78a22882025-07-01 18:56:01 +000048 private logs: Map<string, LogItem[]> = new Map();
gioa1efbad2025-05-21 07:16:45 +000049 private statuses: Map<string, Worker["status"]> = new Map();
50
51 constructor(public readonly serviceName: string) {}
52
gio40c0c992025-07-02 13:18:05 +000053 registerWorker(workerId: string, workerAddress: string, workerStatus?: Worker["status"]): void {
gioa1efbad2025-05-21 07:16:45 +000054 this.workers.set(workerId, workerAddress);
gioa1efbad2025-05-21 07:16:45 +000055 if (workerStatus) {
56 this.statuses.set(workerId, workerStatus);
57 }
58 }
59
60 getWorkerAddress(workerId: string): string | undefined {
61 return this.workers.get(workerId);
62 }
63
gio78a22882025-07-01 18:56:01 +000064 getWorkerLog(workerId: string): LogItem[] | undefined {
gioa1efbad2025-05-21 07:16:45 +000065 return this.logs.get(workerId);
66 }
67
68 getWorkerStatus(workerId: string): Worker["status"] | undefined {
69 return this.statuses.get(workerId);
70 }
71
gio78a22882025-07-01 18:56:01 +000072 getAllLogs(): Map<string, LogItem[]> {
gioa1efbad2025-05-21 07:16:45 +000073 return new Map(this.logs);
74 }
75
76 getAllStatuses(): Map<string, Worker["status"]> {
77 return new Map(this.statuses);
78 }
79
80 getWorkerAddresses(): string[] {
81 return Array.from(this.workers.values());
82 }
83
84 getWorkerIds(): string[] {
85 return Array.from(this.workers.keys());
86 }
87
88 hasLogs(): boolean {
89 return this.logs.size > 0;
90 }
gio918780d2025-05-22 08:24:41 +000091
92 async reloadWorker(workerId: string): Promise<void> {
93 const workerAddress = this.workers.get(workerId);
94 if (!workerAddress) {
95 throw new Error(`Worker ${workerId} not found in service ${this.serviceName}`);
96 }
97 try {
98 const response = await fetch(`${workerAddress}/update`, { method: "POST" });
99 if (!response.ok) {
100 throw new Error(
101 `Failed to trigger reload for worker ${workerId} at ${workerAddress}: ${response.statusText}`,
102 );
103 }
104 console.log(`Reload triggered for worker ${workerId} in service ${this.serviceName}`);
105 } catch (error) {
106 console.error(`Error reloading worker ${workerId} in service ${this.serviceName}:`, error);
107 throw error; // Re-throw to be caught by ProjectMonitor
108 }
109 }
gioa1efbad2025-05-21 07:16:45 +0000110}
111
112export class ProjectMonitor {
113 private serviceMonitors: Map<string, ServiceMonitor> = new Map();
114
115 constructor() {}
116
117 registerWorker(workerData: Worker): void {
118 let serviceMonitor = this.serviceMonitors.get(workerData.service);
119 if (!serviceMonitor) {
120 serviceMonitor = new ServiceMonitor(workerData.service);
121 this.serviceMonitors.set(workerData.service, serviceMonitor);
122 }
gio40c0c992025-07-02 13:18:05 +0000123 serviceMonitor.registerWorker(workerData.id, workerData.address, workerData.status);
gioa1efbad2025-05-21 07:16:45 +0000124 }
125
126 getWorkerAddresses(): string[] {
127 let allAddresses: string[] = [];
128 for (const serviceMonitor of this.serviceMonitors.values()) {
129 allAddresses = allAddresses.concat(serviceMonitor.getWorkerAddresses());
130 }
131 return Array.from(new Set(allAddresses));
132 }
133
gio78a22882025-07-01 18:56:01 +0000134 getWorkerLog(serviceName: string, workerId: string): LogItem[] | undefined {
gioa1efbad2025-05-21 07:16:45 +0000135 const serviceMonitor = this.serviceMonitors.get(serviceName);
136 if (serviceMonitor) {
137 return serviceMonitor.getWorkerLog(workerId);
138 }
139 return undefined;
140 }
141
142 getAllServiceNames(): string[] {
143 return Array.from(this.serviceMonitors.keys());
144 }
145
146 hasLogs(): boolean {
147 for (const serviceMonitor of this.serviceMonitors.values()) {
148 if (serviceMonitor.hasLogs()) {
149 return true;
150 }
151 }
152 return false;
153 }
154
155 getServiceMonitor(serviceName: string): ServiceMonitor | undefined {
156 return this.serviceMonitors.get(serviceName);
157 }
158
159 getWorkerStatusesForService(serviceName: string): Map<string, Worker["status"]> {
160 const serviceMonitor = this.serviceMonitors.get(serviceName);
161 if (serviceMonitor) {
162 return serviceMonitor.getAllStatuses();
163 }
164 return new Map();
165 }
gio918780d2025-05-22 08:24:41 +0000166
167 async reloadWorker(serviceName: string, workerId: string): Promise<void> {
168 const serviceMonitor = this.serviceMonitors.get(serviceName);
169 if (!serviceMonitor) {
170 throw new Error(`Service ${serviceName} not found`);
171 }
172 await serviceMonitor.reloadWorker(workerId);
173 }
gioa1efbad2025-05-21 07:16:45 +0000174}