Document and test handing an error context between threads
The thread-safety section filed two different things under "does not cover, and cannot": sharing a context between threads, and passing one to another thread. Only the first is unsupported. Transfer already works by construction -- the reference count is the only field the library reads across an ownership boundary, and it is only ever touched under the pool lock, so akerr_release_error() does not care which thread checked the slot out. The pool is process-global, not thread-local, so a context outlives the thread that raised it. Calling that unsupported told readers the worker/collector shape was off the table, which either cost them the pattern or cost them the stack trace when they rolled their own struct instead. Split the bullet: transfer joins the covered list and gets its own section with the rule, the worked pattern, and the four receiving-side hazards (PREPARE_ERROR cannot adopt, CATCH assigns over the pointer, FINISH in a void helper still parses its return, and an unhandled error now terminates from the collector's thread). Sharing keeps the "cannot" bullet, narrowed to what it actually is. err_threads_handoff.c proves it: the existing thread tests all keep every context on the thread that raised it, so the transfer path was exercised nowhere. Seven producers hand errors to one collector through a bounded mutex/condvar queue -- the mutex is the thing under test, since it is what publishes the unlocked content writes -- and the collector asserts the context is still a live slot at refcount 1, that message and trace arrive whole and in each producer's order, that the slot was never recycled in flight, and that a thread which never called akerr_next_error() can release it. A second phase reads a context whose raising thread has already exited. Also document why copying a context by assignment is silently wrong: stacktracebufptr is self-referential, so the copy's cursor points into the source's buffer and the first append corrupts a slot the copier no longer owns. TODO.md records the akerr_copy_error() shape that would fix it and the trigger for building it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
19
UPGRADING.md
19
UPGRADING.md
@@ -95,11 +95,20 @@ Safe from any thread, with no coordination on your part:
|
||||
* `akerr_name_for_status(status, NULL)` lookups, concurrently with each other
|
||||
and with registrations of *other* statuses.
|
||||
* `akerr_init()`, from any number of threads at once.
|
||||
* **Handing a context to another thread, and releasing it there.** The reference
|
||||
count is the only field the library reads across threads and it is only ever
|
||||
touched under the pool lock, so `akerr_release_error()` does not care which
|
||||
thread checked the slot out. Contexts live in process-global storage, not
|
||||
thread-local, so one outlives the thread that raised it. What is still yours is
|
||||
the handoff *itself*: it has to carry a happens-before edge, which any mutex,
|
||||
condvar, `pthread_join` or acquire/release atomic gives you.
|
||||
|
||||
Still yours to coordinate:
|
||||
|
||||
* **One error context is owned by one thread.** The library hands it to the
|
||||
thread that raised it. Handing it to another thread is your synchronization.
|
||||
* **Two threads in one context at once.** Ownership moves; it does not fork.
|
||||
Hand a context over and stop touching it — the content is written with no
|
||||
lock, so the handoff is what publishes it. See "Handing an error to another
|
||||
thread" in README.md for the pattern, and for why the queue has to be bounded.
|
||||
* **`akerr_log_method` and `akerr_handler_unhandled_error`** are read on every
|
||||
error and written by nobody but you. Set them during startup, before spawning.
|
||||
* **Renaming a status while another thread looks it up.**
|
||||
@@ -128,9 +137,9 @@ library from that thread.
|
||||
|
||||
## Proving it
|
||||
|
||||
`tests/err_threads_init.c`, `tests/err_threads_pool.c` and
|
||||
`tests/err_threads_registry.c` assert the properties directly and run in the
|
||||
normal suite. The run that proves there is no data race underneath them is
|
||||
`tests/err_threads_init.c`, `tests/err_threads_pool.c`,
|
||||
`tests/err_threads_registry.c` and `tests/err_threads_handoff.c` assert the
|
||||
properties directly and run in the normal suite. The run that proves there is no data race underneath them is
|
||||
ThreadSanitizer:
|
||||
|
||||
```sh
|
||||
|
||||
Reference in New Issue
Block a user