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:
@@ -15,11 +15,12 @@ int main(void)
|
||||
|
||||
akerr_ErrorContext *slots[AKERR_MAX_ARRAY_ERROR];
|
||||
|
||||
/* Check out every slot. */
|
||||
/* Check out every slot. Each arrives holding its own reference, so the
|
||||
* next request cannot be handed the same one. */
|
||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
slots[i] = akerr_next_error();
|
||||
AKERR_CHECK(slots[i] != NULL);
|
||||
slots[i]->refcount = 1;
|
||||
AKERR_CHECK(slots[i]->refcount == 1);
|
||||
}
|
||||
|
||||
/* Pool is fully exhausted: the next request must fail cleanly. */
|
||||
|
||||
@@ -11,9 +11,10 @@
|
||||
* - refcount == 0: releasing a context nobody holds must not underflow the
|
||||
* count; it wipes and returns NULL like any other fully-released slot.
|
||||
*
|
||||
* The macro API never produces a refcount above 1 (ENSURE_ERROR_READY only
|
||||
* increments when it acquires a fresh slot), so this test sets the count
|
||||
* directly to model a caller that took an extra reference.
|
||||
* 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)
|
||||
@@ -56,7 +57,8 @@ int main(void)
|
||||
*/
|
||||
akerr_ErrorContext *unheld = akerr_next_error();
|
||||
AKERR_CHECK(unheld != NULL);
|
||||
AKERR_CHECK(unheld->refcount == 0);
|
||||
AKERR_CHECK(unheld->refcount == 1);
|
||||
unheld->refcount = 0;
|
||||
ret = akerr_release_error(unheld);
|
||||
AKERR_CHECK(ret == NULL);
|
||||
AKERR_CHECK(unheld->refcount == 0);
|
||||
|
||||
149
tests/err_threads.h
Normal file
149
tests/err_threads.h
Normal file
@@ -0,0 +1,149 @@
|
||||
#ifndef AKERR_TEST_THREADS_H
|
||||
#define AKERR_TEST_THREADS_H
|
||||
|
||||
/*
|
||||
* Shared helpers for the thread-safety tests.
|
||||
*
|
||||
* These tests are checkable on their own -- they assert exclusive ownership of
|
||||
* pool slots and of reserved ranges, which is a property, not a symptom -- but
|
||||
* the run that proves the absence of a data race is the one under
|
||||
* ThreadSanitizer:
|
||||
*
|
||||
* cmake -S . -B build/tsan -DAKERR_SANITIZE=thread
|
||||
* cmake --build build/tsan
|
||||
* ctest --test-dir build/tsan --output-on-failure
|
||||
*
|
||||
* Everything shared between the threads here is either read-only after the
|
||||
* threads start, or touched through __atomic builtins. Anything else would be a
|
||||
* race in the *test*, and TSan cannot tell whose bug it is reporting.
|
||||
*
|
||||
* A test body runs on AKERR_TEST_THREADS threads that meet at a barrier first,
|
||||
* so they arrive at the library together instead of in start-up order. Failed
|
||||
* checks are counted per thread rather than returned early: a thread that
|
||||
* abandoned its work would leave the others holding pool slots and turn one
|
||||
* failure into a cascade of unrelated ones.
|
||||
*/
|
||||
|
||||
#include "akerror.h"
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#define AKERR_TEST_THREADS 8
|
||||
|
||||
typedef struct
|
||||
{
|
||||
int id; /* 1-based: 0 means "no thread" below */
|
||||
int failures;
|
||||
pthread_barrier_t *barrier;
|
||||
} akerr_ThreadArg;
|
||||
|
||||
#define AKERR_TCHECK(__arg, __cond) \
|
||||
do { \
|
||||
if ( !(__cond) ) { \
|
||||
fprintf(stderr, "CHECK FAILED (thread %d): %s at %s:%d\n", \
|
||||
(__arg)->id, #__cond, __FILE__, __LINE__); \
|
||||
(__arg)->failures += 1; \
|
||||
} \
|
||||
} while ( 0 )
|
||||
|
||||
/*
|
||||
* A logger that counts instead of printing. The capturing logger in
|
||||
* err_capture.h appends to a shared buffer with a shared length, which is a
|
||||
* data race the moment two threads log at once; these tests need a logger that
|
||||
* is safe to install before spawning and still shows that something was
|
||||
* reported.
|
||||
*/
|
||||
static int akerr_thread_log_count;
|
||||
|
||||
static void __attribute__((unused)) akerr_thread_logger(const char *fmt, ...)
|
||||
{
|
||||
(void)fmt;
|
||||
__atomic_fetch_add(&akerr_thread_log_count, 1, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
static int __attribute__((unused)) akerr_thread_logs(void)
|
||||
{
|
||||
return __atomic_load_n(&akerr_thread_log_count, __ATOMIC_RELAXED);
|
||||
}
|
||||
|
||||
/*
|
||||
* Independent bookkeeping of who holds which pool slot. The library's own
|
||||
* refcount says a slot is checked out; this says which thread it was checked
|
||||
* out to, which is the part a racing akerr_next_error() would get wrong by
|
||||
* handing one slot to two threads at once.
|
||||
*/
|
||||
static int akerr_slot_owner[AKERR_MAX_ARRAY_ERROR];
|
||||
|
||||
/* Returns 0 on success, or the id of the thread that already holds the slot. */
|
||||
static int __attribute__((unused)) akerr_slot_claim(int slot, int id)
|
||||
{
|
||||
int unowned = 0;
|
||||
|
||||
if ( __atomic_compare_exchange_n(&akerr_slot_owner[slot], &unowned, id, 0,
|
||||
__ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE) ) {
|
||||
return 0;
|
||||
}
|
||||
return unowned;
|
||||
}
|
||||
|
||||
static int __attribute__((unused)) akerr_slot_holder(int slot)
|
||||
{
|
||||
return __atomic_load_n(&akerr_slot_owner[slot], __ATOMIC_ACQUIRE);
|
||||
}
|
||||
|
||||
/*
|
||||
* Give the slot up *before* releasing the error context. The other order hands
|
||||
* the slot back to the pool while this thread still claims it, and the next
|
||||
* thread to be given it reports a violation that is the test's fault.
|
||||
*/
|
||||
static void __attribute__((unused)) akerr_slot_drop(int slot)
|
||||
{
|
||||
__atomic_store_n(&akerr_slot_owner[slot], 0, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
/*
|
||||
* Run `body` on AKERR_TEST_THREADS threads and return the total number of
|
||||
* failed checks. A thread that cannot be created is itself a failure, but the
|
||||
* ones already running still get joined.
|
||||
*/
|
||||
static int __attribute__((unused)) akerr_run_threads(void *(*body)(void *))
|
||||
{
|
||||
pthread_t threads[AKERR_TEST_THREADS];
|
||||
akerr_ThreadArg args[AKERR_TEST_THREADS];
|
||||
pthread_barrier_t barrier;
|
||||
int started = 0;
|
||||
int failures = 0;
|
||||
|
||||
if ( pthread_barrier_init(&barrier, NULL, AKERR_TEST_THREADS) != 0 ) {
|
||||
fprintf(stderr, "CHECK FAILED: pthread_barrier_init at %s:%d\n",
|
||||
__FILE__, __LINE__);
|
||||
return 1;
|
||||
}
|
||||
for ( int i = 0; i < AKERR_TEST_THREADS; i++ ) {
|
||||
args[i].id = i + 1;
|
||||
args[i].failures = 0;
|
||||
args[i].barrier = &barrier;
|
||||
if ( pthread_create(&threads[i], NULL, body, &args[i]) != 0 ) {
|
||||
fprintf(stderr, "CHECK FAILED: pthread_create for thread %d"
|
||||
" at %s:%d\n", i + 1, __FILE__, __LINE__);
|
||||
failures++;
|
||||
break;
|
||||
}
|
||||
started++;
|
||||
}
|
||||
/* An unstarted thread never reaches the barrier, so the started ones would
|
||||
* wait for it forever. Nothing to do but say so before hanging is diagnosed
|
||||
* as a deadlock in the library. */
|
||||
if ( started != AKERR_TEST_THREADS ) {
|
||||
fprintf(stderr, "only %d of %d threads started; the barrier will not"
|
||||
" release\n", started, AKERR_TEST_THREADS);
|
||||
}
|
||||
for ( int i = 0; i < started; i++ ) {
|
||||
pthread_join(threads[i], NULL);
|
||||
failures += args[i].failures;
|
||||
}
|
||||
pthread_barrier_destroy(&barrier);
|
||||
return failures;
|
||||
}
|
||||
|
||||
#endif // AKERR_TEST_THREADS_H
|
||||
124
tests/err_threads_init.c
Normal file
124
tests/err_threads_init.c
Normal file
@@ -0,0 +1,124 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include "err_threads.h"
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* akerr_init() runs exactly once, no matter how many threads reach the library
|
||||
* at the same instant.
|
||||
*
|
||||
* This is the hardest of the three to get right, because initialization is
|
||||
* re-entrant: akerr_init() reserves the library's own status band and names its
|
||||
* own codes, and every one of those calls goes through a public entry point
|
||||
* that calls akerr_init() again. The guard against that recursion has to be
|
||||
* per-thread, or a second thread arriving mid-initialization would see the flag
|
||||
* the first thread raised on its way in and walk tables that are still being
|
||||
* built.
|
||||
*
|
||||
* Nothing in main() touches the library before the threads start, so the race
|
||||
* is real: whichever thread wins does the initializing, and the rest must block
|
||||
* until it is finished rather than proceed on half-built tables.
|
||||
*
|
||||
* What proves it ran once rather than several times: each thread reserves its
|
||||
* own status range as its first act. A second pass through akerr_init() would
|
||||
* memset the range table, so a reservation made by a thread that raced ahead
|
||||
* would silently vanish -- exactly the failure the pre-1.0.0 library had. After
|
||||
* the join, every thread's reservation must still be there, still attributed to
|
||||
* that thread.
|
||||
*/
|
||||
|
||||
static int thread_range_base(int id)
|
||||
{
|
||||
return 400000 + (id * 16);
|
||||
}
|
||||
|
||||
static void *init_racer(void *raw)
|
||||
{
|
||||
akerr_ThreadArg *arg = raw;
|
||||
akerr_ErrorContext *e;
|
||||
char owner[32];
|
||||
char name[32];
|
||||
int base = thread_range_base(arg->id);
|
||||
|
||||
snprintf(owner, sizeof(owner), "init-thread-%d", arg->id);
|
||||
snprintf(name, sizeof(name), "Thread %d Error", arg->id);
|
||||
pthread_barrier_wait(arg->barrier);
|
||||
|
||||
/* First touch of the library from this thread, and for one of them the
|
||||
* first touch in the process. */
|
||||
e = akerr_reserve_status_range(base, 16, owner);
|
||||
AKERR_TCHECK(arg, e == NULL);
|
||||
RELEASE_ERROR(e);
|
||||
|
||||
e = akerr_register_status_name(owner, base, name);
|
||||
AKERR_TCHECK(arg, e == NULL);
|
||||
RELEASE_ERROR(e);
|
||||
|
||||
/* The library's own band was reserved by whichever thread initialized, and
|
||||
* every thread must see it as taken -- including the one that did it. A
|
||||
* second initialization would have wiped the reservation and let this
|
||||
* through. */
|
||||
e = akerr_reserve_status_range(0, AKERR_RESERVED_STATUS_COUNT, owner);
|
||||
AKERR_TCHECK(arg, e != NULL);
|
||||
if ( e != NULL ) {
|
||||
AKERR_TCHECK(arg, e->status == AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_TCHECK(arg, strstr(e->message, AKERR_LIBRARY_OWNER) != NULL);
|
||||
}
|
||||
RELEASE_ERROR(e);
|
||||
|
||||
/* The name tables are complete as seen from every thread: the library's own
|
||||
* codes, the generated errno names, and this thread's own registration. */
|
||||
AKERR_TCHECK(arg, strcmp(akerr_name_for_status(AKERR_VALUE, NULL),
|
||||
"Value Error") == 0);
|
||||
AKERR_TCHECK(arg, strcmp(akerr_name_for_status(AKERR_NULLPOINTER, NULL),
|
||||
"Null Pointer Error") == 0);
|
||||
AKERR_TCHECK(arg, strcmp(akerr_name_for_status(EACCES, NULL),
|
||||
"Unknown Error") != 0);
|
||||
AKERR_TCHECK(arg, strcmp(akerr_name_for_status(base, NULL), name) == 0);
|
||||
|
||||
/* And an error raised from this thread renders with a name, which is the
|
||||
* whole point of the tables being complete. */
|
||||
PREPARE_ERROR(errctx);
|
||||
ATTEMPT {
|
||||
FAIL_BREAK(errctx, AKERR_TYPE, "raised during init race by thread %d",
|
||||
arg->id);
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE(errctx, AKERR_TYPE) {
|
||||
AKERR_TCHECK(arg, strstr(errctx->stacktracebuf, "Type Error") != NULL);
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* Installed before anything initializes: akerr_init() only supplies the
|
||||
* default logger when this is still NULL. */
|
||||
akerr_log_method = &akerr_thread_logger;
|
||||
|
||||
int failures = akerr_run_threads(&init_racer);
|
||||
AKERR_CHECK(failures == 0);
|
||||
|
||||
/* Every thread's reservation survived the race, under its own owner. */
|
||||
for ( int id = 1; id <= AKERR_TEST_THREADS; id++ ) {
|
||||
char owner[32];
|
||||
char name[32];
|
||||
int base = thread_range_base(id);
|
||||
|
||||
snprintf(owner, sizeof(owner), "init-thread-%d", id);
|
||||
snprintf(name, sizeof(name), "Thread %d Error", id);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(base, NULL), name) == 0);
|
||||
AKERR_CHECK_RAISES(akerr_reserve_status_range(base, 16, "verifier"),
|
||||
AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_CHECK_MESSAGE_CONTAINS(owner);
|
||||
}
|
||||
|
||||
/* Nothing leaked a pool slot on the way through. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_threads_init ok (%d threads raced initialization)\n",
|
||||
AKERR_TEST_THREADS);
|
||||
return 0;
|
||||
}
|
||||
138
tests/err_threads_pool.c
Normal file
138
tests/err_threads_pool.c
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include "err_threads.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* The error pool under contention.
|
||||
*
|
||||
* AKERR_ARRAY_ERROR is a fixed 128-slot array shared by every thread in the
|
||||
* process, and a slot is checked out by finding refcount == 0 and taking a
|
||||
* reference. Those two steps have to be one operation: a scan that returned an
|
||||
* unclaimed slot would hand the same one to every thread that scanned before
|
||||
* the first of them incremented the count, and each would then format its own
|
||||
* error into the same buffers. That failure is invisible to a single-threaded
|
||||
* test and produces a garbled message rather than a crash, so this test asserts
|
||||
* exclusivity directly.
|
||||
*
|
||||
* akerr_slot_owner[] (see err_threads.h) is the test's own record of who holds
|
||||
* which slot, kept with atomics. Every check-out claims its slot and every
|
||||
* release drops the claim; a slot handed to two threads at once is caught by
|
||||
* the claim failing, whether or not the resulting message is garbled.
|
||||
*
|
||||
* Each thread also asserts that the error it raised is the error it handles,
|
||||
* message and all. That is the same property from the other end: a context
|
||||
* cannot be exclusively ours if another thread's text shows up in it.
|
||||
*/
|
||||
|
||||
#define ITERATIONS 2000
|
||||
|
||||
/* Raise an error and take ownership of whatever slot it came from. */
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *boom(akerr_ThreadArg *arg)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
FAIL(e, AKERR_VALUE, "raised by thread %d", arg->id);
|
||||
AKERR_TCHECK(arg, akerr_slot_claim(e->arrayid, arg->id) == 0);
|
||||
return e;
|
||||
}
|
||||
|
||||
static akerr_ErrorContext AKERR_NOIGNORE *ignorable(akerr_ThreadArg *arg)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
FAIL_RETURN(e, AKERR_IO, "ignored by thread %d", arg->id);
|
||||
}
|
||||
|
||||
/* One raise -> catch -> handle cycle, exclusively owned from end to end. */
|
||||
static void one_cycle(akerr_ThreadArg *arg)
|
||||
{
|
||||
char expected[64];
|
||||
PREPARE_ERROR(e);
|
||||
|
||||
snprintf(expected, sizeof(expected), "raised by thread %d", arg->id);
|
||||
ATTEMPT {
|
||||
CATCH(e, boom(arg));
|
||||
} CLEANUP {
|
||||
} PROCESS(e) {
|
||||
/* case 0: the error we just raised came back clean, which can only
|
||||
* mean another thread wrote over this context. */
|
||||
int error_was_lost = 1;
|
||||
AKERR_TCHECK(arg, error_was_lost == 0);
|
||||
} HANDLE(e, AKERR_VALUE) {
|
||||
AKERR_TCHECK(arg, akerr_slot_holder(e->arrayid) == arg->id);
|
||||
AKERR_TCHECK(arg, strcmp(e->message, expected) == 0);
|
||||
AKERR_TCHECK(arg, strstr(e->stacktracebuf, expected) != NULL);
|
||||
/* Give the slot up before FINISH releases the context: the other order
|
||||
* hands it back to the pool while this thread still claims it. */
|
||||
akerr_slot_drop(e->arrayid);
|
||||
} FINISH_NORETURN(e);
|
||||
}
|
||||
|
||||
/* The same property against the raw pool API, with no macros in between. */
|
||||
static void one_checkout(akerr_ThreadArg *arg)
|
||||
{
|
||||
akerr_ErrorContext *e = akerr_next_error();
|
||||
|
||||
AKERR_TCHECK(arg, e != NULL);
|
||||
if ( e == NULL ) {
|
||||
return;
|
||||
}
|
||||
/* The context arrives already holding its reference. */
|
||||
AKERR_TCHECK(arg, e->refcount == 1);
|
||||
AKERR_TCHECK(arg, akerr_slot_claim(e->arrayid, arg->id) == 0);
|
||||
AKERR_TCHECK(arg, akerr_slot_holder(e->arrayid) == arg->id);
|
||||
akerr_slot_drop(e->arrayid);
|
||||
RELEASE_ERROR(e);
|
||||
AKERR_TCHECK(arg, e == NULL);
|
||||
}
|
||||
|
||||
static void *pool_body(void *raw)
|
||||
{
|
||||
akerr_ThreadArg *arg = raw;
|
||||
char expected[64];
|
||||
|
||||
snprintf(expected, sizeof(expected), "ignored by thread %d", arg->id);
|
||||
pthread_barrier_wait(arg->barrier);
|
||||
|
||||
for ( int i = 0; i < ITERATIONS; i++ ) {
|
||||
one_cycle(arg);
|
||||
one_checkout(arg);
|
||||
}
|
||||
|
||||
/* An ignored error is a fact about the thread that ignored it: each thread
|
||||
* must see its own, not the last one any thread swallowed. */
|
||||
IGNORE(ignorable(arg));
|
||||
AKERR_TCHECK(arg, __akerr_last_ignored != NULL);
|
||||
if ( __akerr_last_ignored != NULL ) {
|
||||
AKERR_TCHECK(arg, __akerr_last_ignored->status == AKERR_IO);
|
||||
AKERR_TCHECK(arg, strcmp(__akerr_last_ignored->message, expected) == 0);
|
||||
}
|
||||
/* IGNORE keeps the reference by design; hand it back so the pool is empty
|
||||
* at the end of the test. */
|
||||
RELEASE_ERROR(__akerr_last_ignored);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_log_method = &akerr_thread_logger;
|
||||
akerr_init();
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
int failures = akerr_run_threads(&pool_body);
|
||||
AKERR_CHECK(failures == 0);
|
||||
|
||||
/* Every context went back to the pool, and every claim was dropped. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||
AKERR_CHECK(akerr_slot_holder(i) == 0);
|
||||
}
|
||||
/* Each thread's IGNORE reported through the log method. */
|
||||
AKERR_CHECK(akerr_thread_logs() >= AKERR_TEST_THREADS);
|
||||
|
||||
fprintf(stderr, "err_threads_pool ok (%d threads x %d cycles)\n",
|
||||
AKERR_TEST_THREADS, ITERATIONS);
|
||||
return 0;
|
||||
}
|
||||
137
tests/err_threads_registry.c
Normal file
137
tests/err_threads_registry.c
Normal file
@@ -0,0 +1,137 @@
|
||||
#include "akerror.h"
|
||||
#include "err_capture.h"
|
||||
#include "err_threads.h"
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* The status registry under contention.
|
||||
*
|
||||
* Two properties, and they fail differently:
|
||||
*
|
||||
* - A reservation is a decision, not a write. The overlap scan and the entry
|
||||
* that follows it have to be one operation, or two threads claiming the
|
||||
* same range both find the table clear and both believe they own it. That
|
||||
* is silent: neither gets an error, and the collision surfaces much later
|
||||
* as one component's status rendering under another's name. The contested
|
||||
* range below is claimed by every thread at once and exactly one may win.
|
||||
*
|
||||
* - The name table is an open-addressed hash table with linear probing. A
|
||||
* concurrent insert that another thread's probe walks through -- an entry
|
||||
* half claimed, a count incremented before the slot was marked used -- loses
|
||||
* names or writes outside the table. So every thread registers a block of
|
||||
* names and reads each one back while the others are still writing, and
|
||||
* interleaves lookups of a name nobody is touching.
|
||||
*
|
||||
* Ownership enforcement has to hold under contention too: after every thread
|
||||
* has reserved, each one tries to name a status inside its neighbour's range
|
||||
* and must be refused. The barrier before that is what makes the expected
|
||||
* refusal exactly AKERR_STATUS_NAME_FOREIGN rather than sometimes
|
||||
* AKERR_STATUS_NAME_UNRESERVED, which is a real distinction and not just test
|
||||
* tidiness: FOREIGN means the registry knew who owned it.
|
||||
*/
|
||||
|
||||
#define NAMES_PER_THREAD 64
|
||||
#define CONTESTED_FIRST 900000
|
||||
#define CONTESTED_COUNT 64
|
||||
|
||||
static int contested_winners;
|
||||
|
||||
static int thread_range_base(int id)
|
||||
{
|
||||
return 500000 + (id * 1000);
|
||||
}
|
||||
|
||||
static void *registry_body(void *raw)
|
||||
{
|
||||
akerr_ThreadArg *arg = raw;
|
||||
akerr_ErrorContext *e;
|
||||
char owner[32];
|
||||
int base = thread_range_base(arg->id);
|
||||
int victim = thread_range_base((arg->id % AKERR_TEST_THREADS) + 1);
|
||||
|
||||
snprintf(owner, sizeof(owner), "registry-%d", arg->id);
|
||||
pthread_barrier_wait(arg->barrier);
|
||||
|
||||
/* One range, every thread, distinct owners. Exactly one may come back
|
||||
* successful; the rest must be told who won. */
|
||||
e = akerr_reserve_status_range(CONTESTED_FIRST, CONTESTED_COUNT, owner);
|
||||
if ( e == NULL ) {
|
||||
__atomic_fetch_add(&contested_winners, 1, __ATOMIC_RELAXED);
|
||||
} else {
|
||||
AKERR_TCHECK(arg, e->status == AKERR_STATUS_RANGE_OVERLAP);
|
||||
AKERR_TCHECK(arg, strstr(e->message, "registry-") != NULL);
|
||||
RELEASE_ERROR(e);
|
||||
}
|
||||
|
||||
/* This thread's own range, which nobody contests. */
|
||||
e = akerr_reserve_status_range(base, NAMES_PER_THREAD, owner);
|
||||
AKERR_TCHECK(arg, e == NULL);
|
||||
RELEASE_ERROR(e);
|
||||
|
||||
for ( int i = 0; i < NAMES_PER_THREAD; i++ ) {
|
||||
char name[48];
|
||||
|
||||
snprintf(name, sizeof(name), "registry-%d name %d", arg->id, i);
|
||||
e = akerr_register_status_name(owner, base + i, name);
|
||||
AKERR_TCHECK(arg, e == NULL);
|
||||
RELEASE_ERROR(e);
|
||||
/* Read it back while the other threads are still inserting. */
|
||||
AKERR_TCHECK(arg, strcmp(akerr_name_for_status(base + i, NULL), name) == 0);
|
||||
/* And an entry nobody is touching: a probe sequence that a concurrent
|
||||
* insert walked off loses names that were already there. */
|
||||
AKERR_TCHECK(arg, strcmp(akerr_name_for_status(AKERR_VALUE, NULL),
|
||||
"Value Error") == 0);
|
||||
}
|
||||
|
||||
/* Everyone has reserved by the time anyone tries to trespass. */
|
||||
pthread_barrier_wait(arg->barrier);
|
||||
|
||||
e = akerr_register_status_name(owner, victim, "Hijack");
|
||||
AKERR_TCHECK(arg, e != NULL);
|
||||
if ( e != NULL ) {
|
||||
AKERR_TCHECK(arg, e->status == AKERR_STATUS_NAME_FOREIGN);
|
||||
}
|
||||
RELEASE_ERROR(e);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
akerr_log_method = &akerr_thread_logger;
|
||||
akerr_init();
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
int failures = akerr_run_threads(®istry_body);
|
||||
AKERR_CHECK(failures == 0);
|
||||
|
||||
/* The contested range went to exactly one owner. */
|
||||
AKERR_CHECK(contested_winners == 1);
|
||||
|
||||
/* Every name every thread registered is present and correct: nothing was
|
||||
* dropped, overwritten, or attributed to the wrong thread. */
|
||||
for ( int id = 1; id <= AKERR_TEST_THREADS; id++ ) {
|
||||
int base = thread_range_base(id);
|
||||
|
||||
for ( int i = 0; i < NAMES_PER_THREAD; i++ ) {
|
||||
char expected[48];
|
||||
|
||||
snprintf(expected, sizeof(expected), "registry-%d name %d", id, i);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(base + i, NULL), expected) == 0);
|
||||
}
|
||||
/* The trespass attempt did not leave a name behind. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(base, NULL), "Hijack") != 0);
|
||||
}
|
||||
|
||||
/* The library's own entries survived every one of those inserts. */
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_VALUE, NULL), "Value Error") == 0);
|
||||
AKERR_CHECK(strcmp(akerr_name_for_status(AKERR_STATUS_NAME_FOREIGN, NULL),
|
||||
"Foreign Status Name") == 0);
|
||||
|
||||
/* Thousands of refusals and registrations later, the pool is empty. */
|
||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||
|
||||
fprintf(stderr, "err_threads_registry ok (%d threads x %d names)\n",
|
||||
AKERR_TEST_THREADS, NAMES_PER_THREAD);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user