269 lines
6.6 KiB
TypeScript
269 lines
6.6 KiB
TypeScript
|
|
/**
|
||
|
|
* ModelGrid Daemon
|
||
|
|
*
|
||
|
|
* Background process for managing containers and serving the API.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import process from 'node:process';
|
||
|
|
import { logger } from './logger.ts';
|
||
|
|
import { TIMING } from './constants.ts';
|
||
|
|
import type { ModelGrid } from './modelgrid.ts';
|
||
|
|
import { ApiServer } from './api/server.ts';
|
||
|
|
import type { IModelGridConfig } from './interfaces/config.ts';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* ModelGrid Daemon
|
||
|
|
*/
|
||
|
|
export class Daemon {
|
||
|
|
private modelgrid: ModelGrid;
|
||
|
|
private isRunning: boolean = false;
|
||
|
|
private apiServer?: ApiServer;
|
||
|
|
|
||
|
|
constructor(modelgrid: ModelGrid) {
|
||
|
|
this.modelgrid = modelgrid;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start the daemon
|
||
|
|
*/
|
||
|
|
public async start(): Promise<void> {
|
||
|
|
if (this.isRunning) {
|
||
|
|
logger.warn('Daemon is already running');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.log('Starting ModelGrid daemon...');
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Initialize ModelGrid
|
||
|
|
await this.modelgrid.initialize();
|
||
|
|
|
||
|
|
const config = this.modelgrid.getConfig();
|
||
|
|
if (!config) {
|
||
|
|
throw new Error('Failed to load configuration');
|
||
|
|
}
|
||
|
|
|
||
|
|
this.logConfigLoaded(config);
|
||
|
|
|
||
|
|
// Start API server
|
||
|
|
await this.startApiServer(config);
|
||
|
|
|
||
|
|
// Start containers
|
||
|
|
await this.startContainers();
|
||
|
|
|
||
|
|
// Preload models if configured
|
||
|
|
await this.preloadModels(config);
|
||
|
|
|
||
|
|
// Setup signal handlers
|
||
|
|
this.setupSignalHandlers();
|
||
|
|
|
||
|
|
this.isRunning = true;
|
||
|
|
|
||
|
|
// Start monitoring loop
|
||
|
|
await this.monitor();
|
||
|
|
} catch (error) {
|
||
|
|
this.isRunning = false;
|
||
|
|
logger.error(`Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
|
process.exit(1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Stop the daemon
|
||
|
|
*/
|
||
|
|
public async stop(): Promise<void> {
|
||
|
|
if (!this.isRunning) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.log('Stopping ModelGrid daemon...');
|
||
|
|
|
||
|
|
this.isRunning = false;
|
||
|
|
|
||
|
|
// Stop API server
|
||
|
|
if (this.apiServer) {
|
||
|
|
await this.apiServer.stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Shutdown ModelGrid (stops containers)
|
||
|
|
await this.modelgrid.shutdown();
|
||
|
|
|
||
|
|
logger.success('ModelGrid daemon stopped');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start the API server
|
||
|
|
*/
|
||
|
|
private async startApiServer(config: IModelGridConfig): Promise<void> {
|
||
|
|
logger.info('Starting API server...');
|
||
|
|
|
||
|
|
this.apiServer = new ApiServer(
|
||
|
|
config.api,
|
||
|
|
this.modelgrid.getContainerManager(),
|
||
|
|
this.modelgrid.getModelRegistry(),
|
||
|
|
);
|
||
|
|
|
||
|
|
await this.apiServer.start();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Start configured containers
|
||
|
|
*/
|
||
|
|
private async startContainers(): Promise<void> {
|
||
|
|
logger.info('Starting containers...');
|
||
|
|
|
||
|
|
const containerManager = this.modelgrid.getContainerManager();
|
||
|
|
await containerManager.startAll();
|
||
|
|
|
||
|
|
// Wait for containers to be healthy
|
||
|
|
logger.dim('Waiting for containers to become healthy...');
|
||
|
|
await this.waitForContainersHealthy();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Wait for all containers to report healthy
|
||
|
|
*/
|
||
|
|
private async waitForContainersHealthy(timeout: number = 60000): Promise<void> {
|
||
|
|
const startTime = Date.now();
|
||
|
|
const containerManager = this.modelgrid.getContainerManager();
|
||
|
|
|
||
|
|
while (Date.now() - startTime < timeout) {
|
||
|
|
const allHealthy = await containerManager.checkAllHealth();
|
||
|
|
|
||
|
|
if (allHealthy) {
|
||
|
|
logger.success('All containers are healthy');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await this.sleep(5000);
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.warn('Timeout waiting for containers to become healthy');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Preload configured models
|
||
|
|
*/
|
||
|
|
private async preloadModels(config: IModelGridConfig): Promise<void> {
|
||
|
|
if (!config.models.autoLoad || config.models.autoLoad.length === 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info(`Preloading ${config.models.autoLoad.length} model(s)...`);
|
||
|
|
|
||
|
|
const modelLoader = this.modelgrid.getModelLoader();
|
||
|
|
const results = await modelLoader.preloadModels(config.models.autoLoad);
|
||
|
|
|
||
|
|
let loaded = 0;
|
||
|
|
let failed = 0;
|
||
|
|
|
||
|
|
for (const [name, result] of results) {
|
||
|
|
if (result.success) {
|
||
|
|
loaded++;
|
||
|
|
logger.dim(` ✓ ${name}`);
|
||
|
|
} else {
|
||
|
|
failed++;
|
||
|
|
logger.warn(` ✗ ${name}: ${result.error}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (failed > 0) {
|
||
|
|
logger.warn(`Preloaded ${loaded}/${config.models.autoLoad.length} models (${failed} failed)`);
|
||
|
|
} else {
|
||
|
|
logger.success(`Preloaded ${loaded} model(s)`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Setup signal handlers for graceful shutdown
|
||
|
|
*/
|
||
|
|
private setupSignalHandlers(): void {
|
||
|
|
const shutdown = async () => {
|
||
|
|
logger.log('');
|
||
|
|
logger.log('Received shutdown signal');
|
||
|
|
await this.stop();
|
||
|
|
process.exit(0);
|
||
|
|
};
|
||
|
|
|
||
|
|
process.on('SIGINT', shutdown);
|
||
|
|
process.on('SIGTERM', shutdown);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Main monitoring loop
|
||
|
|
*/
|
||
|
|
private async monitor(): Promise<void> {
|
||
|
|
logger.log('Starting monitoring loop...');
|
||
|
|
|
||
|
|
const config = this.modelgrid.getConfig();
|
||
|
|
const checkInterval = config?.checkInterval || TIMING.CHECK_INTERVAL_MS;
|
||
|
|
|
||
|
|
while (this.isRunning) {
|
||
|
|
try {
|
||
|
|
// Check container health
|
||
|
|
await this.checkContainerHealth();
|
||
|
|
|
||
|
|
// Log periodic status
|
||
|
|
this.logPeriodicStatus();
|
||
|
|
|
||
|
|
await this.sleep(checkInterval);
|
||
|
|
} catch (error) {
|
||
|
|
logger.error(`Monitor error: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
|
await this.sleep(checkInterval);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check health of all containers
|
||
|
|
*/
|
||
|
|
private async checkContainerHealth(): Promise<void> {
|
||
|
|
const containerManager = this.modelgrid.getContainerManager();
|
||
|
|
const statuses = await containerManager.getAllStatus();
|
||
|
|
|
||
|
|
for (const [id, status] of statuses) {
|
||
|
|
if (status.running && status.health === 'unhealthy') {
|
||
|
|
logger.warn(`Container ${id} is unhealthy, attempting restart...`);
|
||
|
|
|
||
|
|
const container = containerManager.getContainer(id);
|
||
|
|
if (container) {
|
||
|
|
await container.restart();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log periodic status
|
||
|
|
*/
|
||
|
|
private logPeriodicStatus(): void {
|
||
|
|
if (this.apiServer) {
|
||
|
|
const info = this.apiServer.getInfo();
|
||
|
|
if (info.running) {
|
||
|
|
logger.dim(`API server running on ${info.host}:${info.port} (uptime: ${info.uptime}s)`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log configuration loaded message
|
||
|
|
*/
|
||
|
|
private logConfigLoaded(config: IModelGridConfig): void {
|
||
|
|
logger.log('');
|
||
|
|
logger.logBoxTitle('Configuration Loaded', 60, 'success');
|
||
|
|
logger.logBoxLine(`API Port: ${config.api.port}`);
|
||
|
|
logger.logBoxLine(`Containers: ${config.containers.length}`);
|
||
|
|
logger.logBoxLine(`Auto-pull: ${config.models.autoPull ? 'Enabled' : 'Disabled'}`);
|
||
|
|
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000}s`);
|
||
|
|
logger.logBoxEnd();
|
||
|
|
logger.log('');
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sleep for specified milliseconds
|
||
|
|
*/
|
||
|
|
private sleep(ms: number): Promise<void> {
|
||
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
|
}
|
||
|
|
}
|