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> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
37
TODO.md
37
TODO.md
@@ -112,6 +112,43 @@ can be configured with `-DAKERR_SANITIZE=thread`. The whole run then costs a
|
||||
TSan-instrumented suite per mutant (roughly 6s instead of 0.4s), so it belongs
|
||||
behind a flag rather than in the default target or in CI.
|
||||
|
||||
## 9. No way to keep an error context and report it at the same time
|
||||
|
||||
A context can be handed to another thread and released there -- `README.md` now
|
||||
documents that pattern and `tests/err_threads_handoff.c` proves it -- but it is a
|
||||
*move*. A thread that wants to both keep its error and report it upward has to
|
||||
read the fields out into its own record, and it loses the stack trace doing so,
|
||||
because `stacktracebuf` is the one thing that cannot be usefully summarized.
|
||||
|
||||
Copying the struct is not a workaround. `stacktracebufptr` is self-referential
|
||||
(`include/akerror.tmpl.h`), so `akerr_ErrorContext c = *src;` leaves the copy's
|
||||
cursor pointing into the source's buffer -- the copy logs correctly and then
|
||||
corrupts a slot it does not own the first time anything appends to it. `arrayid`
|
||||
is restored after the wipe in `akerr_release_error()` (`src/error.c:395-398`), so
|
||||
a copied id makes the destination impersonate the source's slot for the life of
|
||||
the process.
|
||||
|
||||
If this is ever worth an API, the shape is:
|
||||
|
||||
```c
|
||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_copy_error(akerr_ErrorContext *source,
|
||||
akerr_ErrorContext *destination);
|
||||
```
|
||||
|
||||
taking the destination as a parameter rather than allocating it. An allocating
|
||||
copy could fail on pool exhaustion, and reporting *that* failure needs a pool
|
||||
slot, so it would have to abort -- adding a third `exit()` site to a library that
|
||||
deliberately has two. Caller-allocates puts the pool pressure where it can be
|
||||
managed. The copy must repair four fields: `arrayid` (the destination's own),
|
||||
`refcount` (set to 1, never inherited), `handled` (false -- a copy is a fresh
|
||||
obligation, or `FINISH_NORETURN` on the receiving side drops it silently), and
|
||||
`stacktracebufptr` (re-anchored to the destination's buffer at the *same offset*,
|
||||
so a later append continues the trace instead of overwriting it).
|
||||
|
||||
Not worth building yet: no consumer needs it. The trigger is a consumer that
|
||||
needs a worker's stack *trace*, not just its status and message, at the join
|
||||
point -- `libakstdlib`'s planned `pthread_*` wrappers are the likely first.
|
||||
|
||||
## Unrelated pre-existing issues
|
||||
|
||||
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
||||
|
||||
Reference in New Issue
Block a user