/** * Shutdown Manager for Graceful Application Termination * * This module provides centralized shutdown coordination for the gitea-mirror application. * It ensures that: * - In-progress jobs are properly saved to the database * - Database connections are closed cleanly * - Background services are stopped gracefully * - No data loss occurs during container restarts */ import { db, mirrorJobs } from './db'; import { eq, and } from 'drizzle-orm'; import type { MirrorJob } from './db/schema'; // Shutdown state tracking let shutdownInProgress = false; let shutdownStartTime: Date | null = null; let shutdownCallbacks: Array<() => Promise> = []; let activeJobs = new Set(); let shutdownTimeout: NodeJS.Timeout | null = null; // Configuration const SHUTDOWN_TIMEOUT = 30000; // 30 seconds max shutdown time const JOB_SAVE_TIMEOUT = 10000; // 10 seconds to save job state /** * Register a callback to be executed during shutdown */ export function registerShutdownCallback(callback: () => Promise): void { shutdownCallbacks.push(callback); } /** * Register an active job that needs to be tracked during shutdown */ export function registerActiveJob(jobId: string): void { activeJobs.add(jobId); console.log(`Registered active job: ${jobId} (${activeJobs.size} total active jobs)`); } /** * Unregister a job when it completes normally */ export function unregisterActiveJob(jobId: string): void { activeJobs.delete(jobId); console.log(`Unregistered job: ${jobId} (${activeJobs.size} remaining active jobs)`); } /** * Check if shutdown is currently in progress */ export function isShuttingDown(): boolean { return shutdownInProgress; } /** * Get shutdown status information */ export function getShutdownStatus() { return { inProgress: shutdownInProgress, startTime: shutdownStartTime, activeJobs: Array.from(activeJobs), registeredCallbacks: shutdownCallbacks.length, }; } /** * Save the current state of an active job to the database */ async function saveJobState(jobId: string): Promise { try { console.log(`Saving state for job ${jobId}...`); // Update the job to mark it as interrupted but not failed await db .update(mirrorJobs) .set({ inProgress: false, lastCheckpoint: new Date(), message: 'Job interrupted by application shutdown - will resume on restart', }) .where(eq(mirrorJobs.id, jobId)); console.log(`✅ Saved state for job ${jobId}`); } catch (error) { console.error(`❌ Failed to save state for job ${jobId}:`, error); throw error; } } /** * Save all active jobs to the database */ async function saveAllActiveJobs(): Promise { if (activeJobs.size === 0) { console.log('No active jobs to save'); return; } console.log(`Saving state for ${activeJobs.size} active jobs...`); const savePromises = Array.from(activeJobs).map(async (jobId) => { try { await Promise.race([ saveJobState(jobId), new Promise((_, reject) => { setTimeout(() => reject(new Error(`Timeout saving job ${jobId}`)), JOB_SAVE_TIMEOUT); }) ]); } catch (error) { console.error(`Failed to save job ${jobId} within timeout:`, error); // Continue with other jobs even if one fails } }); await Promise.allSettled(savePromises); console.log('✅ Completed saving all active jobs'); } /** * Execute all registered shutdown callbacks */ async function executeShutdownCallbacks(): Promise { if (shutdownCallbacks.length === 0) { console.log('No shutdown callbacks to execute'); return; } console.log(`Executing ${shutdownCallbacks.length} shutdown callbacks...`); const callbackPromises = shutdownCallbacks.map(async (callback, index) => { try { await callback(); console.log(`✅ Shutdown callback ${index + 1} completed`); } catch (error) { console.error(`❌ Shutdown callback ${index + 1} failed:`, error); // Continue with other callbacks even if one fails } }); await Promise.allSettled(callbackPromises); console.log('✅ Completed all shutdown callbacks'); } /** * Perform graceful shutdown of the application */ export async function gracefulShutdown(signal: string = 'UNKNOWN'): Promise { if (shutdownInProgress) { console.log('⚠️ Shutdown already in progress, ignoring additional signal'); return; } shutdownInProgress = true; shutdownStartTime = new Date(); console.log(`\n🛑 Graceful shutdown initiated by signal: ${signal}`); console.log(`📊 Shutdown status: ${activeJobs.size} active jobs, ${shutdownCallbacks.length} callbacks`); // Set up shutdown timeout shutdownTimeout = setTimeout(() => { console.error(`❌ Shutdown timeout reached (${SHUTDOWN_TIMEOUT}ms), forcing exit`); process.exit(1); }, SHUTDOWN_TIMEOUT); try { // Step 1: Save all active job states console.log('\n📝 Step 1: Saving active job states...'); await saveAllActiveJobs(); // Step 2: Execute shutdown callbacks (stop services, close connections, etc.) console.log('\n🔧 Step 2: Executing shutdown callbacks...'); await executeShutdownCallbacks(); // Step 3: Close database connections console.log('\n💾 Step 3: Closing database connections...'); // Note: Drizzle with bun:sqlite doesn't require explicit connection closing // but we'll add this for completeness and future database changes console.log('\n✅ Graceful shutdown completed successfully'); // Clear the timeout since we completed successfully if (shutdownTimeout) { clearTimeout(shutdownTimeout); shutdownTimeout = null; } // Exit with success code process.exit(0); } catch (error) { console.error('\n❌ Error during graceful shutdown:', error); // Clear the timeout if (shutdownTimeout) { clearTimeout(shutdownTimeout); shutdownTimeout = null; } // Exit with error code process.exit(1); } } /** * Initialize the shutdown manager * This should be called early in the application lifecycle */ export function initializeShutdownManager(): void { console.log('🔧 Initializing shutdown manager...'); // Reset state in case of re-initialization shutdownInProgress = false; shutdownStartTime = null; activeJobs.clear(); shutdownCallbacks = []; // Reset callbacks too // Clear any existing timeout if (shutdownTimeout) { clearTimeout(shutdownTimeout); shutdownTimeout = null; } console.log('✅ Shutdown manager initialized'); } /** * Force immediate shutdown (for emergencies) */ export function forceShutdown(exitCode: number = 1): void { console.error('🚨 Force shutdown requested'); if (shutdownTimeout) { clearTimeout(shutdownTimeout); } process.exit(exitCode); }