Fix blocking sync I/O on the event loop across containers, jobs, and xmlrpc
DevPlace CI / test (push) Failing after 26m53s

The container/workspace reachability probes (socket connect + HTTP check)
ran synchronously with real timeouts inside async request handlers and the
live-view relay's 3-4s ticks, freezing the whole event loop whenever a
container wasn't cleanly reachable - the likely cause of the periodic
app-wide stalls. Converted the probe chain (api._port_reachable/_http_probe,
editor_reachable, instance_runtime, provision.editor_ready/view) to real
async I/O and parallelized the admin container/workspace list decorators.

Also fixes: XmlrpcService.on_disable blocked up to 10s on a synchronous
subprocess.wait inside an async method (now matches TelegramService's
async-subprocess pattern); JobService._sweep_expired ran every job kind's
cleanup() - including shutil.rmtree on large directories - synchronously
on every tick, now offloaded via asyncio.to_thread for all job kinds at
once; and several smaller blocking reads/writes on request/service paths
(attachment-to-gitea mirroring, stealth chunked downloads, dbapi/isslop
file reads, job payload/report I/O) moved off the loop thread.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 12:59:10 +00:00
co-authored by Claude Sonnet 5
parent cc969aa187
commit 4a13415b43
23 changed files with 132 additions and 104 deletions
@@ -1,5 +1,6 @@
# retoor <retoor@molodetz.nl>
import asyncio
import itertools
import socket
@@ -117,7 +118,7 @@ def test_transitional_phases_are_exactly_starting_and_stopping():
def test_editor_ready_when_the_editor_port_accepts_connections(listener):
port = listener.getsockname()[1]
row = _instance(container_ip="127.0.0.1", editor_port=port)
assert provision.editor_ready(row) is True
assert asyncio.run(provision.editor_ready(row)) is True
def test_editor_ready_is_false_when_nothing_listens():
@@ -126,12 +127,12 @@ def test_editor_ready_is_false_when_nothing_listens():
port = probe.getsockname()[1]
probe.close()
row = _instance(container_ip="127.0.0.1", editor_port=port)
assert provision.editor_ready(row) is False
assert asyncio.run(provision.editor_ready(row)) is False
def test_editor_ready_is_false_without_a_reachable_target(listener):
row = _instance(container_ip="", ports_json="[]")
assert provision.editor_ready(row) is False
assert asyncio.run(provision.editor_ready(row)) is False
def test_editor_ready_is_false_unless_the_container_runs(listener):
@@ -140,7 +141,7 @@ def test_editor_ready_is_false_unless_the_container_runs(listener):
if status == store.ST_RUNNING:
continue
row = _instance(container_ip="127.0.0.1", editor_port=port, status=status)
assert provision.editor_ready(row) is False
assert asyncio.run(provision.editor_ready(row)) is False
def test_editor_ready_is_false_while_suspended(listener):
@@ -148,7 +149,7 @@ def test_editor_ready_is_false_while_suspended(listener):
row = _instance(
container_ip="127.0.0.1", editor_port=port, suspended_at="2026-01-01T00:00:00"
)
assert provision.editor_ready(row) is False
assert asyncio.run(provision.editor_ready(row)) is False
def test_view_carries_the_phase_its_label_readiness_and_the_owner(listener):
@@ -166,13 +167,13 @@ def test_view_carries_the_phase_its_label_readiness_and_the_owner(listener):
"ports_json": "[]",
}
)
view = provision.view(instance)
view = asyncio.run(provision.view(instance))
assert view["phase"] == provision.PHASE_READY
assert view["phase_label"] == "Ready"
assert view["editor_ready"] is True
assert view["owner_uid"] == OWNER
store.update_instance(instance["uid"], {"desired_state": store.DESIRED_STOPPED})
view = provision.view(store.get_instance(instance["uid"]))
view = asyncio.run(provision.view(store.get_instance(instance["uid"])))
assert view["phase"] == provision.PHASE_STOPPING
assert view["editor_ready"] is True