blob: df20e0593de10ec2da1a9f7db95f412880b1e1a1 [file] [log] [blame]
import { z } from "zod";
const LogItemSchema = z.object({
runId: z.string(),
timestampMilli: z.number(),
commit: z.string().optional(),
contents: z.preprocess((val) => {
if (typeof val === "string") {
return Buffer.from(val, "base64").toString("utf-8");
}
throw new Error("Log item contents is not a string");
}, z.string()),
});
export type LogItem = z.infer<typeof LogItemSchema>;
const LogItemsSchema = z.array(LogItemSchema);
export const WorkerSchema = z.object({
id: z.string(),
service: z.string(),
address: z.string().url(),
status: z.optional(
z.object({
commit: z
.optional(
z.object({
hash: z.string(),
message: z.string(),
}),
)
.nullable(),
commands: z.optional(
z.array(
z.object({
command: z.string(),
state: z.string(),
}),
),
),
}),
),
logs: LogItemsSchema.optional(),
});
export type Worker = z.infer<typeof WorkerSchema>;
class ServiceMonitor {
private workers: Map<string, string> = new Map();
private logs: Map<string, LogItem[]> = new Map();
private statuses: Map<string, Worker["status"]> = new Map();
constructor(public readonly serviceName: string) {}
registerWorker(workerId: string, workerAddress: string, workerStatus?: Worker["status"]): void {
this.workers.set(workerId, workerAddress);
if (workerStatus) {
this.statuses.set(workerId, workerStatus);
}
}
getWorkerAddress(workerId: string): string | undefined {
return this.workers.get(workerId);
}
getWorkerLog(workerId: string): LogItem[] | undefined {
return this.logs.get(workerId);
}
getWorkerStatus(workerId: string): Worker["status"] | undefined {
return this.statuses.get(workerId);
}
getAllLogs(): Map<string, LogItem[]> {
return new Map(this.logs);
}
getAllStatuses(): Map<string, Worker["status"]> {
return new Map(this.statuses);
}
getWorkerAddresses(): string[] {
return Array.from(this.workers.values());
}
getWorkerIds(): string[] {
return Array.from(this.workers.keys());
}
hasLogs(): boolean {
return this.logs.size > 0;
}
async reloadWorker(workerId: string): Promise<void> {
const workerAddress = this.workers.get(workerId);
if (!workerAddress) {
throw new Error(`Worker ${workerId} not found in service ${this.serviceName}`);
}
try {
const response = await fetch(`${workerAddress}/update`, { method: "POST" });
if (!response.ok) {
throw new Error(
`Failed to trigger reload for worker ${workerId} at ${workerAddress}: ${response.statusText}`,
);
}
console.log(`Reload triggered for worker ${workerId} in service ${this.serviceName}`);
} catch (error) {
console.error(`Error reloading worker ${workerId} in service ${this.serviceName}:`, error);
throw error; // Re-throw to be caught by ProjectMonitor
}
}
}
export class ProjectMonitor {
private serviceMonitors: Map<string, ServiceMonitor> = new Map();
constructor() {}
registerWorker(workerData: Worker): void {
let serviceMonitor = this.serviceMonitors.get(workerData.service);
if (!serviceMonitor) {
serviceMonitor = new ServiceMonitor(workerData.service);
this.serviceMonitors.set(workerData.service, serviceMonitor);
}
serviceMonitor.registerWorker(workerData.id, workerData.address, workerData.status);
}
getWorkerAddresses(): string[] {
let allAddresses: string[] = [];
for (const serviceMonitor of this.serviceMonitors.values()) {
allAddresses = allAddresses.concat(serviceMonitor.getWorkerAddresses());
}
return Array.from(new Set(allAddresses));
}
getWorkerLog(serviceName: string, workerId: string): LogItem[] | undefined {
const serviceMonitor = this.serviceMonitors.get(serviceName);
if (serviceMonitor) {
return serviceMonitor.getWorkerLog(workerId);
}
return undefined;
}
getAllServiceNames(): string[] {
return Array.from(this.serviceMonitors.keys());
}
hasLogs(): boolean {
for (const serviceMonitor of this.serviceMonitors.values()) {
if (serviceMonitor.hasLogs()) {
return true;
}
}
return false;
}
getServiceMonitor(serviceName: string): ServiceMonitor | undefined {
return this.serviceMonitors.get(serviceName);
}
getWorkerStatusesForService(serviceName: string): Map<string, Worker["status"]> {
const serviceMonitor = this.serviceMonitors.get(serviceName);
if (serviceMonitor) {
return serviceMonitor.getAllStatuses();
}
return new Map();
}
async reloadWorker(serviceName: string, workerId: string): Promise<void> {
const serviceMonitor = this.serviceMonitors.get(serviceName);
if (!serviceMonitor) {
throw new Error(`Service ${serviceName} not found`);
}
await serviceMonitor.reloadWorker(workerId);
}
}