Skip to content

Latest commit

 

History

History
27 lines (20 loc) · 2.91 KB

File metadata and controls

27 lines (20 loc) · 2.91 KB

DR-008: Thread-safety contract

Status: Accepted Date: 2026-04-30 Context: v1's threading semantics are implicit (mutexes exist but contract isn't documented). v2.0 should make the contract explicit.

Options considered:

  1. Internally synchronized, fully re-entrant (formalize status quo) — webserver methods safe from any thread including handlers; matches every peer C++ HTTP library.
  2. Externally synchronized — user holds a mutex; hostile to typical use; contradicts MHD's threading model.
  3. Lifecycle-phased (config phase / running phase) — locks become unnecessary post-start; breaks dynamic-route use cases.

Decision: Option 1.

Rationale: Already what the code does; documenting it is zero-risk. Every peer library takes the same position. Option 2 is hostile; Option 3 trades real flexibility for speculative perf.

Consequences:

  • All webserver public methods documented as thread-safe and re-entrant from handlers, with two exceptions: stop() and ~webserver() (deadlock from inside a handler — they wait for the calling thread).
  • Handlers run concurrently on MHD worker threads. User-side state in handlers must be user-synchronized.
  • http_request is single-threaded per request.
  • http_response is exclusively owned (value type).

Verification (TASK-032):

  • test/integ/threadsafety_stress.cpp — stress test binary threadsafety_stress runs 16 concurrent curl clients for 60 seconds (override via HTTPSERVER_STRESS_SECONDS=N), each request randomly invoking register_path, unregister_path, block_ip, and unblock_ip against the running webserver from inside a handler thread. A duplicate-registration from a handler throws std::invalid_argument rather than causing a data race.
  • CI coverage: the build-type: tsan matrix entry in .github/workflows/verify-build.yml compiles with -fsanitize=thread and runs make check, which automatically picks up threadsafety_stress as a registered check_PROGRAMS entry — no separate workflow wiring is needed.
  • Opt-in negative test stop_from_handler_deadlocks_as_documented (enabled via HTTPSERVER_RUN_STOP_FROM_HANDLER=1) forks a child process that calls stop() from inside a handler. Positive observations of the contract are signal-terminated (libmicrohttpd self-join abort, WIFSIGNALED) or SIGKILL-from-parent after a 5-second timeout (silent deadlock, also WIFSIGNALED). Two sentinel exit codes mark regressions: the unreachable-after-stop() line inside the child (stop() returned from a handler — regression) and the post-curl line (stop() did not block at all — regression). A zero child exit means stop() returned cleanly, which is also a regression against DR-008. ~webserver() carries the same threading constraint as stop() but is not separately exercised because destructor invocation from a running handler is inherently process-unsafe; the stop() test is considered representative.