feat: sync full-stack Docker runtime and UI
This commit is contained in:
+165
-26
@@ -33,6 +33,14 @@ JOB_TYPES = (
|
||||
"asr",
|
||||
)
|
||||
|
||||
|
||||
def _int_env(name: str, default: int) -> int:
|
||||
try:
|
||||
return max(1, int(os.getenv(name, str(default))))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
DEFAULT_CONCURRENCY = {
|
||||
"completion": 2,
|
||||
"pro_completion": 1,
|
||||
@@ -40,8 +48,8 @@ DEFAULT_CONCURRENCY = {
|
||||
"compress": 1,
|
||||
"ocr": 1,
|
||||
"convert": 1,
|
||||
"tts": 1,
|
||||
"asr": 1,
|
||||
"tts": _int_env("JOB_TTS_CONCURRENCY", 2),
|
||||
"asr": _int_env("JOB_ASR_CONCURRENCY", 1),
|
||||
}
|
||||
|
||||
DEFAULT_QUEUE_SIZE = {
|
||||
@@ -51,8 +59,8 @@ DEFAULT_QUEUE_SIZE = {
|
||||
"compress": 8,
|
||||
"ocr": 8,
|
||||
"convert": 8,
|
||||
"tts": 4,
|
||||
"asr": 4,
|
||||
"tts": _int_env("JOB_TTS_MAX_QUEUE", 8),
|
||||
"asr": _int_env("JOB_ASR_MAX_QUEUE", 8),
|
||||
}
|
||||
|
||||
|
||||
@@ -94,13 +102,6 @@ def _bool_env(name: str, default: bool) -> bool:
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def _int_env(name: str, default: int) -> int:
|
||||
try:
|
||||
return max(1, int(os.getenv(name, str(default))))
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _float_env(name: str, default: float) -> float:
|
||||
try:
|
||||
return float(os.getenv(name, str(default)))
|
||||
@@ -248,7 +249,7 @@ class InMemoryJobManager(BaseJobManager):
|
||||
async with self.lock:
|
||||
config = _queue_config(job_type)
|
||||
if self.queue_counts[job_type] >= config.max_queue:
|
||||
raise QueueFullError(f"{job_type} queue is full")
|
||||
raise QueueFullError(job_type, config.max_queue)
|
||||
job_id = request_id or str(uuid.uuid4())
|
||||
self.jobs[job_id] = {
|
||||
"job_id": job_id,
|
||||
@@ -261,6 +262,11 @@ class InMemoryJobManager(BaseJobManager):
|
||||
"cancel_requested": False,
|
||||
"created_at": _now_ms(),
|
||||
"updated_at": _now_ms(),
|
||||
"started_at": 0,
|
||||
"completed_at": 0,
|
||||
"queue_ms": 0,
|
||||
"run_ms": 0,
|
||||
"total_ms": 0,
|
||||
}
|
||||
self.event_history[job_id] = []
|
||||
self.queue_counts[job_type] += 1
|
||||
@@ -305,6 +311,12 @@ class InMemoryJobManager(BaseJobManager):
|
||||
"status": job["status"],
|
||||
"result": job["result"],
|
||||
"error": job["error"],
|
||||
"created_at": job.get("created_at", 0),
|
||||
"started_at": job.get("started_at", 0),
|
||||
"completed_at": job.get("completed_at", 0),
|
||||
"queue_ms": job.get("queue_ms", 0),
|
||||
"run_ms": job.get("run_ms", 0),
|
||||
"total_ms": job.get("total_ms", 0),
|
||||
**metrics,
|
||||
}
|
||||
|
||||
@@ -353,7 +365,10 @@ class InMemoryJobManager(BaseJobManager):
|
||||
self.queue_counts[job_type] = max(0, self.queue_counts[job_type] - 1)
|
||||
self.running_counts[job_type] += 1
|
||||
job["status"] = "running"
|
||||
job["updated_at"] = _now_ms()
|
||||
started_at = _now_ms()
|
||||
job["updated_at"] = started_at
|
||||
job["started_at"] = started_at
|
||||
job["queue_ms"] = max(0, started_at - int(job.get("created_at", started_at)))
|
||||
metrics = self._metrics(job_type)
|
||||
await self._publish(job_id, "started", {"job_id": job_id, "type": job_type, "status": "running", **metrics})
|
||||
|
||||
@@ -364,7 +379,14 @@ class InMemoryJobManager(BaseJobManager):
|
||||
def is_cancelled() -> bool:
|
||||
return bool(job.get("cancel_requested"))
|
||||
|
||||
result = await self.handlers[job_type](job["payload"], emit, is_cancelled)
|
||||
job_payload = dict(job["payload"])
|
||||
job_payload["job_context"] = {
|
||||
"job_id": job_id,
|
||||
"created_at": int(job.get("created_at", 0) or 0),
|
||||
"started_at": int(job.get("started_at", 0) or 0),
|
||||
"queue_ms": int(job.get("queue_ms", 0) or 0),
|
||||
}
|
||||
result = await self.handlers[job_type](job_payload, emit, is_cancelled)
|
||||
async with self.lock:
|
||||
if job["cancel_requested"]:
|
||||
job["status"] = "cancelled"
|
||||
@@ -373,13 +395,35 @@ class InMemoryJobManager(BaseJobManager):
|
||||
return
|
||||
job["status"] = "completed"
|
||||
job["result"] = result
|
||||
job["updated_at"] = _now_ms()
|
||||
completed_at = _now_ms()
|
||||
job["updated_at"] = completed_at
|
||||
job["completed_at"] = completed_at
|
||||
job["run_ms"] = max(0, completed_at - int(job.get("started_at", completed_at)))
|
||||
job["total_ms"] = max(0, completed_at - int(job.get("created_at", completed_at)))
|
||||
metrics = self._metrics(job_type)
|
||||
await self._publish(job_id, "done", {"job_id": job_id, "type": job_type, "status": "completed", "result": result, **metrics})
|
||||
await self._publish(
|
||||
job_id,
|
||||
"done",
|
||||
{
|
||||
"job_id": job_id,
|
||||
"type": job_type,
|
||||
"status": "completed",
|
||||
"result": result,
|
||||
"queue_ms": job.get("queue_ms", 0),
|
||||
"run_ms": job.get("run_ms", 0),
|
||||
"total_ms": job.get("total_ms", 0),
|
||||
**metrics,
|
||||
},
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
async with self.lock:
|
||||
job["status"] = "cancelled"
|
||||
job["cancel_requested"] = True
|
||||
completed_at = _now_ms()
|
||||
job["updated_at"] = completed_at
|
||||
job["completed_at"] = completed_at
|
||||
job["run_ms"] = max(0, completed_at - int(job.get("started_at", completed_at)))
|
||||
job["total_ms"] = max(0, completed_at - int(job.get("created_at", completed_at)))
|
||||
metrics = self._metrics(job_type)
|
||||
await self._publish(job_id, "cancelled", {"job_id": job_id, "type": job_type, "status": "cancelled", **metrics})
|
||||
raise
|
||||
@@ -388,9 +432,26 @@ class InMemoryJobManager(BaseJobManager):
|
||||
async with self.lock:
|
||||
job["status"] = "failed"
|
||||
job["error"] = str(exc)
|
||||
job["updated_at"] = _now_ms()
|
||||
completed_at = _now_ms()
|
||||
job["updated_at"] = completed_at
|
||||
job["completed_at"] = completed_at
|
||||
job["run_ms"] = max(0, completed_at - int(job.get("started_at", completed_at)))
|
||||
job["total_ms"] = max(0, completed_at - int(job.get("created_at", completed_at)))
|
||||
metrics = self._metrics(job_type)
|
||||
await self._publish(job_id, "error", {"job_id": job_id, "type": job_type, "status": "failed", "error": str(exc), **metrics})
|
||||
await self._publish(
|
||||
job_id,
|
||||
"error",
|
||||
{
|
||||
"job_id": job_id,
|
||||
"type": job_type,
|
||||
"status": "failed",
|
||||
"error": str(exc),
|
||||
"queue_ms": job.get("queue_ms", 0),
|
||||
"run_ms": job.get("run_ms", 0),
|
||||
"total_ms": job.get("total_ms", 0),
|
||||
**metrics,
|
||||
},
|
||||
)
|
||||
finally:
|
||||
async with self.lock:
|
||||
self.running_counts[job_type] = max(0, self.running_counts[job_type] - 1)
|
||||
@@ -484,7 +545,7 @@ class RedisJobManager(BaseJobManager):
|
||||
config = _queue_config(job_type)
|
||||
metrics = await self._metrics(job_type)
|
||||
if metrics["queued_count"] >= config.max_queue:
|
||||
raise QueueFullError(f"{job_type} queue is full")
|
||||
raise QueueFullError(job_type, config.max_queue)
|
||||
|
||||
job_id = request_id or str(uuid.uuid4())
|
||||
created_at = _now_ms()
|
||||
@@ -496,6 +557,11 @@ class RedisJobManager(BaseJobManager):
|
||||
"error": "",
|
||||
"created_at": created_at,
|
||||
"updated_at": created_at,
|
||||
"started_at": 0,
|
||||
"completed_at": 0,
|
||||
"queue_ms": 0,
|
||||
"run_ms": 0,
|
||||
"total_ms": 0,
|
||||
"cancel_requested": "0",
|
||||
}
|
||||
await self._set_state(job_id, state)
|
||||
@@ -533,6 +599,12 @@ class RedisJobManager(BaseJobManager):
|
||||
"error": error,
|
||||
"result": _json_loads(result, result),
|
||||
"cancel_requested": state.get("cancel_requested") == "1",
|
||||
"created_at": int(state.get("created_at", "0") or 0),
|
||||
"started_at": int(state.get("started_at", "0") or 0),
|
||||
"completed_at": int(state.get("completed_at", "0") or 0),
|
||||
"queue_ms": int(state.get("queue_ms", "0") or 0),
|
||||
"run_ms": int(state.get("run_ms", "0") or 0),
|
||||
"total_ms": int(state.get("total_ms", "0") or 0),
|
||||
**metrics,
|
||||
}
|
||||
|
||||
@@ -621,6 +693,9 @@ class RedisWorker:
|
||||
semaphore: asyncio.Semaphore,
|
||||
) -> None:
|
||||
job_id = fields["job_id"]
|
||||
started_at = 0
|
||||
created_at = 0
|
||||
queue_ms = 0
|
||||
try:
|
||||
state = await self.manager.get_status(job_id)
|
||||
if not state or state["status"] == "cancelled":
|
||||
@@ -629,13 +704,21 @@ class RedisWorker:
|
||||
|
||||
await self.manager.redis.hincrby(self.manager._metrics_key(job_type), "queued_count", -1)
|
||||
await self.manager.redis.hincrby(self.manager._metrics_key(job_type), "running_count", 1)
|
||||
started_at = _now_ms()
|
||||
created_at = int(state.get("created_at", 0) or 0)
|
||||
queue_ms = max(0, started_at - created_at)
|
||||
await self.manager._set_state(job_id, {
|
||||
"job_id": job_id,
|
||||
"request_id": state["request_id"],
|
||||
"type": job_type,
|
||||
"status": "running",
|
||||
"updated_at": _now_ms(),
|
||||
"created_at": state.get("created_at", _now_ms()),
|
||||
"updated_at": started_at,
|
||||
"created_at": created_at or started_at,
|
||||
"started_at": started_at,
|
||||
"completed_at": 0,
|
||||
"queue_ms": queue_ms,
|
||||
"run_ms": 0,
|
||||
"total_ms": 0,
|
||||
"cancel_requested": "1" if state.get("cancel_requested") else "0",
|
||||
"error": "",
|
||||
})
|
||||
@@ -643,6 +726,12 @@ class RedisWorker:
|
||||
await self.manager._emit_event(job_id, "started", {"job_id": job_id, "type": job_type, "status": "running", **metrics})
|
||||
|
||||
payload = _json_loads(fields["payload"], {})
|
||||
payload["job_context"] = {
|
||||
"job_id": job_id,
|
||||
"created_at": created_at,
|
||||
"started_at": started_at,
|
||||
"queue_ms": queue_ms,
|
||||
}
|
||||
|
||||
async def emit(event: str, data: dict[str, Any]) -> None:
|
||||
live_state = await self.manager.get_status(job_id) or {"status": "running"}
|
||||
@@ -656,6 +745,7 @@ class RedisWorker:
|
||||
result = await self.manager.handlers[job_type](payload, emit, is_cancelled)
|
||||
current = await self.manager.get_status(job_id)
|
||||
if current and current["status"] == "cancelled":
|
||||
await self.manager.redis.xack(queue_key, group, message_id)
|
||||
return
|
||||
|
||||
await self.manager._set_state(job_id, {
|
||||
@@ -664,16 +754,46 @@ class RedisWorker:
|
||||
"type": job_type,
|
||||
"status": "completed",
|
||||
"updated_at": _now_ms(),
|
||||
"created_at": state.get("created_at", _now_ms()),
|
||||
"created_at": created_at or started_at,
|
||||
"started_at": started_at,
|
||||
"completed_at": _now_ms(),
|
||||
"queue_ms": queue_ms,
|
||||
"run_ms": max(0, _now_ms() - started_at),
|
||||
"total_ms": max(0, _now_ms() - (created_at or started_at)),
|
||||
"cancel_requested": "0",
|
||||
"error": "",
|
||||
"result": _json_dumps(result),
|
||||
})
|
||||
metrics = await self.manager._metrics(job_type)
|
||||
await self.manager._emit_event(job_id, "done", {"job_id": job_id, "type": job_type, "status": "completed", "result": result, **metrics})
|
||||
final_state = await self.manager.get_status(job_id) or {}
|
||||
await self.manager._emit_event(
|
||||
job_id,
|
||||
"done",
|
||||
{
|
||||
"job_id": job_id,
|
||||
"type": job_type,
|
||||
"status": "completed",
|
||||
"result": result,
|
||||
"queue_ms": final_state.get("queue_ms", queue_ms),
|
||||
"run_ms": final_state.get("run_ms", 0),
|
||||
"total_ms": final_state.get("total_ms", 0),
|
||||
**metrics,
|
||||
},
|
||||
)
|
||||
await self.manager.redis.xack(queue_key, group, message_id)
|
||||
except asyncio.CancelledError:
|
||||
await self.manager.redis.hset(self.manager._state_key(job_id), mapping={"status": "cancelled", "cancel_requested": "1", "updated_at": _now_ms()})
|
||||
cancelled_at = _now_ms()
|
||||
await self.manager.redis.hset(
|
||||
self.manager._state_key(job_id),
|
||||
mapping={
|
||||
"status": "cancelled",
|
||||
"cancel_requested": "1",
|
||||
"updated_at": cancelled_at,
|
||||
"completed_at": cancelled_at,
|
||||
"run_ms": max(0, cancelled_at - started_at),
|
||||
"total_ms": max(0, cancelled_at - (created_at or started_at)),
|
||||
},
|
||||
)
|
||||
metrics = await self.manager._metrics(job_type)
|
||||
await self.manager._emit_event(job_id, "cancelled", {"job_id": job_id, "type": job_type, "status": "cancelled", **metrics})
|
||||
await self.manager.redis.xack(queue_key, group, message_id)
|
||||
@@ -688,12 +808,31 @@ class RedisWorker:
|
||||
"type": job_type,
|
||||
"status": "failed",
|
||||
"updated_at": _now_ms(),
|
||||
"created_at": state.get("created_at", _now_ms()) if state else _now_ms(),
|
||||
"created_at": created_at or (_now_ms() if state else _now_ms()),
|
||||
"started_at": started_at,
|
||||
"completed_at": _now_ms(),
|
||||
"queue_ms": queue_ms,
|
||||
"run_ms": max(0, _now_ms() - started_at),
|
||||
"total_ms": max(0, _now_ms() - (created_at or started_at)),
|
||||
"cancel_requested": "0",
|
||||
"error": str(exc),
|
||||
})
|
||||
metrics = await self.manager._metrics(job_type)
|
||||
await self.manager._emit_event(job_id, "error", {"job_id": job_id, "type": job_type, "status": "failed", "error": str(exc), **metrics})
|
||||
final_state = await self.manager.get_status(job_id) or {}
|
||||
await self.manager._emit_event(
|
||||
job_id,
|
||||
"error",
|
||||
{
|
||||
"job_id": job_id,
|
||||
"type": job_type,
|
||||
"status": "failed",
|
||||
"error": str(exc),
|
||||
"queue_ms": final_state.get("queue_ms", queue_ms),
|
||||
"run_ms": final_state.get("run_ms", 0),
|
||||
"total_ms": final_state.get("total_ms", 0),
|
||||
**metrics,
|
||||
},
|
||||
)
|
||||
await self.manager.redis.xack(queue_key, group, message_id)
|
||||
finally:
|
||||
self.running_tasks.pop(job_id, None)
|
||||
|
||||
Reference in New Issue
Block a user