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:
- Internally synchronized, fully re-entrant (formalize status quo) —
webservermethods safe from any thread including handlers; matches every peer C++ HTTP library. - Externally synchronized — user holds a mutex; hostile to typical use; contradicts MHD's threading model.
- 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
webserverpublic 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_requestis single-threaded per request.http_responseis exclusively owned (value type).
Verification (TASK-032):
test/integ/threadsafety_stress.cpp— stress test binarythreadsafety_stressruns 16 concurrent curl clients for 60 seconds (override viaHTTPSERVER_STRESS_SECONDS=N), each request randomly invokingregister_path,unregister_path,block_ip, andunblock_ipagainst the runningwebserverfrom inside a handler thread. A duplicate-registration from a handler throwsstd::invalid_argumentrather than causing a data race.- CI coverage: the
build-type: tsanmatrix entry in.github/workflows/verify-build.ymlcompiles with-fsanitize=threadand runsmake check, which automatically picks upthreadsafety_stressas a registeredcheck_PROGRAMSentry — no separate workflow wiring is needed. - Opt-in negative test
stop_from_handler_deadlocks_as_documented(enabled viaHTTPSERVER_RUN_STOP_FROM_HANDLER=1) forks a child process that callsstop()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, alsoWIFSIGNALED). 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 meansstop()returned cleanly, which is also a regression against DR-008.~webserver()carries the same threading constraint asstop()but is not separately exercised because destructor invocation from a running handler is inherently process-unsafe; thestop()test is considered representative.