import { pool } from '../db/pool'; import { testProxy, saveProxyTestResult } from './proxy'; interface ProxyTestJob { id: number; status: string; total_proxies: number; tested_proxies: number; passed_proxies: number; failed_proxies: number; mode?: string; // 'all' | 'failed' | 'inactive' } // Concurrency settings const DEFAULT_CONCURRENCY = 10; // Test 10 proxies at a time // Simple in-memory queue - could be replaced with Bull/Bee-Queue for production const activeJobs = new Map(); // Clean up orphaned jobs on server startup export async function cleanupOrphanedJobs(): Promise { try { const result = await pool.query(` UPDATE proxy_test_jobs SET status = 'cancelled', completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE status IN ('pending', 'running') RETURNING id `); if (result.rows.length > 0) { console.log(`🧹 Cleaned up ${result.rows.length} orphaned proxy test jobs`); } } catch (error) { console.error('Error cleaning up orphaned jobs:', error); } } export type ProxyTestMode = 'all' | 'failed' | 'inactive'; export interface CreateJobResult { jobId: number; totalProxies: number; } export async function createProxyTestJob(mode: ProxyTestMode = 'all', concurrency: number = DEFAULT_CONCURRENCY): Promise { // Check for existing running jobs first const existingJob = await getActiveProxyTestJob(); if (existingJob) { throw new Error('A proxy test job is already running. Please cancel it first.'); } // Get count based on mode let countQuery: string; switch (mode) { case 'failed': countQuery = `SELECT COUNT(*) as count FROM proxies WHERE test_result = 'failed' OR active = false`; break; case 'inactive': countQuery = `SELECT COUNT(*) as count FROM proxies WHERE active = false`; break; default: countQuery = `SELECT COUNT(*) as count FROM proxies`; } const result = await pool.query(countQuery); const totalProxies = parseInt(result.rows[0].count); if (totalProxies === 0) { throw new Error(`No proxies to test with mode '${mode}'`); } const jobResult = await pool.query(` INSERT INTO proxy_test_jobs (status, total_proxies) VALUES ('pending', $1) RETURNING id `, [totalProxies]); const jobId = jobResult.rows[0].id; // Start job in background with mode and concurrency runProxyTestJob(jobId, mode, concurrency).catch(err => { console.error(`❌ Proxy test job ${jobId} failed:`, err); }); return { jobId, totalProxies }; } export async function getProxyTestJob(jobId: number): Promise { const result = await pool.query(` SELECT id, status, total_proxies, tested_proxies, passed_proxies, failed_proxies FROM proxy_test_jobs WHERE id = $1 `, [jobId]); if (result.rows.length === 0) { return null; } return result.rows[0]; } export async function getActiveProxyTestJob(): Promise { const result = await pool.query(` SELECT id, status, total_proxies, tested_proxies, passed_proxies, failed_proxies FROM proxy_test_jobs WHERE status IN ('pending', 'running') ORDER BY created_at DESC LIMIT 1 `); if (result.rows.length === 0) { return null; } return result.rows[0]; } export async function cancelProxyTestJob(jobId: number): Promise { // Try to cancel in-memory job first const jobControl = activeJobs.get(jobId); if (jobControl) { jobControl.cancelled = true; } // Always update database to handle orphaned jobs const result = await pool.query(` UPDATE proxy_test_jobs SET status = 'cancelled', completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $1 AND status IN ('pending', 'running') RETURNING id `, [jobId]); return result.rows.length > 0; } async function runProxyTestJob(jobId: number, mode: ProxyTestMode = 'all', concurrency: number = DEFAULT_CONCURRENCY): Promise { // Register job as active activeJobs.set(jobId, { cancelled: false }); try { // Update status to running await pool.query(` UPDATE proxy_test_jobs SET status = 'running', started_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $1 `, [jobId]); console.log(`πŸ” Starting proxy test job ${jobId} (mode: ${mode}, concurrency: ${concurrency})...`); // Get proxies based on mode let query: string; switch (mode) { case 'failed': query = `SELECT id, host, port, protocol, username, password FROM proxies WHERE test_result = 'failed' OR active = false ORDER BY id`; break; case 'inactive': query = `SELECT id, host, port, protocol, username, password FROM proxies WHERE active = false ORDER BY id`; break; default: query = `SELECT id, host, port, protocol, username, password FROM proxies ORDER BY id`; } const result = await pool.query(query); const proxies = result.rows; let tested = 0; let passed = 0; let failed = 0; // Process proxies in batches for parallel testing for (let i = 0; i < proxies.length; i += concurrency) { // Check if job was cancelled const jobControl = activeJobs.get(jobId); if (jobControl?.cancelled) { console.log(`⏸️ Proxy test job ${jobId} cancelled`); break; } const batch = proxies.slice(i, i + concurrency); // Test batch in parallel const batchResults = await Promise.all( batch.map(async (proxy) => { const testResult = await testProxy( proxy.host, proxy.port, proxy.protocol, proxy.username, proxy.password ); // Save result await saveProxyTestResult(proxy.id, testResult); return testResult.success; }) ); // Count results for (const success of batchResults) { tested++; if (success) { passed++; } else { failed++; } } // Update job progress await pool.query(` UPDATE proxy_test_jobs SET tested_proxies = $1, passed_proxies = $2, failed_proxies = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4 `, [tested, passed, failed, jobId]); // Log progress console.log(`πŸ“Š Job ${jobId}: ${tested}/${proxies.length} proxies tested (${passed} passed, ${failed} failed)`); } // Mark job as completed const jobControl = activeJobs.get(jobId); const finalStatus = jobControl?.cancelled ? 'cancelled' : 'completed'; await pool.query(` UPDATE proxy_test_jobs SET status = $1, completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $2 `, [finalStatus, jobId]); console.log(`βœ… Proxy test job ${jobId} ${finalStatus}: ${tested} tested, ${passed} passed, ${failed} failed`); } catch (error) { console.error(`❌ Proxy test job ${jobId} error:`, error); await pool.query(` UPDATE proxy_test_jobs SET status = 'failed', completed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = $1 `, [jobId]); } finally { // Remove from active jobs activeJobs.delete(jobId); } }