Files
cannaiq/backend/src/services/proxyTestQueue.ts
Kelly 249d3c1b7f fix: Build args format for version info + schema-tolerant routes
CI/CD:
- Fix build_args format in woodpecker CI (comma-separated, not YAML list)
- This fixes "unknown" SHA/version showing on remote deployments

Backend schema-tolerant fixes (graceful fallbacks when tables missing):
- users.ts: Check which columns exist before querying
- worker-registry.ts: Return empty result if table doesn't exist
- task-service.ts: Add tableExists() helper, handle missing tables/views
- proxies.ts: Return totalProxies in test-all response

Frontend fixes:
- Proxies: Use total from response for accurate progress display
- SEO PagesTab: Dim Generate button when no AI provider active

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 09:53:21 -07:00

257 lines
7.3 KiB
TypeScript

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<number, { cancelled: boolean }>();
// Clean up orphaned jobs on server startup
export async function cleanupOrphanedJobs(): Promise<void> {
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<CreateJobResult> {
// 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<ProxyTestJob | null> {
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<ProxyTestJob | null> {
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<boolean> {
// 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<void> {
// 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);
}
}