mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
refactor: own cross-thread background tasks
This commit is contained in:
+3
-2
@@ -13,8 +13,8 @@
|
||||
"runtime_to_db": [],
|
||||
"workflow_to_db": []
|
||||
},
|
||||
"edge_count": 6539,
|
||||
"edge_sha256": "28c7ab8544ede290e6dacf5f126ca1aad95e09627d874a0d23c6476108a88e1b",
|
||||
"edge_count": 6540,
|
||||
"edge_sha256": "3beb7c6b906993a8ff9d0cb9b3f7c912e1a7859785824f472547b8fecf8a4c51",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -3017,6 +3017,7 @@
|
||||
"app.chain._transfer -> app.runtime",
|
||||
"app.chain._transfer -> app.runtime.config",
|
||||
"app.chain._transfer -> app.runtime.log",
|
||||
"app.chain._transfer -> app.runtime.tasks",
|
||||
"app.chain._transfer -> app.schemas",
|
||||
"app.chain._transfer -> app.schemas.history",
|
||||
"app.chain._transfer -> app.schemas.message",
|
||||
|
||||
@@ -34,6 +34,7 @@ def test_owner_gate_tracks_known_registry_without_matching_same_named_methods(
|
||||
task_registry.create(work())
|
||||
resolve_registry(task_registry).create_sync(work, owner=dynamic_owner)
|
||||
get_task_registry().register(task, owner=" ")
|
||||
task_registry.submit_threadsafe(work(), loop=loop)
|
||||
""",
|
||||
)
|
||||
|
||||
@@ -41,11 +42,13 @@ def test_owner_gate_tracks_known_registry_without_matching_same_named_methods(
|
||||
"create",
|
||||
"create_sync",
|
||||
"register",
|
||||
"submit_threadsafe",
|
||||
]
|
||||
assert [violation.reason for violation in violations] == [
|
||||
"缺少显式 owner",
|
||||
"的 owner 必须是非空字符串字面量",
|
||||
"的 owner 必须是非空字符串字面量",
|
||||
"缺少显式 owner",
|
||||
]
|
||||
|
||||
|
||||
@@ -65,6 +68,11 @@ def test_owner_gate_accepts_aliases_and_stable_literal_owners(tmp_path: Path) ->
|
||||
task,
|
||||
owner="api.example.existing",
|
||||
)
|
||||
task_registry.submit_threadsafe(
|
||||
work(),
|
||||
loop=loop,
|
||||
owner="api.example.threadsafe",
|
||||
)
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@@ -87,6 +87,74 @@ def test_task_registry_runs_sync_function_and_tracks_until_completion() -> None:
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_task_registry_owns_threadsafe_submission_until_shutdown() -> None:
|
||||
"""宿主线程提交的协程应先登记 owner,并由 Registry 关停取消和等待。"""
|
||||
|
||||
async def scenario() -> None:
|
||||
registry = TaskRegistry()
|
||||
loop = asyncio.get_running_loop()
|
||||
started = asyncio.Event()
|
||||
cleaned = asyncio.Event()
|
||||
|
||||
async def worker() -> None:
|
||||
"""保持运行直到 Registry 发出取消,并记录清理已完成。"""
|
||||
started.set()
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
finally:
|
||||
cleaned.set()
|
||||
|
||||
completion = await asyncio.to_thread(
|
||||
registry.submit_threadsafe,
|
||||
worker(),
|
||||
loop=loop,
|
||||
owner="test.threadsafe",
|
||||
)
|
||||
await asyncio.wait_for(started.wait(), timeout=1)
|
||||
assert [record.owner for record in registry.records] == [
|
||||
"test.threadsafe"
|
||||
]
|
||||
|
||||
assert await registry.shutdown(timeout_seconds=1.0) is True
|
||||
assert cleaned.is_set()
|
||||
assert completion.cancelled()
|
||||
assert registry.records == ()
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_task_registry_rejects_threadsafe_submission_after_shutdown() -> None:
|
||||
"""关停先赢得竞态时应关闭协程并通过 completion 报告拒绝原因。"""
|
||||
|
||||
async def scenario() -> None:
|
||||
registry = TaskRegistry()
|
||||
loop = asyncio.get_running_loop()
|
||||
reports: list[dict[str, object]] = []
|
||||
previous_handler = loop.get_exception_handler()
|
||||
loop.set_exception_handler(lambda _, context: reports.append(context))
|
||||
|
||||
async def late_worker() -> None:
|
||||
"""模拟关停完成后从宿主线程到达的晚任务。"""
|
||||
|
||||
try:
|
||||
assert await registry.shutdown(timeout_seconds=1.0) is True
|
||||
completion = await asyncio.to_thread(
|
||||
registry.submit_threadsafe,
|
||||
late_worker(),
|
||||
loop=loop,
|
||||
owner="test.threadsafe-late",
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="正在关闭"):
|
||||
await asyncio.wrap_future(completion)
|
||||
|
||||
assert registry.records == ()
|
||||
assert reports[-1]["owner"] == "test.threadsafe-late"
|
||||
finally:
|
||||
loop.set_exception_handler(previous_handler)
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_task_registry_keeps_timed_out_sync_owner_until_real_completion() -> None:
|
||||
"""同步线程超过关停预算后仍应保留 owner,不能把包装任务取消成伪完成。"""
|
||||
|
||||
|
||||
@@ -141,10 +141,10 @@ class TestTransferFailedRetryButtons(unittest.TestCase):
|
||||
) as history_oper_cls, patch(
|
||||
"app.chain._transfer.build_manual_redo_prompt",
|
||||
return_value="retry transfer prompt",
|
||||
), patch(
|
||||
"app.chain._transfer.asyncio.run_coroutine_threadsafe",
|
||||
side_effect=_close_pending_coro,
|
||||
) as run_task:
|
||||
), patch("app.chain._transfer.get_task_registry") as get_registry:
|
||||
get_registry.return_value.submit_threadsafe.side_effect = (
|
||||
_close_pending_coro
|
||||
)
|
||||
history_oper_cls.return_value.get.return_value = history
|
||||
with patch.object(chain, "post_message") as post_message:
|
||||
chain.handle_failed_transfer_callback(
|
||||
@@ -155,7 +155,11 @@ class TestTransferFailedRetryButtons(unittest.TestCase):
|
||||
username="tester",
|
||||
)
|
||||
|
||||
run_task.assert_called_once()
|
||||
get_registry.return_value.submit_threadsafe.assert_called_once()
|
||||
self.assertEqual(
|
||||
get_registry.return_value.submit_threadsafe.call_args.kwargs["owner"],
|
||||
"chain.transfer.ai_takeover",
|
||||
)
|
||||
self.assertEqual(post_message.call_count, 1)
|
||||
self.assertEqual(
|
||||
post_message.call_args_list[0].args[0].title,
|
||||
@@ -224,10 +228,10 @@ class TestTransferFailedRetryButtons(unittest.TestCase):
|
||||
), patch(
|
||||
"app.chain._transfer.get_running_agent_manager",
|
||||
return_value=manager,
|
||||
), patch(
|
||||
"app.chain._transfer.asyncio.run_coroutine_threadsafe",
|
||||
side_effect=_run_pending_coro,
|
||||
):
|
||||
), patch("app.chain._transfer.get_task_registry") as get_registry:
|
||||
get_registry.return_value.submit_threadsafe.side_effect = (
|
||||
_run_pending_coro
|
||||
)
|
||||
history_oper_cls.return_value.get.return_value = history
|
||||
with patch.object(chain, "post_message"), patch.object(
|
||||
chain, "async_post_message", side_effect=fake_async_post_message
|
||||
|
||||
Reference in New Issue
Block a user