Commit Graph

25 Commits

Author SHA1 Message Date
ddidderr 2bbd2ac869 refactor(peer): adopt structured concurrency with supervised shutdown
Replace the detached tokio::spawn pattern in the peer runtime with a
supervised model built on tokio_util's CancellationToken and TaskTracker.
Long-lived services and child tasks now have an explicit parent, a
cancellation path, and a join point. Tauri can request a clean shutdown
on app exit instead of leaking work into process termination.

Background
~~~~~~~~~~

start_peer() previously returned only a command sender. The four startup
services (QUIC server, mDNS discovery, peer liveness, local library
monitor) and their child tasks (ping workers, handshake jobs, download
workers, announcement fan-outs, connection/stream handlers) were spawned
with raw tokio::spawn and detached. Closing the command channel sent
Goodbye notifications but did not stop those services. The mDNS blocking
worker had no cancellation path at all. Active downloads were stored as
JoinHandle<()> and force-aborted, which could interrupt file writes
mid-chunk.

Supervisor
~~~~~~~~~~

The runtime now owns a CancellationToken and a TaskTracker, threaded
through Ctx and PeerCtx. Each long-lived service is spawned through a
small supervisor (spawn_supervised_service) that wraps the service in
catch_unwind and enforces an explicit SupervisionPolicy:

  QuicServer:    Required     (fatal; cancels the runtime if it dies)
  Discovery:     Restart(5s)  (matches the prior self-restart loop)
  Liveness:      Restart(5s)
  LocalMonitor:  BestEffort   (logs and exits, no restart)

A Required failure emits a new RuntimeFailed { component, error } event
to the UI and cancels the runtime; the command loop and goodbye
notifications still run to completion. The Tauri layer forwards the
event as "peer-runtime-failed" so a future UI can surface it.

mDNS cancellation
~~~~~~~~~~~~~~~~~

MdnsBrowser previously blocked on receiver.recv() forever. It now
exposes next_service_timeout(Duration) returning an MdnsServicePoll
enum (Service/Timeout/Closed) via recv_timeout(). The discovery worker
polls at 250ms and checks the shutdown flag between ticks, so
cancellation reaches the blocking thread within one poll interval
instead of waiting for the next mDNS event.

Downloads
~~~~~~~~~

active_downloads is now HashMap<String, CancellationToken>. Each
download gets a child token of the runtime shutdown, checked at chunk
and peer-attempt boundaries (never inside file writes). When all peers
with a game disappear, liveness cancels the token and emits
DownloadGameFilesAllPeersGone; the download exits Ok(()) without
emitting a duplicate Failed event.

DownloadStateGuard (context.rs) is held inside the download task and
clears downloading_games + active_downloads on Drop, covering the happy
path, error returns, cancellation, and task abort. Drop falls back to
spawning the cleanup if write-lock contention prevents try_write.

Public API and Tauri integration
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

start_peer() now returns PeerRuntimeHandle exposing:

  fn sender(&self) -> UnboundedSender<PeerCommand>
  fn shutdown(&self)
  async fn wait_stopped(&mut self)

The Tauri layer stores the handle in managed state and switches its
main loop from .run(ctx) to .build(ctx).run(|h, e| ...). On
RunEvent::Exit it calls handle.shutdown() and blocks up to 2s on
wait_stopped(), giving services time to cancel and Goodbye packets time
to flush over a healthy LAN while staying short enough not to delay
process exit noticeably on a dead network.

The command loop distinguishes graceful shutdown from unexpected
channel closure: if recv() returns None and shutdown.is_cancelled() is
set, the loop returns Ok(()) silently. Only an unexpected close (no
cancellation observed) still emits RuntimeFailed. This avoids a
spurious failure event on every normal app close.

User-visible behavior changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

- Closing the app no longer leaks services into process termination;
  Goodbye notifications are reliably attempted before exit.
- Downloads cancel cleanly (between chunks) instead of force-aborting
  mid-write.
- A new "peer-runtime-failed" Tauri event fires when a Required service
  cannot recover. No frontend handler exists yet — that is a follow-up.

Tradeoffs
~~~~~~~~~

