fix(deltachat-rpc-client): shutdown all threads after RPC server crash

RPC server subprocess may exit for various reasons,
e.g. because of panic or because of debug_assert! during testing,
or because the process is killed manually or by OOM killer.

We still want to shutdown all threads cleanly in this case
and not fail in Rpc.close(). Otherwise Python process cannot exit
as it waits forever for all threads to terminate.

Without the fix, when added test is run in pytest,
pytest prints "ERROR at teardown of test_shutdown" and gets stuck.
When pytest is manually terminated with ^C, the following error is printed:

```
Exception ignored while joining a thread in _thread._shutdown():
Traceback (most recent call last):
  File "/usr/lib/python3.14/threading.py", line 1583, in _shutdown
    _thread_shutdown()
KeyboardInterrupt:
```

_thread_shutdown() is the function waiting for all non-daemon threads to finish.
This commit is contained in:
link2xt
2026-10-03 16:46:12 +00:00
committed by l
parent 70730490b2
commit 8cedea1958
2 changed files with 17 additions and 1 deletions
@@ -149,7 +149,10 @@ class Rpc:
def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
self.stop_io_for_all_accounts()
# JSON-RPC error may happen if RPC server process has crashed already.
# We still want to shutdown all threads in this case.
with contextlib.suppress(JsonRpcError):
self.stop_io_for_all_accounts()
# Let `events_loop` stop cleanly on `closing` before the pipe goes away,
# otherwise it might exit through an "RPC server closed" error instead.
self.events_thread.join()
@@ -35,6 +35,19 @@ def test_sleep(rpc) -> None:
assert sleep_5_future in pending
def test_shutdown(rpc) -> None:
"""Test RPC client shutdown if RPC server process is terminated.
This is a regression test, at the time of adding it RPC client
did not terminate all threads and python process did not exit.
Shutting down all threads is tested implicitly by pytest-timeout.
"""
rpc.process.kill()
rpc.process.wait()
with pytest.raises(JsonRpcError):
rpc.get_system_info()
def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",