139 lines
4.7 KiB
C
139 lines
4.7 KiB
C
|
|
#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;
|
||
|
|
}
|