taolib.testing.task_queue.server.app

taolib.testing.task_queue.server.app#

FastAPI 应用工厂模块。

创建和配置 FastAPI 应用实例。

Attributes#

Functions#

lifespan(→ collections.abc.AsyncGenerator[None])

应用生命周期管理。

create_app(→ fastapi.FastAPI)

创建 FastAPI 应用实例。

Module Contents#

async taolib.testing.task_queue.server.app.lifespan(app: fastapi.FastAPI) collections.abc.AsyncGenerator[None]#

应用生命周期管理。

taolib.testing.task_queue.server.app.create_app() fastapi.FastAPI#

创建 FastAPI 应用实例。

taolib.testing.task_queue.server.app._DASHBOARD_HTML = Multiline-String#
Show Value
"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Task Queue Dashboard</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
        .header { background: #e67e22; color: white; padding: 20px; display: flex; justify-content: space-between; align-items: center; }
        .header h1 { font-size: 24px; font-weight: 500; }
        .header button { background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.4); padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; }
        .header button:hover { background: rgba(255,255,255,0.3); }
        .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
        .card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
        .card h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 1px solid #eee; padding-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
        .metrics { display: flex; gap: 20px; flex-wrap: wrap; }
        .metric-card { flex: 1; min-width: 140px; text-align: center; padding: 15px; border-radius: 8px; background: #f8f9fa; }
        .metric-value { font-size: 36px; font-weight: 700; }
        .metric-label { font-size: 13px; color: #666; margin-top: 5px; }
        .metric-card.pending .metric-value { color: #f39c12; }
        .metric-card.running .metric-value { color: #3498db; }
        .metric-card.completed .metric-value { color: #27ae60; }
        .metric-card.failed .metric-value { color: #e74c3c; }
        .queue-bars { display: flex; gap: 15px; align-items: flex-end; }
        .queue-bar { flex: 1; text-align: center; }
        .queue-bar-fill { background: #3498db; border-radius: 4px 4px 0 0; min-height: 4px; transition: height 0.3s; margin: 0 10px; }
        .queue-bar-label { font-size: 13px; color: #666; margin-top: 8px; }
        .queue-bar-value { font-size: 18px; font-weight: 600; color: #333; margin-top: 4px; }
        .queue-bar.high .queue-bar-fill { background: #e74c3c; }
        .queue-bar.normal .queue-bar-fill { background: #f39c12; }
        .queue-bar.low .queue-bar-fill { background: #27ae60; }
        table { width: 100%; border-collapse: collapse; }
        th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #eee; font-size: 14px; }
        th { background: #f8f9fa; font-weight: 500; color: #666; }
        tr:hover { background: #f8f9fa; }
        .status { padding: 3px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; display: inline-block; }
        .status.pending { background: #fff3cd; color: #856404; }
        .status.running { background: #cce5ff; color: #004085; }
        .status.completed { background: #d4edda; color: #155724; }
        .status.failed { background: #f8d7da; color: #721c24; }
        .status.retrying { background: #e2e3f1; color: #383d6e; }
        .status.cancelled { background: #e2e2e2; color: #555; }
        .priority { padding: 3px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; }
        .priority.high { background: #fce4ec; color: #c62828; }
        .priority.normal { background: #fff8e1; color: #f57f17; }
        .priority.low { background: #e8f5e9; color: #2e7d32; }
        .btn { padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; }
        .btn-primary { background: #3498db; color: white; }
        .btn-primary:hover { background: #2980b9; }
        .btn-warning { background: #f39c12; color: white; }
        .btn-warning:hover { background: #e67e22; }
        .btn-sm { padding: 4px 8px; font-size: 12px; }
        .modal-overlay { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 100; justify-content: center; align-items: center; }
        .modal-overlay.active { display: flex; }
        .modal { background: white; border-radius: 8px; padding: 24px; max-width: 700px; width: 90%; max-height: 80vh; overflow-y: auto; }
        .modal h3 { margin-bottom: 16px; }
        .modal .close-btn { float: right; background: none; border: none; font-size: 20px; cursor: pointer; color: #666; }
        .modal pre { background: #f5f5f5; padding: 12px; border-radius: 4px; overflow-x: auto; font-size: 13px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
        .detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
        .detail-label { width: 120px; font-weight: 500; color: #666; flex-shrink: 0; }
        .detail-value { flex: 1; }
        .empty { text-align: center; padding: 30px; color: #999; }
        .task-id { font-family: monospace; font-size: 13px; color: #666; cursor: pointer; }
        .task-id:hover { color: #3498db; text-decoration: underline; }
        .error-text { color: #e74c3c; font-size: 13px; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
    </style>
</head>
<body>
    <div class="header">
        <h1>Task Queue Dashboard</h1>
        <button onclick="refreshAll()">Refresh</button>
    </div>
    <div class="container">
        <div class="card">
            <h2>Overview</h2>
            <div class="metrics">
                <div class="metric-card pending">
                    <div class="metric-value" id="metricPending">-</div>
                    <div class="metric-label">Pending</div>
                </div>
                <div class="metric-card running">
                    <div class="metric-value" id="metricRunning">-</div>
                    <div class="metric-label">Running</div>
                </div>
                <div class="metric-card completed">
                    <div class="metric-value" id="metricCompleted">-</div>
                    <div class="metric-label">Completed</div>
                </div>
                <div class="metric-card failed">
                    <div class="metric-value" id="metricFailed">-</div>
                    <div class="metric-label">Failed</div>
                </div>
            </div>
        </div>
        <div class="card">
            <h2>Queue Depths</h2>
            <div class="queue-bars" id="queueBars">
                <div class="queue-bar high">
                    <div class="queue-bar-fill" id="barHigh" style="height:4px"></div>
                    <div class="queue-bar-value" id="valHigh">0</div>
                    <div class="queue-bar-label">HIGH</div>
                </div>
                <div class="queue-bar normal">
                    <div class="queue-bar-fill" id="barNormal" style="height:4px"></div>
                    <div class="queue-bar-value" id="valNormal">0</div>
                    <div class="queue-bar-label">NORMAL</div>
                </div>
                <div class="queue-bar low">
                    <div class="queue-bar-fill" id="barLow" style="height:4px"></div>
                    <div class="queue-bar-value" id="valLow">0</div>
                    <div class="queue-bar-label">LOW</div>
                </div>
            </div>
        </div>
        <div class="card">
            <h2>Running Tasks</h2>
            <table>
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Type</th>
                        <th>Priority</th>
                        <th>Started</th>
                        <th>Duration</th>
                    </tr>
                </thead>
                <tbody id="runningTable">
                    <tr><td colspan="5" class="empty">Loading...</td></tr>
                </tbody>
            </table>
        </div>
        <div class="card">
            <h2>Recent Failed Tasks</h2>
            <table>
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>Type</th>
                        <th>Error</th>
                        <th>Retries</th>
                        <th>Failed At</th>
                        <th>Action</th>
                    </tr>
                </thead>
                <tbody id="failedTable">
                    <tr><td colspan="6" class="empty">Loading...</td></tr>
                </tbody>
            </table>
        </div>
    </div>

    <div class="modal-overlay" id="taskModal">
        <div class="modal">
            <button class="close-btn" onclick="closeModal()">&times;</button>
            <h3>Task Detail</h3>
            <div id="taskDetail"></div>
        </div>
    </div>

    <script>
        const API = '/api/v1';

        async function loadStats() {
            try {
                const res = await fetch(API + '/stats');
                const d = await res.json();
                document.getElementById('metricPending').textContent = d.pending + (d.retrying ? '+' + d.retrying : '');
                document.getElementById('metricRunning').textContent = d.running;
                document.getElementById('metricCompleted').textContent = d.completed;
                document.getElementById('metricFailed').textContent = d.failed;
                updateBars(d.queue_high, d.queue_normal, d.queue_low);
            } catch(e) { console.error('Stats error:', e); }
        }

        function updateBars(h, n, l) {
            const max = Math.max(h, n, l, 1);
            const scale = 80;
            document.getElementById('barHigh').style.height = Math.max(4, (h/max)*scale) + 'px';
            document.getElementById('barNormal').style.height = Math.max(4, (n/max)*scale) + 'px';
            document.getElementById('barLow').style.height = Math.max(4, (l/max)*scale) + 'px';
            document.getElementById('valHigh').textContent = h;
            document.getElementById('valNormal').textContent = n;
            document.getElementById('valLow').textContent = l;
        }

        async function loadRunning() {
            try {
                const res = await fetch(API + '/tasks?status=running&limit=50');
                const d = await res.json();
                const tbody = document.getElementById('runningTable');
                if (!d.items || d.items.length === 0) {
                    tbody.innerHTML = '<tr><td colspan="5" class="empty">No running tasks</td></tr>';
                    return;
                }
                const now = Date.now();
                tbody.innerHTML = d.items.map(t => {
                    const started = t.started_at ? new Date(t.started_at) : null;
                    const dur = started ? ((now - started.getTime())/1000).toFixed(1)+'s' : '-';
                    return `<tr>
                        <td><span class="task-id" onclick="viewTask('${t.id}')">${t.id.substring(0,12)}...</span></td>
                        <td>${t.task_type}</td>
                        <td><span class="priority ${t.priority}">${t.priority}</span></td>
                        <td>${started ? started.toLocaleTimeString() : '-'}</td>
                        <td>${dur}</td>
                    </tr>`;
                }).join('');
            } catch(e) { console.error('Running error:', e); }
        }

        async function loadFailed() {
            try {
                const res = await fetch(API + '/tasks?status=failed&limit=20');
                const d = await res.json();
                const tbody = document.getElementById('failedTable');
                if (!d.items || d.items.length === 0) {
                    tbody.innerHTML = '<tr><td colspan="6" class="empty">No failed tasks</td></tr>';
                    return;
                }
                tbody.innerHTML = d.items.map(t => {
                    const failedAt = t.completed_at ? new Date(t.completed_at).toLocaleString() : '-';
                    const errShort = (t.error_message || '').substring(0, 60);
                    return `<tr>
                        <td><span class="task-id" onclick="viewTask('${t.id}')">${t.id.substring(0,12)}...</span></td>
                        <td>${t.task_type}</td>
                        <td class="error-text" title="${(t.error_message||'').replace(/"/g,'&quot;')}">${errShort}</td>
                        <td>${t.retry_count}/${t.max_retries}</td>
                        <td>${failedAt}</td>
                        <td><button class="btn btn-warning btn-sm" onclick="retryTask('${t.id}')">Retry</button></td>
                    </tr>`;
                }).join('');
            } catch(e) { console.error('Failed error:', e); }
        }

        async function retryTask(id) {
            try {
                const res = await fetch(API + '/tasks/' + id + '/retry', { method: 'POST' });
                if (res.ok) {
                    alert('Task re-queued for retry.');
                    refreshAll();
                } else {
                    const d = await res.json();
                    alert('Retry failed: ' + (d.detail || 'Unknown error'));
                }
            } catch(e) { alert('Retry error: ' + e.message); }
        }

        async function viewTask(id) {
            try {
                const res = await fetch(API + '/tasks/' + id);
                const t = await res.json();
                const detail = document.getElementById('taskDetail');
                detail.innerHTML = `
                    <div class="detail-row"><div class="detail-label">ID</div><div class="detail-value" style="font-family:monospace">${t.id}</div></div>
                    <div class="detail-row"><div class="detail-label">Type</div><div class="detail-value">${t.task_type}</div></div>
                    <div class="detail-row"><div class="detail-label">Status</div><div class="detail-value"><span class="status ${t.status}">${t.status}</span></div></div>
                    <div class="detail-row"><div class="detail-label">Priority</div><div class="detail-value"><span class="priority ${t.priority}">${t.priority}</span></div></div>
                    <div class="detail-row"><div class="detail-label">Retries</div><div class="detail-value">${t.retry_count}/${t.max_retries}</div></div>
                    <div class="detail-row"><div class="detail-label">Created</div><div class="detail-value">${new Date(t.created_at).toLocaleString()}</div></div>
                    <div class="detail-row"><div class="detail-label">Started</div><div class="detail-value">${t.started_at ? new Date(t.started_at).toLocaleString() : '-'}</div></div>
                    <div class="detail-row"><div class="detail-label">Completed</div><div class="detail-value">${t.completed_at ? new Date(t.completed_at).toLocaleString() : '-'}</div></div>
                    <div class="detail-row"><div class="detail-label">Tags</div><div class="detail-value">${(t.tags||[]).join(', ') || '-'}</div></div>
                    <div class="detail-row"><div class="detail-label">Params</div><div class="detail-value"><pre>${JSON.stringify(t.params, null, 2)}</pre></div></div>
                    ${t.result ? `<div class="detail-row"><div class="detail-label">Result</div><div class="detail-value"><pre>${JSON.stringify(t.result, null, 2)}</pre></div></div>` : ''}
                    ${t.error_message ? `<div class="detail-row"><div class="detail-label">Error</div><div class="detail-value" style="color:#e74c3c">${t.error_message}</div></div>` : ''}
                    ${t.error_traceback ? `<div class="detail-row"><div class="detail-label">Traceback</div><div class="detail-value"><pre>${t.error_traceback}</pre></div></div>` : ''}
                `;
                document.getElementById('taskModal').classList.add('active');
            } catch(e) { alert('Failed to load task: ' + e.message); }
        }

        function closeModal() {
            document.getElementById('taskModal').classList.remove('active');
        }

        document.getElementById('taskModal').addEventListener('click', function(e) {
            if (e.target === this) closeModal();
        });

        function refreshAll() {
            loadStats();
            loadRunning();
            loadFailed();
        }

        refreshAll();
        setInterval(refreshAll, 10000);
    </script>
</body>
</html>
"""