реворк задач
This commit is contained in:
parent
735d8b2736
commit
de0a28ad62
3 changed files with 88 additions and 6 deletions
|
|
@ -28,6 +28,23 @@ from app.services.sse import SSEEvent, sse_manager
|
|||
_TASK_DEVICE_CONCURRENCY = settings.TASK_DEVICE_CONCURRENCY
|
||||
|
||||
|
||||
async def _quick_ping(ip: str, attempts: int = 4) -> bool:
|
||||
"""Quick reachability check: ping -c 1 -W 1, up to `attempts` times."""
|
||||
for _ in range(attempts):
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"ping", "-c", "1", "-W", "1", ip,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
await asyncio.wait_for(proc.wait(), timeout=3)
|
||||
if proc.returncode == 0:
|
||||
return True
|
||||
except (asyncio.TimeoutError, OSError):
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
async def _emit(task_id: str, message: str, level: str = "info") -> None:
|
||||
"""Write a progress line to DB and push to SSE subscribers."""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
|
@ -126,6 +143,41 @@ async def _run_on_device(
|
|||
task_row = await db.get(Task, task_id)
|
||||
if task_row and task_row.cancel_requested:
|
||||
return None # None = skipped due to cancellation
|
||||
|
||||
# Pre-ping reachability check (skip for ping action itself)
|
||||
if action.name != "ping":
|
||||
reachable = await _quick_ping(device.ip)
|
||||
if not reachable:
|
||||
await _emit(task_id, f"{device.ip}: unreachable, skipping", "warn")
|
||||
async with AsyncSessionLocal() as db:
|
||||
async with db.begin():
|
||||
tr = TaskResult(
|
||||
task_id=task_id,
|
||||
device_id=device.id,
|
||||
status="skipped",
|
||||
executed_at=datetime.now(timezone.utc),
|
||||
duration_ms=0,
|
||||
stderr="Device unreachable (ping pre-check failed)",
|
||||
)
|
||||
db.add(tr)
|
||||
await sse_manager.publish(SSEEvent(
|
||||
type="task_result",
|
||||
task_id=task_id,
|
||||
data={
|
||||
"device_id": device.id,
|
||||
"device_ip": device.ip,
|
||||
"device_hostname": device.hostname,
|
||||
"status": "skipped",
|
||||
"duration_ms": 0,
|
||||
"stdout": None,
|
||||
"stderr": "Device unreachable (ping pre-check failed)",
|
||||
"error": None,
|
||||
},
|
||||
))
|
||||
return None
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
async with db.begin():
|
||||
tr = TaskResult(
|
||||
task_id=task_id,
|
||||
device_id=device.id,
|
||||
|
|
@ -284,6 +336,11 @@ async def _run_task(task_id: str) -> None:
|
|||
return
|
||||
|
||||
await _emit(task_id, f"Running on {len(targets)} device(s)...", "info")
|
||||
await sse_manager.publish(SSEEvent(
|
||||
type="task_meta",
|
||||
task_id=task_id,
|
||||
data={"total": len(targets)},
|
||||
))
|
||||
sem = asyncio.Semaphore(_TASK_DEVICE_CONCURRENCY)
|
||||
|
||||
# Start heartbeat writer
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface SSEHandlers {
|
|||
error: string | null
|
||||
}) => void
|
||||
onTaskStatus?: (data: { status: string; total: number; ok: number; failed: number }) => void
|
||||
onTaskMeta?: (data: { total: number }) => void
|
||||
}
|
||||
|
||||
export function useSSE(taskId: string | null | undefined, handlers: SSEHandlers) {
|
||||
|
|
@ -35,6 +36,7 @@ export function useSSE(taskId: string | null | undefined, handlers: SSEHandlers)
|
|||
if (ev.event === 'task_event') handlersRef.current.onTaskEvent?.(data)
|
||||
else if (ev.event === 'task_result') handlersRef.current.onTaskResult?.(data)
|
||||
else if (ev.event === 'task_status') handlersRef.current.onTaskStatus?.(data)
|
||||
else if (ev.event === 'task_meta') handlersRef.current.onTaskMeta?.(data)
|
||||
} catch {
|
||||
// ignore parse errors
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
Descriptions,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Progress,
|
||||
Row,
|
||||
Space,
|
||||
Spin,
|
||||
|
|
@ -83,7 +84,7 @@ export function TaskDetailPage() {
|
|||
|
||||
const [log, setLog] = useState<LogLine[]>([])
|
||||
const [liveResults, setLiveResults] = useState<Record<number, DeviceResultState>>({})
|
||||
const logEndRef = useRef<HTMLDivElement>(null)
|
||||
const [totalDevices, setTotalDevices] = useState<number | null>(null)
|
||||
const [stdoutModal, setStdoutModal] = useState<string | null>(null)
|
||||
|
||||
const seenLogKeys = useRef(new Set<string>())
|
||||
|
|
@ -118,12 +119,9 @@ export function TaskDetailPage() {
|
|||
device_hostname: data.device_hostname ?? null,
|
||||
},
|
||||
})),
|
||||
onTaskMeta: (data) => setTotalDevices(data.total),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
logEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}, [log])
|
||||
|
||||
// Merge DB results with live SSE updates
|
||||
const mergedResults = useMemo<(TaskResult & Partial<DeviceResultState>)[]>(() => {
|
||||
if (!task?.device_results) return []
|
||||
|
|
@ -210,6 +208,7 @@ export function TaskDetailPage() {
|
|||
const s = r.status
|
||||
if (s === 'success') return <Tag icon={<CheckCircleOutlined />} color="success">Успешно</Tag>
|
||||
if (s === 'failed') return <Tag icon={<CloseCircleOutlined />} color="error">Ошибка</Tag>
|
||||
if (s === 'skipped') return <Tag icon={<StopOutlined />} color="warning">Пропущено</Tag>
|
||||
return <Tag icon={<LoadingOutlined />} color="processing">Ожидание</Tag>
|
||||
},
|
||||
},
|
||||
|
|
@ -348,6 +347,30 @@ export function TaskDetailPage() {
|
|||
</Col>
|
||||
</Row>
|
||||
|
||||
{totalDevices != null && totalDevices > 0 && (
|
||||
<Row style={{ marginBottom: 16 }}>
|
||||
<Col span={24}>
|
||||
{(() => {
|
||||
const completed = mergedResults.filter((r) => r.status !== 'running').length
|
||||
const failed = mergedResults.filter((r) => r.status === 'failed').length
|
||||
const percent = Math.round((completed / totalDevices) * 100)
|
||||
const isDone = !isActive
|
||||
const progressStatus = isDone
|
||||
? (failed > 0 ? 'exception' : 'success')
|
||||
: 'active'
|
||||
return (
|
||||
<Progress
|
||||
percent={percent}
|
||||
status={progressStatus}
|
||||
format={() => `${completed} / ${totalDevices}`}
|
||||
strokeColor={failed > 0 ? undefined : '#52c41a'}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={24}>
|
||||
<Card
|
||||
|
|
@ -378,7 +401,7 @@ export function TaskDetailPage() {
|
|||
</div>
|
||||
))
|
||||
)}
|
||||
<div ref={logEndRef} />
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue