CampVerse CI
Deploy backend from GitHub Actions
89a74d9
Raw History Blame Contribute Delete
8.5 kB
/**
* Request Timeout Middleware
* Handles request timeouts and prevents hanging requests
*/
const { logger } = require('./errorHandler');
/**
* Create timeout middleware
* @param {number} timeout - Timeout in milliseconds
* @param {string} message - Custom timeout message
*/
function createTimeoutMiddleware(timeout = 30000, message = 'Request timeout') {
return (req, res, next) => {
// Set timeout for the request
const timeoutId = setTimeout(() => {
if (!res.headersSent) {
logger.warn(`Request timeout: ${req.method} ${req.url}`, {
correlationId: req.correlationId,
userAgent: req.get('User-Agent'),
ip: req.ip,
timeout
});
res.status(408).json({
success: false,
error: message,
timeout,
correlationId: req.correlationId,
timestamp: new Date().toISOString()
});
}
}, timeout);
// Clear timeout when response is finished
res.on('finish', () => {
clearTimeout(timeoutId);
});
// Clear timeout when response is closed
res.on('close', () => {
clearTimeout(timeoutId);
});
// Store timeout ID for potential manual clearing
req.timeoutId = timeoutId;
next();
};
}
/**
* Different timeout configurations for different route types
*/
const timeoutConfigs = {
// Quick operations (auth, simple queries)
fast: createTimeoutMiddleware(5000, 'Request timeout - operation took too long'),
// Standard operations (CRUD, most API calls)
standard: createTimeoutMiddleware(15000, 'Request timeout - please try again'),
// Slow operations (file uploads, complex queries)
slow: createTimeoutMiddleware(30000, 'Request timeout - operation is taking longer than expected'),
// Very slow operations (bulk operations, reports)
bulk: createTimeoutMiddleware(60000, 'Request timeout - bulk operation exceeded time limit'),
// File upload operations
upload: createTimeoutMiddleware(120000, 'Upload timeout - file upload took too long'),
};
/**
* Smart timeout middleware that adjusts based on request type
*/
function smartTimeout(req, res, next) {
let timeout = 30000; // default
let message = 'Request timeout';
// Determine timeout based on request characteristics
if (req.files || req.is('multipart/form-data')) {
timeout = 120000; // 2 minutes for file uploads
message = 'Upload timeout';
} else if (req.url.includes('/bulk') || req.url.includes('/export')) {
timeout = 60000; // 1 minute for bulk operations
message = 'Bulk operation timeout';
} else if (req.url.includes('/analytics') || req.url.includes('/dashboard')) {
timeout = 30000; // 30 seconds for analytics
message = 'Analytics timeout';
} else if (req.method === 'GET' && req.url.includes('/search')) {
timeout = 10000; // 10 seconds for search
message = 'Search timeout';
} else if (req.method === 'POST' && (req.url.includes('/login') || req.url.includes('/register'))) {
timeout = 180000; // 3 minutes for auth (account for slow network/server)
message = 'Authentication timeout';
} else if (req.method === 'POST' && (req.url.includes('/forgot-password') || req.url.includes('/reset-password'))) {
timeout = 180000; // 3 minutes for password reset emails (email sending can be slow)
message = 'Password reset timeout';
}
// Apply the determined timeout
return createTimeoutMiddleware(timeout, message)(req, res, next);
}
/**
* Circuit breaker pattern for external service calls
*/
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.resetTimeout = options.resetTimeout || 60000; // 1 minute
this.monitoringPeriod = options.monitoringPeriod || 10000; // 10 seconds
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.failureCount = 0;
this.lastFailureTime = null;
this.successCount = 0;
}
async execute(operation, fallback = null) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.resetTimeout) {
this.state = 'HALF_OPEN';
this.successCount = 0;
} else {
logger.warn('Circuit breaker is OPEN, using fallback');
return fallback ? await fallback() : Promise.reject(new Error('Circuit breaker is OPEN'));
}
}
try {
const result = await operation();
if (this.state === 'HALF_OPEN') {
this.successCount++;
if (this.successCount >= 3) {
this.state = 'CLOSED';
this.failureCount = 0;
logger.info('Circuit breaker reset to CLOSED');
}
}
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
logger.error(`Circuit breaker opened after ${this.failureCount} failures`);
}
if (fallback) {
logger.warn('Operation failed, using fallback');
return await fallback();
}
throw error;
}
}
getState() {
return {
state: this.state,
failureCount: this.failureCount,
lastFailureTime: this.lastFailureTime,
successCount: this.successCount
};
}
reset() {
this.state = 'CLOSED';
this.failureCount = 0;
this.lastFailureTime = null;
this.successCount = 0;
logger.info('Circuit breaker manually reset');
}
}
/**
* Retry mechanism with exponential backoff
*/
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
logger.error(`Operation failed after ${maxRetries} attempts:`, error);
throw error;
}
const delay = baseDelay * Math.pow(2, attempt - 1);
logger.warn(`Operation failed (attempt ${attempt}/${maxRetries}), retrying in ${delay}ms:`, error.message);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
/**
* Request queue for managing concurrent requests
*/
class RequestQueue {
constructor(maxConcurrent = 100) {
this.maxConcurrent = maxConcurrent;
this.running = 0;
this.queue = [];
}
async add(operation) {
return new Promise((resolve, reject) => {
this.queue.push({
operation,
resolve,
reject,
timestamp: Date.now()
});
this.process();
});
}
async process() {
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
return;
}
this.running++;
const { operation, resolve, reject } = this.queue.shift();
try {
const result = await operation();
resolve(result);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process(); // Process next item in queue
}
}
getStats() {
return {
running: this.running,
queued: this.queue.length,
maxConcurrent: this.maxConcurrent
};
}
clear() {
this.queue.forEach(({ reject }) => {
reject(new Error('Queue cleared'));
});
this.queue = [];
}
}
// Create global instances
const mlApiCircuitBreaker = new CircuitBreaker({
failureThreshold: 3,
resetTimeout: 30000,
monitoringPeriod: 5000
});
const emailCircuitBreaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 60000,
monitoringPeriod: 10000
});
const requestQueue = new RequestQueue(
parseInt(process.env.REQUEST_QUEUE_CONCURRENCY || '10', 10)
);
/**
* Middleware to add request to queue for rate limiting
*/
function queueMiddleware(req, res, next) {
// Skip queueing for health checks and static files
if (req.url === '/health' || req.url.startsWith('/api-docs')) {
return next();
}
requestQueue.add(async () => {
return new Promise((resolve) => {
res.on('finish', resolve);
res.on('close', resolve);
next();
});
}).catch(next);
}
module.exports = {
// Timeout middlewares
createTimeoutMiddleware,
smartTimeout,
timeoutConfigs,
// Circuit breakers
CircuitBreaker,
mlApiCircuitBreaker,
emailCircuitBreaker,
// Retry mechanism
retryWithBackoff,
// Request queue
RequestQueue,
requestQueue,
queueMiddleware
};