mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 07:27:15 +08:00
fix(runtime): 收敛跨线程任务提交终态 (#6446)
This commit is contained in:
+88
-24
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
import threading
|
||||
from collections.abc import Coroutine
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
@@ -19,6 +20,14 @@ class TaskRecord:
|
||||
cancel_on_shutdown: bool
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class _ThreadsafeSubmission:
|
||||
"""持有跨线程提交从排队到真实 Task 发布之间的生命周期。"""
|
||||
|
||||
coroutine: Coroutine[Any, Any, Any]
|
||||
task: asyncio.Task[Any] | None = None
|
||||
|
||||
|
||||
class TaskRegistry:
|
||||
"""管理由宿主创建的进程内后台任务,并提供统一取消与等待入口。"""
|
||||
|
||||
@@ -27,6 +36,10 @@ class TaskRegistry:
|
||||
self._records: dict[asyncio.Task[Any], TaskRecord] = {}
|
||||
self._shutdown_cancel_requested: set[asyncio.Task[Any]] = set()
|
||||
self._shutdown_timeout_reported: set[asyncio.Task[Any]] = set()
|
||||
self._threadsafe_submissions: dict[
|
||||
concurrent.futures.Future[Any], _ThreadsafeSubmission
|
||||
] = {}
|
||||
self._state_lock = threading.Lock()
|
||||
self._accepting = True
|
||||
|
||||
@property
|
||||
@@ -77,37 +90,61 @@ class TaskRegistry:
|
||||
owner: str,
|
||||
cancel_on_shutdown: bool = True,
|
||||
) -> concurrent.futures.Future[Any]:
|
||||
"""从宿主线程提交协程,并在目标循环内原子登记 owner 后执行。"""
|
||||
"""从宿主线程提交协程,并持有排队阶段直至发布真实 Task。"""
|
||||
completion: concurrent.futures.Future[Any] = concurrent.futures.Future()
|
||||
task_holder: dict[str, asyncio.Task[Any]] = {}
|
||||
submission = _ThreadsafeSubmission(coroutine=coroutine)
|
||||
|
||||
def mirror_completion(task: asyncio.Task[Any]) -> None:
|
||||
"""把登记任务的真实终态镜像给跨线程调用方。"""
|
||||
if completion.done():
|
||||
return
|
||||
if task.cancelled():
|
||||
completion.cancel()
|
||||
try:
|
||||
if task.cancelled():
|
||||
completion.cancel()
|
||||
return
|
||||
exception = task.exception()
|
||||
if exception is not None:
|
||||
completion.set_exception(exception)
|
||||
else:
|
||||
completion.set_result(task.result())
|
||||
except concurrent.futures.InvalidStateError:
|
||||
# completion 可由调用线程同时取消,真实 Task 仍由 Registry 观察。
|
||||
return
|
||||
exception = task.exception()
|
||||
if exception is not None:
|
||||
completion.set_exception(exception)
|
||||
else:
|
||||
completion.set_result(task.result())
|
||||
|
||||
def submit_on_loop() -> None:
|
||||
"""在目标循环内完成 accepting 检查、任务创建和 owner 登记。"""
|
||||
if completion.cancelled():
|
||||
"""在目标循环内把 pending submission 原子移交给真实 Task。"""
|
||||
close_coroutine = False
|
||||
task: asyncio.Task[Any] | None = None
|
||||
error: Exception | None = None
|
||||
with self._state_lock:
|
||||
current = self._threadsafe_submissions.get(completion)
|
||||
if current is not submission:
|
||||
return
|
||||
if completion.cancelled():
|
||||
self._threadsafe_submissions.pop(completion, None)
|
||||
close_coroutine = True
|
||||
else:
|
||||
try:
|
||||
task = self.create(
|
||||
coroutine,
|
||||
owner=owner,
|
||||
cancel_on_shutdown=cancel_on_shutdown,
|
||||
)
|
||||
except Exception as caught:
|
||||
error = caught
|
||||
close_coroutine = True
|
||||
else:
|
||||
submission.task = task
|
||||
finally:
|
||||
self._threadsafe_submissions.pop(completion, None)
|
||||
|
||||
if close_coroutine:
|
||||
coroutine.close()
|
||||
return
|
||||
try:
|
||||
task = self.create(
|
||||
coroutine,
|
||||
owner=owner,
|
||||
cancel_on_shutdown=cancel_on_shutdown,
|
||||
)
|
||||
except Exception as error:
|
||||
if not completion.done():
|
||||
if error is not None:
|
||||
try:
|
||||
completion.set_exception(error)
|
||||
except concurrent.futures.InvalidStateError:
|
||||
pass
|
||||
loop.call_exception_handler(
|
||||
{
|
||||
"message": "MoviePilot 跨线程后台任务提交失败",
|
||||
@@ -116,7 +153,8 @@ class TaskRegistry:
|
||||
}
|
||||
)
|
||||
return
|
||||
task_holder["task"] = task
|
||||
if task is None:
|
||||
return
|
||||
task.add_done_callback(mirror_completion)
|
||||
if completion.cancelled() and not task.done():
|
||||
task.cancel()
|
||||
@@ -127,7 +165,17 @@ class TaskRegistry:
|
||||
"""调用方取消 completion 时,把取消请求转交目标循环中的真实任务。"""
|
||||
if not submitted.cancelled():
|
||||
return
|
||||
task = task_holder.get("task")
|
||||
close_coroutine = False
|
||||
with self._state_lock:
|
||||
task = submission.task
|
||||
if (
|
||||
task is None
|
||||
and self._threadsafe_submissions.get(submitted) is submission
|
||||
):
|
||||
self._threadsafe_submissions.pop(submitted, None)
|
||||
close_coroutine = True
|
||||
if close_coroutine:
|
||||
coroutine.close()
|
||||
if task is not None and not task.done():
|
||||
try:
|
||||
loop.call_soon_threadsafe(task.cancel)
|
||||
@@ -135,10 +183,20 @@ class TaskRegistry:
|
||||
pass
|
||||
|
||||
completion.add_done_callback(cancel_registered_task)
|
||||
with self._state_lock:
|
||||
if not self._accepting:
|
||||
coroutine.close()
|
||||
raise RuntimeError("后台任务登记器正在关闭,不能再创建新任务")
|
||||
self._threadsafe_submissions[completion] = submission
|
||||
try:
|
||||
loop.call_soon_threadsafe(submit_on_loop)
|
||||
except RuntimeError:
|
||||
coroutine.close()
|
||||
with self._state_lock:
|
||||
close_coroutine = (
|
||||
self._threadsafe_submissions.pop(completion, None) is submission
|
||||
)
|
||||
if close_coroutine:
|
||||
coroutine.close()
|
||||
raise
|
||||
return completion
|
||||
|
||||
@@ -182,7 +240,13 @@ class TaskRegistry:
|
||||
|
||||
async def shutdown(self, *, timeout_seconds: float = 10.0) -> bool:
|
||||
"""停止接收并有限等待存量任务,返回全部 owner 是否真实收敛。"""
|
||||
self._accepting = False
|
||||
with self._state_lock:
|
||||
self._accepting = False
|
||||
pending_submissions = tuple(self._threadsafe_submissions.items())
|
||||
self._threadsafe_submissions.clear()
|
||||
for completion, submission in pending_submissions:
|
||||
submission.coroutine.close()
|
||||
completion.cancel()
|
||||
records = self.records
|
||||
tasks = [record.task for record in records]
|
||||
for record in records:
|
||||
|
||||
Reference in New Issue
Block a user