fix(tui_gateway): withdraw a server request when its wait is interrupted
The settlement rewrite dropped main's try/finally around event.wait(): a KeyboardInterrupt/SystemExit/injected error during the wait left the request in _open forever — replayed to every reconnecting client as a zombie card and reported by pending_kind() as a human still being waited on. Pop it under _lock and emit request.cancel(interrupted) before re-raising. Also fold the two settlement-wins tests into one parametrized test (same invariant, two settlement paths).
This commit is contained in:
@@ -313,18 +313,25 @@ def test_server_request_round_trip_uses_response_frame(capture):
|
||||
assert not server_requests._open
|
||||
|
||||
|
||||
def test_response_settlement_wins_over_a_later_cancel(capture):
|
||||
@pytest.mark.parametrize("method, qids, settle, expected", [
|
||||
("sudo", None,
|
||||
lambda sr, req: sr.resolve_response({"id": req.id, "result": {"value": "yes"}}) is True,
|
||||
{"value": "yes"}),
|
||||
# Batch clarify's lock-based resolution follows the same first-settlement rule.
|
||||
("clarify", ["q1"], lambda sr, req: sr.lock_answer(req.id, "q1", "yes") == [], {"answers": {"q1": "yes"}}),
|
||||
])
|
||||
def test_settlement_wins_over_a_later_cancel(capture, method, qids, settle, expected):
|
||||
"""A response and cancellation may race; the first settlement owns the result."""
|
||||
from tui_gateway import server_requests
|
||||
|
||||
req = server_requests.ServerRequest("s1", "sudo", {})
|
||||
req = server_requests.ServerRequest("s1", method, {}, qids=qids)
|
||||
with server_requests._lock:
|
||||
server_requests._open[req.id] = req
|
||||
|
||||
assert server_requests.resolve_response({"id": req.id, "result": {"value": "yes"}})
|
||||
assert settle(server_requests, req)
|
||||
assert server_requests.cancel("s1") == 0
|
||||
assert req.answered is True
|
||||
assert req.result == {"value": "yes"}
|
||||
assert req.result == expected
|
||||
assert req.event.is_set()
|
||||
|
||||
|
||||
@@ -353,21 +360,6 @@ def test_send_returns_an_answer_committed_after_the_deadline_expired(capture, mo
|
||||
assert not server_requests._open
|
||||
|
||||
|
||||
def test_final_clarify_lock_settlement_wins_over_a_later_cancel(capture):
|
||||
"""Batch clarify's lock-based resolution follows the same first-settlement rule."""
|
||||
from tui_gateway import server_requests
|
||||
|
||||
req = server_requests.ServerRequest("s1", "clarify", {}, qids=["q1"])
|
||||
with server_requests._lock:
|
||||
server_requests._open[req.id] = req
|
||||
|
||||
assert server_requests.lock_answer(req.id, "q1", "yes") == []
|
||||
assert server_requests.cancel("s1") == 0
|
||||
assert req.answered is True
|
||||
assert req.result == {"answers": {"q1": "yes"}}
|
||||
assert req.event.is_set()
|
||||
|
||||
|
||||
def test_server_request_error_response_fails_fast(capture):
|
||||
"""A shared-channel client without a handler answers -32601 instead of waiting for the deadline."""
|
||||
from tui_gateway import server_requests
|
||||
|
||||
@@ -109,7 +109,17 @@ def send(method: str, sid: str, params: dict, *, timeout: float | None,
|
||||
"""
|
||||
req = ServerRequest(sid, method, params, qids=qids)
|
||||
_register(req)
|
||||
req.event.wait(timeout)
|
||||
try:
|
||||
req.event.wait(timeout)
|
||||
except BaseException:
|
||||
# The wait itself died (KeyboardInterrupt, SystemExit, injected error): withdraw the request
|
||||
# or it stays in _open forever — replayed to every reconnecting client and reported by
|
||||
# pending_kind() as a human still being waited on.
|
||||
with _lock:
|
||||
still_open = _open.pop(req.id, None) is req
|
||||
if still_open:
|
||||
_emit_cancel(req, "interrupted")
|
||||
raise
|
||||
with _lock:
|
||||
# The verdict is the state committed under the lock, never wait()'s return value: a
|
||||
# response frame can land after the deadline expires and before this removal, and
|
||||
|
||||
Reference in New Issue
Block a user