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>
This commit is contained in:
342
src/error.c
342
src/error.c
@@ -1,15 +1,44 @@
|
||||
#include "akerror.h"
|
||||
#include "lock.h"
|
||||
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#endif // AKERR_USE_STDLIB
|
||||
|
||||
akerr_ErrorContext __akerr_last_ditch;
|
||||
akerr_ErrorContext *__akerr_last_ignored;
|
||||
/*
|
||||
* Per-thread state.
|
||||
*
|
||||
* The last-ditch context is where a failure gets reported when there is no pool
|
||||
* slot to report it from -- akerr_release_error(NULL). One shared copy would
|
||||
* have two threads formatting a message into the same buffer, so each thread
|
||||
* gets its own. Thread-local storage is zero initialized, which is why
|
||||
* akerr_last_ditch_context() below sets the stack-trace cursor lazily rather
|
||||
* than akerr_init() setting it for everyone: akerr_init() runs on one thread
|
||||
* and cannot reach the others' copies.
|
||||
*
|
||||
* It is not small (an akerr_ErrorContext is tens of kilobytes), but the storage
|
||||
* is allocated per thread only when that thread first touches the library's
|
||||
* thread-local block, and the alternative is a shared buffer that two threads
|
||||
* can be writing at once.
|
||||
*/
|
||||
static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch;
|
||||
AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
|
||||
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
||||
akerr_ErrorLogFunction akerr_log_method = NULL;
|
||||
|
||||
/*
|
||||
* One recursive lock over both the error pool and the status registry. See
|
||||
* src/lock.h for why it is one lock, and why it is recursive.
|
||||
*
|
||||
* Everything below that touches either table does so through a function whose
|
||||
* name ends in _locked, called from a wrapper that takes the lock and releases
|
||||
* it on the single return path. The wrappers exist because the locked bodies
|
||||
* are written with the FAIL_*_RETURN macros, which return from the middle of a
|
||||
* function -- so those bodies cannot be the ones holding the lock.
|
||||
*/
|
||||
static akerr_Mutex akerr_state_lock;
|
||||
|
||||
/*
|
||||
* Status-name registry.
|
||||
*
|
||||
@@ -97,6 +126,9 @@ akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
|
||||
* A range test also accepts pointers into the *interior* of an element, which
|
||||
* would then be treated as the head of an akerr_ErrorContext and written
|
||||
* through. Keep this an element-wise scan; it is not a missed optimization.
|
||||
*
|
||||
* Takes no lock: the addresses of the pool slots are fixed for the life of the
|
||||
* process, and nothing here reads a slot's contents.
|
||||
*/
|
||||
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
||||
{
|
||||
@@ -156,77 +188,121 @@ void __akerr_name_library_status(int status, const char *name)
|
||||
}
|
||||
|
||||
/*
|
||||
* Idempotent. `inited` is set before any work so that the registry calls below
|
||||
* -- and the public registry entry points, which all call akerr_init() so that
|
||||
* a consumer reserving its range before anything else touches the library
|
||||
* cannot have that reservation wiped by a later first-use of the pool -- see
|
||||
* themselves as already initialized instead of recursing.
|
||||
* The calling thread's last-ditch context.
|
||||
*
|
||||
* Thread-local storage starts zeroed, so a NULL stack-trace cursor means this
|
||||
* thread has not used its copy yet. Checking the cursor rather than a separate
|
||||
* flag keeps the whole thing self-describing: the cursor is the one field that
|
||||
* must not be zero for the context to be usable at all.
|
||||
*/
|
||||
static akerr_ErrorContext *akerr_last_ditch_context(void)
|
||||
{
|
||||
if ( __akerr_last_ditch.stacktracebufptr == NULL ) {
|
||||
memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext));
|
||||
__akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf;
|
||||
}
|
||||
return &__akerr_last_ditch;
|
||||
}
|
||||
|
||||
static AKERR_THREAD_LOCAL int akerr_initializing;
|
||||
static akerr_Once akerr_state_once = AKERR_ONCE_INIT;
|
||||
|
||||
/*
|
||||
* Runs exactly once per process, under akerr_once(). Everything it calls --
|
||||
* the registry entry points, and the pool underneath them -- calls akerr_init()
|
||||
* itself, so the first thing it does is raise the re-entry flag; see
|
||||
* akerr_init() below.
|
||||
*
|
||||
* The lock is initialized before anything that could take it. That ordering is
|
||||
* the reason this work lives in a once-routine instead of a
|
||||
* check-a-flag-and-go: a second thread arriving while this one is still
|
||||
* populating the tables must block until they are complete, not walk them.
|
||||
*/
|
||||
static void akerr_init_state(void)
|
||||
{
|
||||
akerr_mutex_init(&akerr_state_lock);
|
||||
akerr_initializing = 1;
|
||||
|
||||
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
|
||||
AKERR_ARRAY_ERROR[i].arrayid = i;
|
||||
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
|
||||
}
|
||||
__akerr_last_ignored = NULL;
|
||||
(void)akerr_last_ditch_context();
|
||||
if ( akerr_log_method == NULL ) {
|
||||
akerr_log_method = &akerr_default_logger;
|
||||
}
|
||||
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
|
||||
memset((void *)&akerr_status_names[0], 0x00, sizeof(akerr_status_names));
|
||||
memset((void *)&akerr_status_ranges[0], 0x00, sizeof(akerr_status_ranges));
|
||||
akerr_status_name_count = 0;
|
||||
akerr_status_range_count = 0;
|
||||
|
||||
/* errno and AKERR_* values are the library-owned compatibility band.
|
||||
* This must precede every registration below: naming a status is only
|
||||
* permitted inside a reserved range. Terminal for the same reason as
|
||||
* __akerr_name_library_status(), and handled the same way: without this
|
||||
* band the library owns nothing, so none of the names below could
|
||||
* register either. */
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
|
||||
* list is exhaustive so a new code cannot be added without one. */
|
||||
__akerr_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
__akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
__akerr_name_library_status(AKERR_API, "API Error");
|
||||
__akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error");
|
||||
__akerr_name_library_status(AKERR_TYPE, "Type Error");
|
||||
__akerr_name_library_status(AKERR_KEY, "Key Error");
|
||||
__akerr_name_library_status(AKERR_INDEX, "Index Error");
|
||||
__akerr_name_library_status(AKERR_FORMAT, "Format Error");
|
||||
__akerr_name_library_status(AKERR_IO, "Input Output Error");
|
||||
__akerr_name_library_status(AKERR_VALUE, "Value Error");
|
||||
__akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error");
|
||||
__akerr_name_library_status(AKERR_EOF, "End Of File");
|
||||
__akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
__akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break");
|
||||
__akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented");
|
||||
__akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
|
||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
||||
akerr_init_errno();
|
||||
#endif
|
||||
|
||||
akerr_initializing = 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Idempotent, and safe to call from any thread at any time. Every public entry
|
||||
* point calls it -- so that a consumer reserving its range before anything else
|
||||
* touches the library cannot have that reservation wiped by a later first-use
|
||||
* of the pool -- which means it is also on the path of everything
|
||||
* akerr_init_state() itself calls.
|
||||
*
|
||||
* The re-entry guard is thread local, and has to be: only the thread running
|
||||
* the once-routine may skip past it. A second thread arriving mid-initialization
|
||||
* must block inside akerr_once() until the tables are complete, which a shared
|
||||
* flag set at the top of initialization would have let it walk right past.
|
||||
*/
|
||||
void akerr_init()
|
||||
{
|
||||
static int inited = 0;
|
||||
if ( inited == 0 ) {
|
||||
inited = 1;
|
||||
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
|
||||
AKERR_ARRAY_ERROR[i].arrayid = i;
|
||||
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
|
||||
}
|
||||
__akerr_last_ignored = NULL;
|
||||
memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext));
|
||||
__akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf;
|
||||
if ( akerr_log_method == NULL ) {
|
||||
akerr_log_method = &akerr_default_logger;
|
||||
}
|
||||
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
|
||||
memset((void *)&akerr_status_names[0], 0x00, sizeof(akerr_status_names));
|
||||
memset((void *)&akerr_status_ranges[0], 0x00, sizeof(akerr_status_ranges));
|
||||
akerr_status_name_count = 0;
|
||||
akerr_status_range_count = 0;
|
||||
|
||||
/* errno and AKERR_* values are the library-owned compatibility band.
|
||||
* This must precede every registration below: naming a status is only
|
||||
* permitted inside a reserved range. Terminal for the same reason as
|
||||
* __akerr_name_library_status(), and handled the same way: without this
|
||||
* band the library owns nothing, so none of the names below could
|
||||
* register either. */
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT,
|
||||
AKERR_LIBRARY_OWNER));
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
/* Every AKERR_* code gets a name; tests/err_error_names.c asserts the
|
||||
* list is exhaustive so a new code cannot be added without one. */
|
||||
__akerr_name_library_status(AKERR_NULLPOINTER, "Null Pointer Error");
|
||||
__akerr_name_library_status(AKERR_OUTOFBOUNDS, "Out Of Bounds Error");
|
||||
__akerr_name_library_status(AKERR_API, "API Error");
|
||||
__akerr_name_library_status(AKERR_ATTRIBUTE, "Attribute Error");
|
||||
__akerr_name_library_status(AKERR_TYPE, "Type Error");
|
||||
__akerr_name_library_status(AKERR_KEY, "Key Error");
|
||||
__akerr_name_library_status(AKERR_INDEX, "Index Error");
|
||||
__akerr_name_library_status(AKERR_FORMAT, "Format Error");
|
||||
__akerr_name_library_status(AKERR_IO, "Input Output Error");
|
||||
__akerr_name_library_status(AKERR_VALUE, "Value Error");
|
||||
__akerr_name_library_status(AKERR_RELATIONSHIP, "Relationship Error");
|
||||
__akerr_name_library_status(AKERR_EOF, "End Of File");
|
||||
__akerr_name_library_status(AKERR_CIRCULAR_REFERENCE, "Circular Reference Error");
|
||||
__akerr_name_library_status(AKERR_ITERATOR_BREAK, "Iterator Break");
|
||||
__akerr_name_library_status(AKERR_NOT_IMPLEMENTED, "Not Implemented");
|
||||
__akerr_name_library_status(AKERR_BADEXC, "Invalid akerr_ErrorContext");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_OVERLAP, "Status Range Overlap");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_FULL, "Status Range Table Full");
|
||||
__akerr_name_library_status(AKERR_STATUS_RANGE_INVALID, "Invalid Status Range");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_UNRESERVED, "Unreserved Status Name");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_FOREIGN, "Foreign Status Name");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_FULL, "Status Name Registry Full");
|
||||
__akerr_name_library_status(AKERR_STATUS_NAME_INVALID, "Invalid Status Name");
|
||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
||||
akerr_init_errno();
|
||||
#endif
|
||||
if ( akerr_initializing != 0 ) {
|
||||
return;
|
||||
}
|
||||
akerr_once(&akerr_state_once, &akerr_init_state);
|
||||
}
|
||||
|
||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
||||
@@ -237,23 +313,46 @@ void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
||||
exit(errctx->status);
|
||||
}
|
||||
|
||||
/*
|
||||
* Claim the lowest free slot. Finding it and taking the reference are one
|
||||
* operation under the lock: a scan that returned an unclaimed slot would hand
|
||||
* the same one to every thread that scanned before the first of them got around
|
||||
* to incrementing the count.
|
||||
*/
|
||||
akerr_ErrorContext *akerr_next_error()
|
||||
{
|
||||
akerr_ErrorContext *found = (akerr_ErrorContext *)NULL;
|
||||
|
||||
akerr_init();
|
||||
akerr_mutex_lock(&akerr_state_lock);
|
||||
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
if ( AKERR_ARRAY_ERROR[i].refcount == 0 ) {
|
||||
return &AKERR_ARRAY_ERROR[i];
|
||||
found = &AKERR_ARRAY_ERROR[i];
|
||||
found->refcount = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (akerr_ErrorContext *)NULL;
|
||||
akerr_mutex_unlock(&akerr_state_lock);
|
||||
return found;
|
||||
}
|
||||
|
||||
/*
|
||||
* The wipe returns the slot to the pool, so it and the decrement that triggers
|
||||
* it are one operation under the lock. Otherwise a thread that saw the count
|
||||
* reach zero could be handed the slot by akerr_next_error() and start writing
|
||||
* its error into it while the releasing thread was still memsetting it.
|
||||
*/
|
||||
akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
||||
{
|
||||
int oldid = 0;
|
||||
akerr_ErrorContext *remaining = err;
|
||||
|
||||
akerr_init();
|
||||
if ( err == NULL ) {
|
||||
akerr_ErrorContext *errctx = &__akerr_last_ditch;
|
||||
akerr_ErrorContext *errctx = akerr_last_ditch_context();
|
||||
FAIL_RETURN(errctx, AKERR_NULLPOINTER, "akerr_release_error got NULL context pointer");
|
||||
}
|
||||
akerr_mutex_lock(&akerr_state_lock);
|
||||
if ( err->refcount > 0 ) {
|
||||
err->refcount -= 1;
|
||||
}
|
||||
@@ -262,9 +361,10 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
||||
memset(err, 0x00, sizeof(akerr_ErrorContext));
|
||||
err->stacktracebufptr = (char *)&err->stacktracebuf;
|
||||
err->arrayid = oldid;
|
||||
return NULL;
|
||||
remaining = NULL;
|
||||
}
|
||||
return err;
|
||||
akerr_mutex_unlock(&akerr_state_lock);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
|
||||
@@ -288,7 +388,7 @@ static unsigned akerr_status_hash(int status)
|
||||
/*
|
||||
* Find the slot holding `status`. With create != 0, claim a free slot for it if
|
||||
* it is not present yet. Returns NULL when the status is absent and either no
|
||||
* slot was requested or the registry is full.
|
||||
* slot was requested or the registry is full. Caller holds akerr_state_lock.
|
||||
*
|
||||
* The `& (AKERR_STATUS_NAME_SLOTS - 1)` below is load-bearing and fails
|
||||
* silently: off by one in either direction and the probe indexes past
|
||||
@@ -328,7 +428,8 @@ static akerr_StatusName *akerr_status_slot(int status, int create)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* The reservation covering `status`, or NULL if nobody has claimed it. */
|
||||
/* The reservation covering `status`, or NULL if nobody has claimed it. Caller
|
||||
* holds akerr_state_lock. */
|
||||
static akerr_StatusRange *akerr_range_for_status(int status)
|
||||
{
|
||||
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
||||
@@ -348,10 +449,14 @@ static akerr_StatusRange *akerr_range_for_status(int status)
|
||||
* fails to register degrades into "Unknown Error" in stack traces, which is
|
||||
* exactly the kind of quiet loss this registry exists to prevent -- so the
|
||||
* message carries everything a caller needs to see in a stack trace.
|
||||
*
|
||||
* Caller holds akerr_state_lock. The FAIL_* macros below re-enter the library
|
||||
* to build their error -- a pool slot from akerr_next_error(), and a status
|
||||
* name for the stack trace -- and that re-entry is why the lock is recursive.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner,
|
||||
int status,
|
||||
const char *name)
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name_locked(const char *owner,
|
||||
int status,
|
||||
const char *name)
|
||||
{
|
||||
akerr_StatusRange *range;
|
||||
akerr_StatusName *entry;
|
||||
@@ -394,12 +499,16 @@ static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *ow
|
||||
|
||||
/*
|
||||
* Register a name for a status inside a range the caller reserved. Both strings
|
||||
* are checked here rather than only inside akerr_store_status_name(): the store
|
||||
* accepts a NULL owner for the legacy akerr_name_for_status() path, so a NULL
|
||||
* arriving through *this* entry point would be read as "caller did not identify
|
||||
* itself" and skip the ownership check entirely.
|
||||
* are checked here rather than only inside akerr_store_status_name_locked(): the
|
||||
* store accepts a NULL owner for the legacy akerr_name_for_status() path, so a
|
||||
* NULL arriving through *this* entry point would be read as "caller did not
|
||||
* identify itself" and skip the ownership check entirely.
|
||||
*
|
||||
* Caller holds akerr_state_lock.
|
||||
*/
|
||||
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name_locked(const char *owner,
|
||||
int status,
|
||||
const char *name)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
@@ -410,10 +519,21 @@ akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, co
|
||||
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
||||
"Refusing to name status %d for %s: the name is NULL",
|
||||
status, owner);
|
||||
PASS(errctx, akerr_store_status_name(owner, status, name));
|
||||
PASS(errctx, akerr_store_status_name_locked(owner, status, name));
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, const char *name)
|
||||
{
|
||||
akerr_ErrorContext *errctx;
|
||||
|
||||
akerr_init();
|
||||
akerr_mutex_lock(&akerr_state_lock);
|
||||
errctx = akerr_register_status_name_locked(owner, status, name);
|
||||
akerr_mutex_unlock(&akerr_state_lock);
|
||||
return errctx;
|
||||
}
|
||||
|
||||
/*
|
||||
* Return or set a name. Status magnitude is unrelated to storage size.
|
||||
*
|
||||
@@ -426,16 +546,40 @@ akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, co
|
||||
* The lookup path (name == NULL) deliberately stays clear of all of this. FAIL
|
||||
* calls it to render a status into a stack trace, so it must not itself need an
|
||||
* error context.
|
||||
*
|
||||
* The name is returned by pointer into the registry, which never resizes and
|
||||
* never removes an entry, so the pointer is good for the life of the process.
|
||||
* Its *contents* are stable as long as nobody registers a second name for the
|
||||
* same status: a rename overwrites the buffer in place, and a lookup on another
|
||||
* thread can be reading it. Register names during initialization -- renaming a
|
||||
* live status while other threads run is the one registry operation the lock
|
||||
* cannot make safe, because the reader is outside it by then.
|
||||
*/
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name(const char *owner,
|
||||
int status,
|
||||
const char *name)
|
||||
{
|
||||
akerr_ErrorContext *errctx;
|
||||
|
||||
akerr_mutex_lock(&akerr_state_lock);
|
||||
errctx = akerr_store_status_name_locked(owner, status, name);
|
||||
akerr_mutex_unlock(&akerr_state_lock);
|
||||
return errctx;
|
||||
}
|
||||
|
||||
char *akerr_name_for_status(int status, char *name)
|
||||
{
|
||||
akerr_StatusName *entry;
|
||||
char *found = "Unknown Error";
|
||||
|
||||
akerr_init();
|
||||
if ( name != NULL ) {
|
||||
PREPARE_ERROR(errctx);
|
||||
int refused = 0;
|
||||
|
||||
/* The store takes and releases the lock itself, so the handler below
|
||||
* calls akerr_log_method -- consumer code, which may do anything at all
|
||||
* including calling back into this library -- without holding it. */
|
||||
ATTEMPT {
|
||||
CATCH(errctx, akerr_store_status_name(NULL, status, name));
|
||||
} CLEANUP {
|
||||
@@ -449,15 +593,22 @@ char *akerr_name_for_status(int status, char *name)
|
||||
return "Unknown Error";
|
||||
}
|
||||
}
|
||||
akerr_mutex_lock(&akerr_state_lock);
|
||||
entry = akerr_status_slot(status, 0);
|
||||
if ( entry == NULL ) {
|
||||
return "Unknown Error";
|
||||
if ( entry != NULL ) {
|
||||
found = entry->name;
|
||||
}
|
||||
return entry->name;
|
||||
akerr_mutex_unlock(&akerr_state_lock);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Reserve an inclusive status interval and reject collisions. */
|
||||
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
/* Reserve an inclusive status interval and reject collisions. Caller holds
|
||||
* akerr_state_lock: the overlap scan and the entry that follows it are one
|
||||
* decision, so two threads claiming overlapping ranges at once must not be able
|
||||
* to both find the table clear. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range_locked(int first_status,
|
||||
int count,
|
||||
const char *owner)
|
||||
{
|
||||
int last_status;
|
||||
PREPARE_ERROR(errctx);
|
||||
@@ -510,3 +661,14 @@ akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, cons
|
||||
akerr_status_range_count++;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
|
||||
{
|
||||
akerr_ErrorContext *errctx;
|
||||
|
||||
akerr_init();
|
||||
akerr_mutex_lock(&akerr_state_lock);
|
||||
errctx = akerr_reserve_status_range_locked(first_status, count, owner);
|
||||
akerr_mutex_unlock(&akerr_state_lock);
|
||||
return errctx;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user