- Workspace tokio-util now requires the "rt" feature for TaskTracker.
- The mDNS worker still runs in spawn_blocking and may stay parked
  briefly between 250ms polls — acceptable for a desktop app.
- The 2s shutdown timeout on app exit is a deliberate compromise.

Tests
~~~~~

New unit tests:
  - DownloadStateGuard clears tracking on completion, cancellation, and
    parent-task abort (context.rs).
  - Required failure cancels the runtime and emits RuntimeFailed
    (startup.rs).
  - Restart policy restarts until shutdown is requested (startup.rs).
  - PeerRuntimeHandle.shutdown() observable via wait_stopped()
    (startup.rs).
  - Peers-gone cancellation emits only PeersGone, no duplicate Failed
    (services/liveness.rs).

Test plan
~~~~~~~~~

  cargo test --workspace
  cargo clippy --workspace --all-targets

Manual smoke test on two peers on the same LAN:
  1. Start a download, verify chunks transfer.
  2. Close the receiving app mid-download — verify the sending peer
     logs a Goodbye, not a connection-reset error.
  3. Stop the sending peer mid-download — verify the receiver emits
     DownloadGameFilesAllPeersGone, not Failed.

Follow-ups
~~~~~~~~~~

- Frontend handler for "peer-runtime-failed".
- Consider exposing the runtime handle's stopped watch to the frontend
  for a reconnecting indicator on Required failures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 07:53:51 +02:00
ddidderr b4585b663a ChatGPT Codex 5.5 xhigh refactored even more 2026-05-02 15:31:37 +02:00
ddidderr 86d0f93ede asd 2026-02-26 20:12:25 +01:00
ddidderr f9923bd61e feat: Implement length-delimited framing for QUIC stream communication using tokio-util and futures. 2025-11-18 20:39:38 +01:00
ddidderr 833c8afedf game thumbnails 2025-11-14 09:03:05 +01:00
ddidderr 651e3db988 mdns fix: use heuristic to find suitable interface and use IP of that interface to anounce service 2025-11-13 20:35:29 +01:00
ddidderr f2808192f7 wip 2025-11-12 22:22:33 +01:00
ddidderr 82842c15c3 wip 2025-11-08 20:56:35 +01:00
ddidderr 403168a00a [deps] cargo update 2025-11-08 08:58:45 +01:00
ddidderr 32c7659702 [deps] upgrade 2025-08-12 08:37:25 +02:00
ddidderr b8329bd0b1 [feat] use mimalloc 2025-07-11 13:13:14 +02:00
ddidderr d69cf115c8 [client] run as admin 2025-03-20 22:34:34 +01:00
ddidderr 78f7ff2405 [wip] use windows crate to run as admin 2025-03-20 20:57:32 +01:00
ddidderr 19434cd1b1 [deps] cargo update (and a few unused crates for future stuff) 2025-03-20 19:38:35 +01:00
ddidderr bcf9ad68ad [deps] cargo update 2025-03-02 13:05:35 +01:00
ddidderr d1eb185498 [deps] move all deps into main toml and update 2024-12-03 20:49:13 +01:00
ddidderr 2b64d1e4ba [client][server] file transfer working, ui not ready for it 2024-11-14 23:26:31 +01:00
ddidderr a6ed6e04fe [backup] games from server with images 2024-11-13 23:51:28 +01:00
ddidderr 5d45c4ce4b [ui] change framework from Tauri Leptos to Tauri Vanilla (Typescript) and React 2024-11-13 22:15:04 +01:00
ddidderr 1388bc2115 [feat] use eti game.db, commit not working, something is wrong with game.id in the client/frontend 2024-11-12 22:12:49 +01:00
ddidderr 89af1f9176 [omg] GUI, mDNS, list games on client start 2024-11-10 15:11:22 +01:00
ddidderr 9f63ace39c [feat] use clap to pass IP and port 2024-11-08 22:39:02 +01:00
ddidderr 9d8f579a0f [feat][code] proto crate, one stream per request 2024-11-08 22:22:50 +01:00
ddidderr 04a39790b8 [code] restructured into different crates 2024-11-08 10:05:24 +01:00
ddidderr 70e3aaea17 lanspread: Game Distribution on LAN parties (WIP) 2024-09-29 16:16:58 +02:00