Files
libakerror/tests/err_release_refcount.c

74 lines
2.7 KiB
C
Raw Normal View History

#include "akerror.h"
#include "err_capture.h"
#include <string.h>
/*
* akerr_release_error() drops one reference and only recycles the slot when the
* last one goes away. Two of its refcount edges had no test:
*
* - refcount > 1: the release must decrement and return the context intact,
* not wipe it out from under the reference that is still held.
* - refcount == 0: releasing a context nobody holds must not underflow the
* count; it wipes and returns NULL like any other fully-released slot.
*
Make the error pool and status registry thread safe Every entry point may now be called from any thread. akerr_init() runs exactly once however many threads race into it, the pool hands each slot to exactly one thread, and reservations, registrations and lookups are serialized against each other. One recursive lock covers both tables (src/lock.h, private). Recursive because raising an error re-enters the library -- FAIL needs a pool slot and a status name -- and single because two locks would mean an ordering to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN macros are split into *_locked functions behind wrappers that take and release the lock on one path; consumer callbacks are never called under it. This is an ABI break, hence 2.0.0 and SOVERSION 2: - akerr_next_error() now returns a context that already holds its reference. Finding a free slot and claiming it has to be one operation, or two threads scanning at once are handed the same slot. ENSURE_ERROR_READY no longer increments. - __akerr_last_ignored is thread-local, as is the last-ditch context used to report akerr_release_error(NULL). The threading backend is chosen at configure time by AKERR_THREADS (auto, pthread, none). auto fails the configure when it cannot find POSIX threads rather than quietly building a library that reports itself thread safe and is not. generrno.sh stamps the decision into the generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree with the library about it. Tests: err_threads_init, err_threads_pool and err_threads_registry assert exclusive slot ownership, exactly one winner for a contested range, and every registered name readable back under contention. AKERR_SANITIZE builds the library and the tests with any sanitizer; scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs it. Removing the pool lock makes both the sanitizer and the plain assertions fail, so the tests are not vacuous. Documented in README.md and UPGRADING.md, including what this does not cover: renaming a status while another thread looks it up, and which of two simultaneous unhandled errors sets the exit status. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
* The macro API never produces a refcount above 1 (akerr_next_error() takes the
* one reference a fresh slot gets), so this test sets the count directly to
* model a caller that took an extra reference, and clears it to model a slot
* nobody holds.
*/
int main(void)
{
akerr_capture_install();
akerr_init();
AKERR_CHECK(akerr_slots_in_use() == 0);
/* Check out a slot and give it two holders. */
akerr_ErrorContext *held = akerr_next_error();
AKERR_CHECK(held != NULL);
int slotid = held->arrayid;
held->refcount = 2;
held->status = AKERR_VALUE;
snprintf((char *)held->message, AKERR_MAX_ERROR_CONTEXT_STRING_LENGTH,
"still referenced");
/* First release: one holder left, so the context survives untouched. */
akerr_ErrorContext *ret = akerr_release_error(held);
AKERR_CHECK(ret == held);
AKERR_CHECK(held->refcount == 1);
AKERR_CHECK(held->status == AKERR_VALUE);
AKERR_CHECK(strcmp(held->message, "still referenced") == 0);
AKERR_CHECK(akerr_slots_in_use() == 1);
/* Second release: last holder gone, so the slot is wiped and recycled. */
ret = akerr_release_error(held);
AKERR_CHECK(ret == NULL);
AKERR_CHECK(held->refcount == 0);
AKERR_CHECK(held->status == 0);
AKERR_CHECK(held->message[0] == '\0');
/* The wipe must preserve the slot's identity and stacktrace cursor. */
AKERR_CHECK(held->arrayid == slotid);
AKERR_CHECK(held->stacktracebufptr == (char *)&held->stacktracebuf);
AKERR_CHECK(akerr_slots_in_use() == 0);
/*
* Releasing an unheld slot: refcount is already 0, so there is nothing to
* decrement and the count must not go negative.
*/
akerr_ErrorContext *unheld = akerr_next_error();
AKERR_CHECK(unheld != NULL);
Make the error pool and status registry thread safe Every entry point may now be called from any thread. akerr_init() runs exactly once however many threads race into it, the pool hands each slot to exactly one thread, and reservations, registrations and lookups are serialized against each other. One recursive lock covers both tables (src/lock.h, private). Recursive because raising an error re-enters the library -- FAIL needs a pool slot and a status name -- and single because two locks would mean an ordering to get wrong. Registry bodies that use the early-returning FAIL_*_RETURN macros are split into *_locked functions behind wrappers that take and release the lock on one path; consumer callbacks are never called under it. This is an ABI break, hence 2.0.0 and SOVERSION 2: - akerr_next_error() now returns a context that already holds its reference. Finding a free slot and claiming it has to be one operation, or two threads scanning at once are handed the same slot. ENSURE_ERROR_READY no longer increments. - __akerr_last_ignored is thread-local, as is the last-ditch context used to report akerr_release_error(NULL). The threading backend is chosen at configure time by AKERR_THREADS (auto, pthread, none). auto fails the configure when it cannot find POSIX threads rather than quietly building a library that reports itself thread safe and is not. generrno.sh stamps the decision into the generated header as AKERR_THREAD_SAFE, so a consumer cannot disagree with the library about it. Tests: err_threads_init, err_threads_pool and err_threads_registry assert exclusive slot ownership, exactly one winner for a contested range, and every registered name readable back under contention. AKERR_SANITIZE builds the library and the tests with any sanitizer; scripts/thread_test.sh runs the suite under ThreadSanitizer and CI runs it. Removing the pool lock makes both the sanitizer and the plain assertions fail, so the tests are not vacuous. Documented in README.md and UPGRADING.md, including what this does not cover: renaming a status while another thread looks it up, and which of two simultaneous unhandled errors sets the exit status. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 08:31:22 -04:00
AKERR_CHECK(unheld->refcount == 1);
unheld->refcount = 0;
ret = akerr_release_error(unheld);
AKERR_CHECK(ret == NULL);
AKERR_CHECK(unheld->refcount == 0);
AKERR_CHECK(akerr_slots_in_use() == 0);
/* The pool is still healthy afterwards. */
akerr_ErrorContext *probe = akerr_next_error();
AKERR_CHECK(probe != NULL);
fprintf(stderr, "err_release_refcount ok\n");
return 0;
}