feat(backend): wait for DB on start

This commit is contained in:
tigattack
2025-09-21 23:20:35 +01:00
parent c3365fedb2
commit 677d3b4df1
2 changed files with 64 additions and 40 deletions

View File

@@ -8,36 +8,36 @@ const { PrismaClient } = require('@prisma/client');
// Parse DATABASE_URL and add connection pooling parameters // Parse DATABASE_URL and add connection pooling parameters
function getOptimizedDatabaseUrl() { function getOptimizedDatabaseUrl() {
const originalUrl = process.env.DATABASE_URL; const originalUrl = process.env.DATABASE_URL;
if (!originalUrl) { if (!originalUrl) {
throw new Error('DATABASE_URL environment variable is required'); throw new Error('DATABASE_URL environment variable is required');
} }
// Parse the URL // Parse the URL
const url = new URL(originalUrl); const url = new URL(originalUrl);
// Add connection pooling parameters for multiple instances // Add connection pooling parameters for multiple instances
url.searchParams.set('connection_limit', '5'); // Reduced from default 10 url.searchParams.set('connection_limit', '5'); // Reduced from default 10
url.searchParams.set('pool_timeout', '10'); // 10 seconds url.searchParams.set('pool_timeout', '10'); // 10 seconds
url.searchParams.set('connect_timeout', '10'); // 10 seconds url.searchParams.set('connect_timeout', '10'); // 10 seconds
url.searchParams.set('idle_timeout', '300'); // 5 minutes url.searchParams.set('idle_timeout', '300'); // 5 minutes
url.searchParams.set('max_lifetime', '1800'); // 30 minutes url.searchParams.set('max_lifetime', '1800'); // 30 minutes
return url.toString(); return url.toString();
} }
// Create optimized Prisma client // Create optimized Prisma client
function createPrismaClient() { function createPrismaClient() {
const optimizedUrl = getOptimizedDatabaseUrl(); const optimizedUrl = getOptimizedDatabaseUrl();
return new PrismaClient({ return new PrismaClient({
datasources: { datasources: {
db: { db: {
url: optimizedUrl url: optimizedUrl
} }
}, },
log: process.env.NODE_ENV === 'development' log: process.env.NODE_ENV === 'development'
? ['query', 'info', 'warn', 'error'] ? ['query', 'info', 'warn', 'error']
: ['warn', 'error'], : ['warn', 'error'],
errorFormat: 'pretty' errorFormat: 'pretty'
}); });
@@ -54,6 +54,33 @@ async function checkDatabaseConnection(prisma) {
} }
} }
// Wait for database to be available with retry logic
async function waitForDatabase(prisma, options = {}) {
const maxAttempts = options.maxAttempts || parseInt(process.env.DB_MAX_ATTEMPTS) || 30;
const waitInterval = options.waitInterval || parseInt(process.env.DB_WAIT_INTERVAL) || 2;
console.log(`Waiting for database connection (max ${maxAttempts} attempts, ${waitInterval}s interval)...`);
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const isConnected = await checkDatabaseConnection(prisma);
if (isConnected) {
console.log(`Database connected successfully after ${attempt} attempt(s)`);
return true;
}
} catch (error) {
// checkDatabaseConnection already logs the error
}
if (attempt < maxAttempts) {
console.log(`⏳ Database not ready (attempt ${attempt}/${maxAttempts}), retrying in ${waitInterval}s...`);
await new Promise(resolve => setTimeout(resolve, waitInterval * 1000));
}
}
throw new Error(`❌ Database failed to become available after ${maxAttempts} attempts`);
}
// Graceful disconnect with retry // Graceful disconnect with retry
async function disconnectPrisma(prisma, maxRetries = 3) { async function disconnectPrisma(prisma, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) { for (let i = 0; i < maxRetries; i++) {
@@ -75,6 +102,7 @@ async function disconnectPrisma(prisma, maxRetries = 3) {
module.exports = { module.exports = {
createPrismaClient, createPrismaClient,
checkDatabaseConnection, checkDatabaseConnection,
waitForDatabase,
disconnectPrisma, disconnectPrisma,
getOptimizedDatabaseUrl getOptimizedDatabaseUrl
}; };

View File

@@ -3,7 +3,7 @@ const express = require('express');
const cors = require('cors'); const cors = require('cors');
const helmet = require('helmet'); const helmet = require('helmet');
const rateLimit = require('express-rate-limit'); const rateLimit = require('express-rate-limit');
const { createPrismaClient, checkDatabaseConnection, disconnectPrisma } = require('./config/database'); const { createPrismaClient, waitForDatabase, disconnectPrisma } = require('./config/database');
const winston = require('winston'); const winston = require('winston');
// Import routes // Import routes
@@ -27,24 +27,24 @@ const prisma = createPrismaClient();
function compareVersions(version1, version2) { function compareVersions(version1, version2) {
const v1Parts = version1.split('.').map(Number); const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number); const v2Parts = version2.split('.').map(Number);
// Ensure both arrays have the same length // Ensure both arrays have the same length
const maxLength = Math.max(v1Parts.length, v2Parts.length); const maxLength = Math.max(v1Parts.length, v2Parts.length);
while (v1Parts.length < maxLength) v1Parts.push(0); while (v1Parts.length < maxLength) v1Parts.push(0);
while (v2Parts.length < maxLength) v2Parts.push(0); while (v2Parts.length < maxLength) v2Parts.push(0);
for (let i = 0; i < maxLength; i++) { for (let i = 0; i < maxLength; i++) {
if (v1Parts[i] > v2Parts[i]) return true; if (v1Parts[i] > v2Parts[i]) return true;
if (v1Parts[i] < v2Parts[i]) return false; if (v1Parts[i] < v2Parts[i]) return false;
} }
return false; // versions are equal return false; // versions are equal
} }
// Function to check and import agent version on startup // Function to check and import agent version on startup
async function checkAndImportAgentVersion() { async function checkAndImportAgentVersion() {
console.log('🔍 Starting agent version auto-import check...'); console.log('🔍 Starting agent version auto-import check...');
// Skip if auto-import is disabled // Skip if auto-import is disabled
if (process.env.AUTO_IMPORT_AGENT_VERSION === 'false') { if (process.env.AUTO_IMPORT_AGENT_VERSION === 'false') {
console.log('❌ Auto-import of agent version is disabled'); console.log('❌ Auto-import of agent version is disabled');
@@ -53,16 +53,16 @@ async function checkAndImportAgentVersion() {
} }
return; return;
} }
try { try {
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
// Path to the agent script file // Path to the agent script file
const agentScriptPath = path.join(__dirname, '../../agents/patchmon-agent.sh'); const agentScriptPath = path.join(__dirname, '../../agents/patchmon-agent.sh');
console.log('📁 Agent script path:', agentScriptPath); console.log('📁 Agent script path:', agentScriptPath);
// Check if file exists // Check if file exists
if (!fs.existsSync(agentScriptPath)) { if (!fs.existsSync(agentScriptPath)) {
console.log('❌ Agent script file not found, skipping version check'); console.log('❌ Agent script file not found, skipping version check');
@@ -72,10 +72,10 @@ async function checkAndImportAgentVersion() {
return; return;
} }
console.log('✅ Agent script file found'); console.log('✅ Agent script file found');
// Read the file content // Read the file content
const scriptContent = fs.readFileSync(agentScriptPath, 'utf8'); const scriptContent = fs.readFileSync(agentScriptPath, 'utf8');
// Extract version from script content // Extract version from script content
const versionMatch = scriptContent.match(/AGENT_VERSION="([^"]+)"/); const versionMatch = scriptContent.match(/AGENT_VERSION="([^"]+)"/);
if (!versionMatch) { if (!versionMatch) {
@@ -85,15 +85,15 @@ async function checkAndImportAgentVersion() {
} }
return; return;
} }
const localVersion = versionMatch[1]; const localVersion = versionMatch[1];
console.log('📋 Local version:', localVersion); console.log('📋 Local version:', localVersion);
// Check if this version already exists in database // Check if this version already exists in database
const existingVersion = await prisma.agent_versions.findUnique({ const existingVersion = await prisma.agent_versions.findUnique({
where: { version: localVersion } where: { version: localVersion }
}); });
if (existingVersion) { if (existingVersion) {
console.log(`✅ Agent version ${localVersion} already exists in database`); console.log(`✅ Agent version ${localVersion} already exists in database`);
if (process.env.ENABLE_LOGGING === 'true') { if (process.env.ENABLE_LOGGING === 'true') {
@@ -102,21 +102,21 @@ async function checkAndImportAgentVersion() {
return; return;
} }
console.log(`🆕 Agent version ${localVersion} not found in database`); console.log(`🆕 Agent version ${localVersion} not found in database`);
// Check if there are any existing versions to compare with // Check if there are any existing versions to compare with
const allVersions = await prisma.agent_versions.findMany({ const allVersions = await prisma.agent_versions.findMany({
select: { version: true }, select: { version: true },
orderBy: { created_at: 'desc' } orderBy: { created_at: 'desc' }
}); });
if (allVersions.length > 0) { if (allVersions.length > 0) {
console.log(`📊 Found ${allVersions.length} existing versions in database`); console.log(`📊 Found ${allVersions.length} existing versions in database`);
console.log(`📊 Latest version: ${allVersions[0].version}`); console.log(`📊 Latest version: ${allVersions[0].version}`);
// Simple version comparison (assuming semantic versioning) // Simple version comparison (assuming semantic versioning)
const isNewer = compareVersions(localVersion, allVersions[0].version); const isNewer = compareVersions(localVersion, allVersions[0].version);
console.log(`🔄 Version comparison: ${localVersion} > ${allVersions[0].version} = ${isNewer}`); console.log(`🔄 Version comparison: ${localVersion} > ${allVersions[0].version} = ${isNewer}`);
if (!isNewer) { if (!isNewer) {
console.log(`❌ Agent version ${localVersion} is not newer than existing versions, skipping import`); console.log(`❌ Agent version ${localVersion} is not newer than existing versions, skipping import`);
if (process.env.ENABLE_LOGGING === 'true') { if (process.env.ENABLE_LOGGING === 'true') {
@@ -125,7 +125,7 @@ async function checkAndImportAgentVersion() {
return; return;
} }
} }
// Version doesn't exist, create it // Version doesn't exist, create it
const agentVersion = await prisma.agent_versions.create({ const agentVersion = await prisma.agent_versions.create({
data: { data: {
@@ -150,12 +150,12 @@ async function checkAndImportAgentVersion() {
updated_at: new Date() updated_at: new Date()
} }
}); });
console.log(`🎉 Successfully auto-imported new agent version ${localVersion} on startup`); console.log(`🎉 Successfully auto-imported new agent version ${localVersion} on startup`);
if (process.env.ENABLE_LOGGING === 'true') { if (process.env.ENABLE_LOGGING === 'true') {
logger.info(`✅ Auto-imported new agent version ${localVersion} on startup`); logger.info(`✅ Auto-imported new agent version ${localVersion} on startup`);
} }
} catch (error) { } catch (error) {
console.error('❌ Failed to check/import agent version on startup:', error.message); console.error('❌ Failed to check/import agent version on startup:', error.message);
if (process.env.ENABLE_LOGGING === 'true') { if (process.env.ENABLE_LOGGING === 'true') {
@@ -308,9 +308,9 @@ app.use((err, req, res, next) => {
if (process.env.ENABLE_LOGGING === 'true') { if (process.env.ENABLE_LOGGING === 'true') {
logger.error(err.stack); logger.error(err.stack);
} }
res.status(500).json({ res.status(500).json({
error: 'Something went wrong!', error: 'Something went wrong!',
message: process.env.NODE_ENV === 'development' ? err.message : undefined message: process.env.NODE_ENV === 'development' ? err.message : undefined
}); });
}); });
@@ -341,26 +341,22 @@ process.on('SIGTERM', async () => {
// Start server with database health check // Start server with database health check
async function startServer() { async function startServer() {
try { try {
// Check database connection before starting server // Wait for database to be available
const isConnected = await checkDatabaseConnection(prisma); await waitForDatabase(prisma);
if (!isConnected) {
console.error('❌ Database connection failed. Server not started.');
process.exit(1);
}
if (process.env.ENABLE_LOGGING === 'true') { if (process.env.ENABLE_LOGGING === 'true') {
logger.info('✅ Database connection successful'); logger.info('✅ Database connection successful');
} }
// Check and import agent version on startup // Check and import agent version on startup
await checkAndImportAgentVersion(); await checkAndImportAgentVersion();
app.listen(PORT, () => { app.listen(PORT, () => {
if (process.env.ENABLE_LOGGING === 'true') { if (process.env.ENABLE_LOGGING === 'true') {
logger.info(`Server running on port ${PORT}`); logger.info(`Server running on port ${PORT}`);
logger.info(`Environment: ${process.env.NODE_ENV}`); logger.info(`Environment: ${process.env.NODE_ENV}`);
} }
// Start update scheduler // Start update scheduler
updateScheduler.start(); updateScheduler.start();
}); });
@@ -372,4 +368,4 @@ async function startServer() {
startServer(); startServer();
module.exports = app; module.exports = app;