seo-generator-server/lib/batch/BatchProcessor.original.js
StillHammer a2ffe7fec5 Refactor batch processing system with shared QueueProcessor base class
• 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>
2025-09-19 02:04:48 +08:00

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 };