• Created QueueProcessor base class for shared queue management, retry logic, and persistence • Refactored BatchProcessor to extend QueueProcessor (385→142 lines, 63% reduction) • Created BatchController with comprehensive API endpoints for batch operations • Added Digital Ocean templates integration with caching • Integrated batch endpoints into ManualServer with proper routing • Fixed infinite recursion bug in queue status calculations • Eliminated ~400 lines of duplicate code across processors • Maintained backward compatibility with existing test interfaces Architecture benefits: - Single source of truth for queue processing logic - Simplified maintenance and bug fixes - Clear separation between AutoProcessor (production) and BatchProcessor (R&D) - Extensible design for future processor types 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
622 lines
16 KiB
JavaScript
622 lines
16 KiB
JavaScript
// ========================================
|
|
// BATCH PROCESSOR - SYSTÈME DE QUEUE
|
|
// Responsabilité: Traitement batch des lignes Google Sheets avec pipeline modulaire
|
|
// ========================================
|
|
|
|
const { logSh } = require('../ErrorReporting');
|
|
const { tracer } = require('../trace');
|
|
const { handleModularWorkflow } = require('../Main');
|
|
const { readInstructionsData } = require('../BrainConfig');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
/**
|
|
* BATCH PROCESSOR
|
|
* Système de queue pour traiter les lignes Google Sheets une par une
|
|
*/
|
|
class BatchProcessor {
|
|
|
|
constructor() {
|
|
this.statusPath = path.join(__dirname, '../../config/batch-status.json');
|
|
this.configPath = path.join(__dirname, '../../config/batch-config.json');
|
|
this.queuePath = path.join(__dirname, '../../config/batch-queue.json');
|
|
|
|
// État du processeur
|
|
this.isRunning = false;
|
|
this.isPaused = false;
|
|
this.currentRow = null;
|
|
this.queue = [];
|
|
this.errors = [];
|
|
this.results = [];
|
|
|
|
// Configuration par défaut
|
|
this.config = {
|
|
selective: 'standardEnhancement',
|
|
adversarial: 'light',
|
|
humanSimulation: 'none',
|
|
patternBreaking: 'none',
|
|
intensity: 1.0,
|
|
rowRange: { start: 2, end: 10 },
|
|
saveIntermediateSteps: false
|
|
};
|
|
|
|
// Métriques
|
|
this.startTime = null;
|
|
this.processedCount = 0;
|
|
this.errorCount = 0;
|
|
|
|
// Callbacks pour updates
|
|
this.onStatusUpdate = null;
|
|
this.onProgress = null;
|
|
this.onError = null;
|
|
this.onComplete = null;
|
|
|
|
this.initializeProcessor();
|
|
}
|
|
|
|
/**
|
|
* Initialise le processeur
|
|
*/
|
|
async initializeProcessor() {
|
|
try {
|
|
// Charger la configuration
|
|
await this.loadConfig();
|
|
|
|
// Initialiser la queue si elle n'existe pas
|
|
await this.initializeQueue();
|
|
|
|
logSh('🎯 BatchProcessor initialisé', 'DEBUG');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur initialisation BatchProcessor: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialise les fichiers de configuration (alias pour compatibilité tests)
|
|
*/
|
|
async initializeFiles() {
|
|
try {
|
|
// Créer le dossier config s'il n'existe pas
|
|
const configDir = path.dirname(this.configPath);
|
|
await fs.mkdir(configDir, { recursive: true });
|
|
|
|
// Créer config par défaut si inexistant
|
|
try {
|
|
await fs.access(this.configPath);
|
|
} catch {
|
|
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
|
|
logSh('📝 Configuration batch par défaut créée', 'DEBUG');
|
|
}
|
|
|
|
// Créer status par défaut si inexistant
|
|
const defaultStatus = {
|
|
status: 'idle',
|
|
currentRow: null,
|
|
totalRows: 0,
|
|
progress: 0,
|
|
startTime: null,
|
|
estimatedEnd: null,
|
|
errors: [],
|
|
lastResult: null,
|
|
config: this.config
|
|
};
|
|
|
|
try {
|
|
await fs.access(this.statusPath);
|
|
} catch {
|
|
await fs.writeFile(this.statusPath, JSON.stringify(defaultStatus, null, 2));
|
|
logSh('📊 Status batch par défaut créé', 'DEBUG');
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur initialisation fichiers batch: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Charge la configuration
|
|
*/
|
|
async loadConfig() {
|
|
try {
|
|
const configData = await fs.readFile(this.configPath, 'utf8');
|
|
this.config = JSON.parse(configData);
|
|
logSh(`📋 Configuration chargée: ${JSON.stringify(this.config)}`, 'DEBUG');
|
|
} catch (error) {
|
|
logSh('⚠️ Configuration non trouvée, utilisation des valeurs par défaut', 'WARNING');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialise la queue
|
|
*/
|
|
async initializeQueue() {
|
|
try {
|
|
// Essayer de charger la queue existante
|
|
try {
|
|
const queueData = await fs.readFile(this.queuePath, 'utf8');
|
|
const savedQueue = JSON.parse(queueData);
|
|
|
|
if (savedQueue.queue && Array.isArray(savedQueue.queue)) {
|
|
this.queue = savedQueue.queue;
|
|
this.processedCount = savedQueue.processedCount || 0;
|
|
logSh(`📊 Queue restaurée: ${this.queue.length} éléments`, 'DEBUG');
|
|
}
|
|
} catch {
|
|
// Queue n'existe pas, on la créera
|
|
}
|
|
|
|
// Si queue vide, la populer depuis la configuration
|
|
if (this.queue.length === 0) {
|
|
await this.populateQueue();
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur initialisation queue: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Popule la queue avec les lignes à traiter
|
|
*/
|
|
async populateQueue() {
|
|
try {
|
|
this.queue = [];
|
|
|
|
const { start, end } = this.config.rowRange;
|
|
|
|
for (let rowNumber = start; rowNumber <= end; rowNumber++) {
|
|
this.queue.push({
|
|
rowNumber,
|
|
status: 'pending',
|
|
attempts: 0,
|
|
maxAttempts: 3,
|
|
error: null,
|
|
result: null,
|
|
startTime: null,
|
|
endTime: null
|
|
});
|
|
}
|
|
|
|
await this.saveQueue();
|
|
|
|
logSh(`📋 Queue populée: ${this.queue.length} lignes (${start} à ${end})`, 'INFO');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur population queue: ${error.message}`, 'ERROR');
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sauvegarde la queue
|
|
*/
|
|
async saveQueue() {
|
|
try {
|
|
const queueData = {
|
|
queue: this.queue,
|
|
processedCount: this.processedCount,
|
|
lastUpdate: new Date().toISOString()
|
|
};
|
|
|
|
await fs.writeFile(this.queuePath, JSON.stringify(queueData, null, 2));
|
|
} catch (error) {
|
|
logSh(`❌ Erreur sauvegarde queue: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// CONTRÔLES PRINCIPAUX
|
|
// ========================================
|
|
|
|
/**
|
|
* Démarre le traitement batch
|
|
*/
|
|
async start() {
|
|
return tracer.run('BatchProcessor.start', async () => {
|
|
if (this.isRunning) {
|
|
throw new Error('Le traitement est déjà en cours');
|
|
}
|
|
|
|
logSh('🚀 Démarrage traitement batch', 'INFO');
|
|
|
|
this.isRunning = true;
|
|
this.isPaused = false;
|
|
this.startTime = new Date();
|
|
this.processedCount = 0;
|
|
this.errorCount = 0;
|
|
|
|
// Charger la configuration la plus récente
|
|
await this.loadConfig();
|
|
|
|
// Si queue vide ou configuration changée, repopuler
|
|
if (this.queue.length === 0) {
|
|
await this.populateQueue();
|
|
}
|
|
|
|
// Mettre à jour le status
|
|
await this.updateStatus();
|
|
|
|
// Démarrer le traitement asynchrone
|
|
this.processQueue().catch(error => {
|
|
logSh(`❌ Erreur traitement queue: ${error.message}`, 'ERROR');
|
|
this.handleError(error);
|
|
});
|
|
|
|
return this.getStatus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Arrête le traitement batch
|
|
*/
|
|
async stop() {
|
|
return tracer.run('BatchProcessor.stop', async () => {
|
|
logSh('🛑 Arrêt traitement batch', 'INFO');
|
|
|
|
this.isRunning = false;
|
|
this.isPaused = false;
|
|
this.currentRow = null;
|
|
|
|
await this.updateStatus();
|
|
|
|
return this.getStatus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Met en pause le traitement
|
|
*/
|
|
async pause() {
|
|
return tracer.run('BatchProcessor.pause', async () => {
|
|
if (!this.isRunning) {
|
|
throw new Error('Aucun traitement en cours');
|
|
}
|
|
|
|
logSh('⏸️ Mise en pause traitement batch', 'INFO');
|
|
|
|
this.isPaused = true;
|
|
|
|
await this.updateStatus();
|
|
|
|
return this.getStatus();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reprend le traitement
|
|
*/
|
|
async resume() {
|
|
return tracer.run('BatchProcessor.resume', async () => {
|
|
if (!this.isRunning || !this.isPaused) {
|
|
throw new Error('Aucun traitement en pause');
|
|
}
|
|
|
|
logSh('▶️ Reprise traitement batch', 'INFO');
|
|
|
|
this.isPaused = false;
|
|
|
|
await this.updateStatus();
|
|
|
|
// Reprendre le traitement
|
|
this.processQueue().catch(error => {
|
|
logSh(`❌ Erreur reprise traitement: ${error.message}`, 'ERROR');
|
|
this.handleError(error);
|
|
});
|
|
|
|
return this.getStatus();
|
|
});
|
|
}
|
|
|
|
// ========================================
|
|
// TRAITEMENT QUEUE
|
|
// ========================================
|
|
|
|
/**
|
|
* Traite la queue élément par élément
|
|
*/
|
|
async processQueue() {
|
|
return tracer.run('BatchProcessor.processQueue', async () => {
|
|
while (this.isRunning && !this.isPaused) {
|
|
// Chercher le prochain élément à traiter
|
|
const nextItem = this.queue.find(item => item.status === 'pending' ||
|
|
(item.status === 'error' && item.attempts < item.maxAttempts));
|
|
|
|
if (!nextItem) {
|
|
// Queue terminée
|
|
logSh('✅ Traitement queue terminé', 'INFO');
|
|
await this.complete();
|
|
break;
|
|
}
|
|
|
|
// Traiter l'élément
|
|
await this.processItem(nextItem);
|
|
|
|
// Pause entre les éléments (pour éviter rate limiting)
|
|
await this.sleep(1000);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Traite un élément de la queue
|
|
*/
|
|
async processItem(item) {
|
|
return tracer.run('BatchProcessor.processItem', async () => {
|
|
logSh(`🔄 Traitement ligne ${item.rowNumber} (tentative ${item.attempts + 1}/${item.maxAttempts})`, 'INFO');
|
|
|
|
this.currentRow = item.rowNumber;
|
|
item.status = 'processing';
|
|
item.startTime = new Date().toISOString();
|
|
item.attempts++;
|
|
|
|
await this.updateStatus();
|
|
await this.saveQueue();
|
|
|
|
try {
|
|
// Traiter la ligne avec le pipeline modulaire
|
|
const result = await this.processRow(item.rowNumber);
|
|
|
|
// Succès
|
|
item.status = 'completed';
|
|
item.result = result;
|
|
item.endTime = new Date().toISOString();
|
|
item.error = null;
|
|
|
|
this.processedCount++;
|
|
|
|
logSh(`✅ Ligne ${item.rowNumber} traitée avec succès`, 'INFO');
|
|
|
|
// Callback succès
|
|
if (this.onProgress) {
|
|
this.onProgress(item, this.getProgress());
|
|
}
|
|
|
|
} catch (error) {
|
|
// Erreur
|
|
item.error = {
|
|
message: error.message,
|
|
stack: error.stack,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
if (item.attempts >= item.maxAttempts) {
|
|
item.status = 'failed';
|
|
this.errorCount++;
|
|
|
|
logSh(`❌ Ligne ${item.rowNumber} échouée définitivement après ${item.attempts} tentatives`, 'ERROR');
|
|
} else {
|
|
item.status = 'error';
|
|
logSh(`⚠️ Ligne ${item.rowNumber} échouée, retry possible`, 'WARNING');
|
|
}
|
|
|
|
// Callback erreur
|
|
if (this.onError) {
|
|
this.onError(item, error);
|
|
}
|
|
}
|
|
|
|
this.currentRow = null;
|
|
await this.updateStatus();
|
|
await this.saveQueue();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Traite une ligne spécifique
|
|
*/
|
|
async processRow(rowNumber) {
|
|
return tracer.run('BatchProcessor.processRow', { rowNumber }, async () => {
|
|
// Configuration pour cette ligne
|
|
const rowConfig = {
|
|
rowNumber,
|
|
source: 'batch_processor',
|
|
selectiveStack: this.config.selective,
|
|
adversarialMode: this.config.adversarial,
|
|
humanSimulationMode: this.config.humanSimulation,
|
|
patternBreakingMode: this.config.patternBreaking,
|
|
intensity: this.config.intensity,
|
|
saveIntermediateSteps: this.config.saveIntermediateSteps
|
|
};
|
|
|
|
logSh(`🎯 Configuration ligne ${rowNumber}: ${JSON.stringify(rowConfig)}`, 'DEBUG');
|
|
|
|
// Exécuter le workflow modulaire
|
|
const result = await handleModularWorkflow(rowConfig);
|
|
|
|
logSh(`📊 Résultat ligne ${rowNumber}: ${result ? 'SUCCESS' : 'FAILED'}`, 'INFO');
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
// ========================================
|
|
// GESTION ÉTAT
|
|
// ========================================
|
|
|
|
/**
|
|
* Met à jour le status
|
|
*/
|
|
async updateStatus() {
|
|
const status = this.getStatus();
|
|
|
|
try {
|
|
await fs.writeFile(this.statusPath, JSON.stringify(status, null, 2));
|
|
|
|
// Callback update
|
|
if (this.onStatusUpdate) {
|
|
this.onStatusUpdate(status);
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur mise à jour status: ${error.message}`, 'ERROR');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retourne le status actuel
|
|
*/
|
|
getStatus() {
|
|
const now = new Date();
|
|
const completedItems = this.queue.filter(item => item.status === 'completed').length;
|
|
const failedItems = this.queue.filter(item => item.status === 'failed').length;
|
|
const totalItems = this.queue.length;
|
|
|
|
const progress = totalItems > 0 ? ((completedItems + failedItems) / totalItems) * 100 : 0;
|
|
|
|
let status = 'idle';
|
|
if (this.isRunning && this.isPaused) {
|
|
status = 'paused';
|
|
} else if (this.isRunning) {
|
|
status = 'running';
|
|
} else if (completedItems + failedItems === totalItems && totalItems > 0) {
|
|
status = 'completed';
|
|
}
|
|
|
|
return {
|
|
status,
|
|
currentRow: this.currentRow,
|
|
totalRows: totalItems,
|
|
completedRows: completedItems,
|
|
failedRows: failedItems,
|
|
progress: Math.round(progress),
|
|
startTime: this.startTime ? this.startTime.toISOString() : null,
|
|
estimatedEnd: this.estimateCompletionTime(),
|
|
errors: this.queue.filter(item => item.error).map(item => ({
|
|
rowNumber: item.rowNumber,
|
|
error: item.error,
|
|
attempts: item.attempts
|
|
})),
|
|
lastResult: this.getLastResult(),
|
|
config: this.config,
|
|
queue: this.queue
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Retourne la progression détaillée
|
|
*/
|
|
getProgress() {
|
|
const status = this.getStatus();
|
|
const now = new Date();
|
|
const elapsed = this.startTime ? now - this.startTime : 0;
|
|
|
|
const avgTimePerRow = status.completedRows > 0 ? elapsed / status.completedRows : 0;
|
|
const remainingRows = status.totalRows - status.completedRows - status.failedRows;
|
|
const estimatedRemaining = avgTimePerRow * remainingRows;
|
|
|
|
return {
|
|
...status,
|
|
metrics: {
|
|
elapsedTime: elapsed,
|
|
avgTimePerRow: avgTimePerRow,
|
|
estimatedRemaining: estimatedRemaining,
|
|
completionPercentage: status.progress,
|
|
throughput: status.completedRows > 0 && elapsed > 0 ? (status.completedRows / (elapsed / 1000 / 60)) : 0 // rows/minute
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Estime l'heure de fin
|
|
*/
|
|
estimateCompletionTime() {
|
|
if (!this.startTime || !this.isRunning || this.isPaused) {
|
|
return null;
|
|
}
|
|
|
|
const progress = this.getProgress();
|
|
if (progress.metrics.estimatedRemaining > 0) {
|
|
const endTime = new Date(Date.now() + progress.metrics.estimatedRemaining);
|
|
return endTime.toISOString();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Retourne le dernier résultat
|
|
*/
|
|
getLastResult() {
|
|
const completedItems = this.queue.filter(item => item.status === 'completed');
|
|
if (completedItems.length === 0) return null;
|
|
|
|
const lastItem = completedItems[completedItems.length - 1];
|
|
return {
|
|
rowNumber: lastItem.rowNumber,
|
|
result: lastItem.result,
|
|
endTime: lastItem.endTime
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Gère les erreurs critiques
|
|
*/
|
|
async handleError(error) {
|
|
logSh(`💥 Erreur critique BatchProcessor: ${error.message}`, 'ERROR');
|
|
|
|
this.isRunning = false;
|
|
this.isPaused = false;
|
|
|
|
await this.updateStatus();
|
|
|
|
if (this.onError) {
|
|
this.onError(null, error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Termine le traitement
|
|
*/
|
|
async complete() {
|
|
logSh('🏁 Traitement batch terminé', 'INFO');
|
|
|
|
this.isRunning = false;
|
|
this.isPaused = false;
|
|
this.currentRow = null;
|
|
|
|
await this.updateStatus();
|
|
|
|
if (this.onComplete) {
|
|
this.onComplete(this.getStatus());
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// UTILITAIRES
|
|
// ========================================
|
|
|
|
/**
|
|
* Pause l'exécution
|
|
*/
|
|
async sleep(ms) {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
|
|
/**
|
|
* Reset la queue
|
|
*/
|
|
async resetQueue() {
|
|
logSh('🔄 Reset de la queue', 'INFO');
|
|
|
|
this.queue = [];
|
|
this.processedCount = 0;
|
|
this.errorCount = 0;
|
|
|
|
await this.populateQueue();
|
|
await this.updateStatus();
|
|
}
|
|
|
|
/**
|
|
* Configure les callbacks
|
|
*/
|
|
setCallbacks({ onStatusUpdate, onProgress, onError, onComplete }) {
|
|
this.onStatusUpdate = onStatusUpdate;
|
|
this.onProgress = onProgress;
|
|
this.onError = onError;
|
|
this.onComplete = onComplete;
|
|
}
|
|
}
|
|
|
|
// ============= EXPORTS =============
|
|
module.exports = { BatchProcessor }; |