ruv-swarm
Version:
High-performance neural network swarm orchestration in WebAssembly
587 lines (484 loc) • 17 kB
JavaScript
/**
* High-Availability SQLite Connection Pool for ruv-swarm
*
* This implementation addresses the production readiness concerns:
* - Connection exhaustion prevention
* - Deadlock avoidance under load
* - System availability during high concurrency
* - Proper resource lifecycle management
*
* Design decisions:
* - Single primary connection for writes (SQLite single-writer limitation)
* - Multiple reader connections in WAL mode for concurrent reads
* - Worker thread pool for CPU-intensive queries
* - Connection health monitoring and auto-recovery
* - Graceful degradation under pressure
*/
import Database from 'better-sqlite3';
import { Worker } from 'worker_threads';
import { EventEmitter } from 'events';
import path from 'path';
import fs from 'fs';
import os from 'os';
class SQLiteConnectionPool extends EventEmitter {
constructor(dbPath, options = {}) {
super();
this.dbPath = dbPath;
this.options = {
// Pool configuration
maxReaders: options.maxReaders || Math.max(4, os.cpus().length),
maxWorkers: options.maxWorkers || Math.max(2, Math.floor(os.cpus().length / 2)),
// Connection timeouts
acquireTimeout: options.acquireTimeout || 30000, // 30 seconds
idleTimeout: options.idleTimeout || 300000, // 5 minutes
// Health monitoring
healthCheckInterval: options.healthCheckInterval || 60000, // 1 minute
maxRetries: options.maxRetries || 3,
// Performance settings
mmapSize: options.mmapSize || 268435456, // 256MB
cacheSize: options.cacheSize || -64000, // 64MB
// High availability
enableBackup: options.enableBackup || false,
backupInterval: options.backupInterval || 3600000, // 1 hour
...options
};
// Connection pools
this.writeConnection = null;
this.readerConnections = [];
this.availableReaders = [];
this.busyReaders = new Set();
// Worker thread pool
this.workers = [];
this.availableWorkers = [];
this.busyWorkers = new Set();
// Request queues
this.readQueue = [];
this.writeQueue = [];
this.workerQueue = [];
// Health monitoring
this.isHealthy = true;
this.healthCheckTimer = null;
this.lastHealthCheck = Date.now();
// Statistics
this.stats = {
totalReads: 0,
totalWrites: 0,
totalWorkerTasks: 0,
failedConnections: 0,
averageReadTime: 0,
averageWriteTime: 0,
activeConnections: 0
};
// Prepared statements cache
this.preparedStatements = new Map();
this.initialize();
}
async initialize() {
try {
// Ensure database directory exists
const dataDir = path.dirname(this.dbPath);
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
// Initialize write connection
await this.initializeWriteConnection();
// Initialize reader connections
await this.initializeReaderConnections();
// Initialize worker threads
await this.initializeWorkerThreads();
// Start health monitoring
this.startHealthMonitoring();
// Setup cleanup handlers
this.setupCleanupHandlers();
this.emit('ready');
} catch (error) {
this.emit('error', error);
throw error;
}
}
async initializeWriteConnection() {
try {
this.writeConnection = new Database(this.dbPath);
this.configureConnection(this.writeConnection);
// Test connection
this.writeConnection.prepare('SELECT 1').get();
this.emit('write-connection-ready');
} catch (error) {
this.stats.failedConnections++;
throw new Error(`Failed to initialize write connection: ${error.message}`);
}
}
async initializeReaderConnections() {
for (let i = 0; i < this.options.maxReaders; i++) {
try {
const reader = new Database(this.dbPath, { readonly: true });
this.configureReadOnlyConnection(reader);
// Test connection
reader.prepare('SELECT 1').get();
this.readerConnections.push(reader);
this.availableReaders.push(reader);
this.emit('reader-connection-ready', i);
} catch (error) {
this.stats.failedConnections++;
console.error(`Failed to initialize reader connection ${i}:`, error);
// Continue with fewer readers rather than failing completely
}
}
if (this.availableReaders.length === 0) {
throw new Error('Failed to initialize any reader connections');
}
}
async initializeWorkerThreads() {
const workerScript = path.join(path.dirname(new URL(import.meta.url).pathname), 'sqlite-worker.js');
for (let i = 0; i < this.options.maxWorkers; i++) {
try {
const worker = new Worker(workerScript, {
workerData: {
dbPath: this.dbPath,
options: this.options
}
});
worker.on('error', (error) => {
this.emit('worker-error', error);
this.handleWorkerError(worker, error);
});
worker.on('exit', (code) => {
if (code !== 0) {
this.emit('worker-exit', code);
this.handleWorkerExit(worker, code);
}
});
this.workers.push(worker);
this.availableWorkers.push(worker);
this.emit('worker-ready', i);
} catch (error) {
console.error(`Failed to initialize worker ${i}:`, error);
// Continue with fewer workers rather than failing completely
}
}
}
configureConnection(db) {
// Essential SQLite optimizations for high availability
db.pragma('journal_mode = WAL'); // Enable WAL mode for concurrent reads
db.pragma('synchronous = NORMAL'); // Balance safety and performance
db.pragma('temp_store = MEMORY'); // Use memory for temp tables
db.pragma('mmap_size = ' + this.options.mmapSize);
db.pragma('cache_size = ' + this.options.cacheSize);
db.pragma('foreign_keys = ON'); // Enable foreign key constraints
db.pragma('busy_timeout = 5000'); // 5 second timeout for busy database
// Optimize query planner
db.pragma('optimize');
}
configureReadOnlyConnection(db) {
// Configuration for readonly connections - limited pragma statements
try {
// These are safe for readonly connections
db.pragma('temp_store = MEMORY'); // Use memory for temp tables
db.pragma('cache_size = ' + this.options.cacheSize);
db.pragma('busy_timeout = 5000'); // 5 second timeout for busy database
} catch (error) {
// Some pragma statements might fail on readonly connections
// This is expected behavior, continue without them
console.debug('Some pragma statements skipped for readonly connection:', error.message);
}
}
// Read operation with connection pooling
async read(sql, params = []) {
const startTime = Date.now();
try {
const connection = await this.acquireReaderConnection();
const stmt = this.getPreparedStatement(connection, sql);
const result = stmt.all(params);
this.releaseReaderConnection(connection);
// Update statistics
this.stats.totalReads++;
this.updateAverageTime('read', Date.now() - startTime);
return result;
} catch (error) {
this.emit('read-error', error);
throw error;
}
}
// Write operation with queuing
async write(sql, params = []) {
return new Promise((resolve, reject) => {
this.writeQueue.push({
sql,
params,
resolve,
reject,
timestamp: Date.now()
});
this.processWriteQueue();
});
}
// Transaction support
async transaction(fn) {
const startTime = Date.now();
try {
const result = this.writeConnection.transaction(fn)();
this.stats.totalWrites++;
this.updateAverageTime('write', Date.now() - startTime);
return result;
} catch (error) {
this.emit('transaction-error', error);
throw error;
}
}
// CPU-intensive query execution in worker thread
async executeInWorker(sql, params = []) {
return new Promise((resolve, reject) => {
this.workerQueue.push({
sql,
params,
resolve,
reject,
timestamp: Date.now()
});
this.processWorkerQueue();
});
}
async acquireReaderConnection() {
return new Promise((resolve, reject) => {
if (this.availableReaders.length > 0) {
const connection = this.availableReaders.pop();
this.busyReaders.add(connection);
resolve(connection);
} else {
// Queue the request
this.readQueue.push({ resolve, reject, timestamp: Date.now() });
// Timeout handling
setTimeout(() => {
const index = this.readQueue.findIndex(item => item.resolve === resolve);
if (index !== -1) {
this.readQueue.splice(index, 1);
reject(new Error('Reader connection acquire timeout'));
}
}, this.options.acquireTimeout);
}
});
}
releaseReaderConnection(connection) {
if (this.busyReaders.has(connection)) {
this.busyReaders.delete(connection);
// Process queued read requests
if (this.readQueue.length > 0) {
const { resolve } = this.readQueue.shift();
this.busyReaders.add(connection);
resolve(connection);
} else {
this.availableReaders.push(connection);
}
}
}
async processWriteQueue() {
if (this.writeQueue.length === 0) return;
const { sql, params, resolve, reject, timestamp } = this.writeQueue.shift();
const startTime = Date.now();
try {
// Check for timeout
if (Date.now() - timestamp > this.options.acquireTimeout) {
reject(new Error('Write operation timeout'));
return;
}
const stmt = this.getPreparedStatement(this.writeConnection, sql);
const result = stmt.run(params);
this.stats.totalWrites++;
this.updateAverageTime('write', Date.now() - startTime);
resolve(result);
} catch (error) {
this.emit('write-error', error);
reject(error);
}
// Process next item in queue
if (this.writeQueue.length > 0) {
setImmediate(() => this.processWriteQueue());
}
}
async processWorkerQueue() {
if (this.workerQueue.length === 0 || this.availableWorkers.length === 0) return;
const worker = this.availableWorkers.pop();
const { sql, params, resolve, reject, timestamp } = this.workerQueue.shift();
try {
// Check for timeout
if (Date.now() - timestamp > this.options.acquireTimeout) {
reject(new Error('Worker task timeout'));
this.availableWorkers.push(worker);
return;
}
this.busyWorkers.add(worker);
const messageHandler = (result) => {
this.busyWorkers.delete(worker);
this.availableWorkers.push(worker);
this.stats.totalWorkerTasks++;
if (result.error) {
reject(new Error(result.error));
} else {
resolve(result.data);
}
// Process next item in queue
if (this.workerQueue.length > 0) {
setImmediate(() => this.processWorkerQueue());
}
};
worker.once('message', messageHandler);
worker.postMessage({ sql, params });
} catch (error) {
this.busyWorkers.delete(worker);
this.availableWorkers.push(worker);
reject(error);
}
}
getPreparedStatement(connection, sql) {
const key = `${connection._id || 'main'}_${sql}`;
if (!this.preparedStatements.has(key)) {
this.preparedStatements.set(key, connection.prepare(sql));
}
return this.preparedStatements.get(key);
}
startHealthMonitoring() {
this.healthCheckTimer = setInterval(() => {
this.performHealthCheck();
}, this.options.healthCheckInterval);
}
async performHealthCheck() {
const startTime = Date.now();
try {
// Check write connection
this.writeConnection.prepare('SELECT 1').get();
// Check reader connections
for (const reader of this.readerConnections) {
reader.prepare('SELECT 1').get();
}
// Check worker threads
const workerHealthPromises = this.workers.map(worker => {
return new Promise((resolve) => {
const timeout = setTimeout(() => resolve(false), 1000);
worker.once('message', (result) => {
clearTimeout(timeout);
resolve(result.health === 'ok');
});
worker.postMessage({ health: true });
});
});
const workerHealthResults = await Promise.all(workerHealthPromises);
const healthyWorkers = workerHealthResults.filter(Boolean).length;
this.isHealthy = healthyWorkers >= Math.floor(this.options.maxWorkers / 2);
this.lastHealthCheck = Date.now();
this.emit('health-check', {
healthy: this.isHealthy,
duration: Date.now() - startTime,
workers: healthyWorkers,
readers: this.availableReaders.length,
stats: this.getStats()
});
} catch (error) {
this.isHealthy = false;
this.emit('health-check-error', error);
}
}
handleWorkerError(worker, error) {
// Remove failed worker from pools
this.busyWorkers.delete(worker);
const index = this.availableWorkers.indexOf(worker);
if (index !== -1) {
this.availableWorkers.splice(index, 1);
}
// Attempt to create replacement worker
this.createReplacementWorker();
}
handleWorkerExit(worker, code) {
// Remove exited worker from pools
this.busyWorkers.delete(worker);
const index = this.availableWorkers.indexOf(worker);
if (index !== -1) {
this.availableWorkers.splice(index, 1);
}
// Attempt to create replacement worker
this.createReplacementWorker();
}
async createReplacementWorker() {
try {
const workerScript = path.join(path.dirname(new URL(import.meta.url).pathname), 'sqlite-worker.js');
const worker = new Worker(workerScript, {
workerData: {
dbPath: this.dbPath,
options: this.options
}
});
worker.on('error', (error) => {
this.emit('worker-error', error);
this.handleWorkerError(worker, error);
});
worker.on('exit', (code) => {
if (code !== 0) {
this.emit('worker-exit', code);
this.handleWorkerExit(worker, code);
}
});
this.workers.push(worker);
this.availableWorkers.push(worker);
this.emit('worker-replaced');
} catch (error) {
this.emit('worker-replacement-failed', error);
}
}
updateAverageTime(type, time) {
const statKey = `average${type.charAt(0).toUpperCase() + type.slice(1)}Time`;
const countKey = `total${type.charAt(0).toUpperCase() + type.slice(1)}s`;
if (this.stats[statKey] === 0) {
this.stats[statKey] = time;
} else {
this.stats[statKey] = (this.stats[statKey] + time) / 2;
}
}
getStats() {
return {
...this.stats,
activeConnections: this.busyReaders.size + (this.writeConnection ? 1 : 0),
availableReaders: this.availableReaders.length,
availableWorkers: this.availableWorkers.length,
readQueueLength: this.readQueue.length,
writeQueueLength: this.writeQueue.length,
workerQueueLength: this.workerQueue.length,
isHealthy: this.isHealthy,
lastHealthCheck: this.lastHealthCheck
};
}
setupCleanupHandlers() {
const cleanup = () => {
this.close();
};
process.on('exit', cleanup);
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('uncaughtException', cleanup);
}
async close() {
try {
// Stop health monitoring
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
}
// Close all connections
if (this.writeConnection) {
this.writeConnection.close();
}
for (const reader of this.readerConnections) {
reader.close();
}
// Terminate worker threads
for (const worker of this.workers) {
await worker.terminate();
}
// Clear all queues
this.readQueue.length = 0;
this.writeQueue.length = 0;
this.workerQueue.length = 0;
this.emit('closed');
} catch (error) {
this.emit('close-error', error);
}
}
}
export { SQLiteConnectionPool };