Release ignored error contexts #21
@@ -14,11 +14,12 @@ What that covers:
|
|||||||
against each other and against lookups. Two threads reserving the same range
|
against each other and against lookups. Two threads reserving the same range
|
||||||
cannot both win — exactly one gets `NULL` and the other gets
|
cannot both win — exactly one gets `NULL` and the other gets
|
||||||
`AKERR_STATUS_RANGE_OVERLAP` naming the winner.
|
`AKERR_STATUS_RANGE_OVERLAP` naming the winner.
|
||||||
* **Per-thread state.** `IGNORE` uses `__akerr_last_ignored` as a scratch pointer
|
* **Per-thread state.** `IGNORE` copies the swallowed context into its
|
||||||
while it logs an error, then releases the context and clears the pointer.
|
thread-local `__akerr_last_ignored` snapshot before releasing the pool slot.
|
||||||
That scratch pointer and the last-ditch context used to report
|
The snapshot remains valid until that thread ignores another error, so a
|
||||||
`akerr_release_error(NULL)` are thread-local, so concurrent calls cannot
|
later pool checkout cannot overwrite it. The snapshot and the last-ditch
|
||||||
overwrite each other's state.
|
context used to report `akerr_release_error(NULL)` are thread-local, so
|
||||||
|
concurrent calls cannot overwrite each other's state.
|
||||||
* **Handing a context from one thread to another.** A context is not thread
|
* **Handing a context from one thread to another.** A context is not thread
|
||||||
state — it lives in `AKERR_ARRAY_ERROR`, which is process-global — so it
|
state — it lives in `AKERR_ARRAY_ERROR`, which is process-global — so it
|
||||||
outlives the thread that raised it. The reference count is the only field the
|
outlives the thread that raised it. The reference count is the only field the
|
||||||
|
|||||||
@@ -173,12 +173,12 @@ extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
|||||||
extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
extern akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
||||||
extern akerr_ErrorLogFunction akerr_log_method;
|
extern akerr_ErrorLogFunction akerr_log_method;
|
||||||
/*
|
/*
|
||||||
* IGNORE()'s per-thread scratch pointer. It is non-NULL only while IGNORE()
|
* IGNORE()'s per-thread snapshot. IGNORE() copies the swallowed error here
|
||||||
* logs the swallowed error; IGNORE() releases the context and clears this
|
* before releasing its pool context, so this remains a useful debugging aid
|
||||||
* pointer before returning to its caller. Thread local only when
|
* after the pool slot is reused. The snapshot is read-only and is replaced by
|
||||||
* AKERR_THREAD_SAFE is 1.
|
* the next ignored error. Thread local only when AKERR_THREAD_SAFE is 1.
|
||||||
*/
|
*/
|
||||||
extern AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
|
static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ignored;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Drop one reference, returning NULL once the last one is gone so the caller can
|
* Drop one reference, returning NULL once the last one is gone so the caller can
|
||||||
@@ -435,11 +435,20 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
|
|||||||
FINISH_LOGIC(__err_context, true);
|
FINISH_LOGIC(__err_context, true);
|
||||||
|
|
||||||
#define IGNORE(__stmt) \
|
#define IGNORE(__stmt) \
|
||||||
__akerr_last_ignored = __stmt; \
|
do { \
|
||||||
if ( __akerr_last_ignored != NULL ) { \
|
akerr_ErrorContext *__akerr_ignored = __stmt; \
|
||||||
LOG_ERROR_WITH_MESSAGE(__akerr_last_ignored, "** IGNORED ERROR **"); \
|
if ( __akerr_ignored != NULL ) { \
|
||||||
RELEASE_ERROR(__akerr_last_ignored); \
|
memcpy(&__akerr_last_ignored, __akerr_ignored, \
|
||||||
|
logikoma marked this conversation as resolved
Outdated
|
|||||||
}
|
sizeof(__akerr_last_ignored)); \
|
||||||
|
__akerr_last_ignored.stacktracebufptr = \
|
||||||
|
(char *)&__akerr_last_ignored.stacktracebuf; \
|
||||||
|
akerr_ErrorContext *__akerr_ignored_snapshot = \
|
||||||
|
&__akerr_last_ignored; \
|
||||||
|
LOG_ERROR_WITH_MESSAGE(__akerr_ignored_snapshot, \
|
||||||
|
"** IGNORED ERROR **"); \
|
||||||
|
RELEASE_ERROR(__akerr_ignored); \
|
||||||
|
} \
|
||||||
|
} while ( 0 )
|
||||||
|
|
||||||
#define CLEANUP \
|
#define CLEANUP \
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
* It is not small (an akerr_ErrorContext is tens of kilobytes), but the storage
|
* 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
|
* 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
|
* thread-local block, and the alternative is a shared buffer that two threads
|
||||||
* can be writing at once.
|
* can be writing at once. The per-thread IGNORE() snapshot lives in the public
|
||||||
|
* template header because the macro copies into it at the call site.
|
||||||
*/
|
*/
|
||||||
static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch;
|
static AKERR_THREAD_LOCAL akerr_ErrorContext __akerr_last_ditch;
|
||||||
AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
|
|
||||||
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
akerr_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
||||||
akerr_ErrorLogFunction akerr_log_method = NULL;
|
akerr_ErrorLogFunction akerr_log_method = NULL;
|
||||||
|
|
||||||
@@ -233,7 +233,6 @@ static void akerr_init_state(void)
|
|||||||
AKERR_ARRAY_ERROR[i].arrayid = i;
|
AKERR_ARRAY_ERROR[i].arrayid = i;
|
||||||
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
|
AKERR_ARRAY_ERROR[i].stacktracebufptr = (char *)&AKERR_ARRAY_ERROR[i].stacktracebuf;
|
||||||
}
|
}
|
||||||
__akerr_last_ignored = NULL;
|
|
||||||
(void)akerr_last_ditch_context();
|
(void)akerr_last_ditch_context();
|
||||||
if ( akerr_log_method == NULL ) {
|
if ( akerr_log_method == NULL ) {
|
||||||
akerr_log_method = &akerr_default_logger;
|
akerr_log_method = &akerr_default_logger;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#include "akerror.h"
|
#include "akerror.h"
|
||||||
#include "err_capture.h"
|
#include "err_capture.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
/* IGNORE logs and releases an error, then lets execution continue. */
|
/* IGNORE snapshots and logs an error, releases its pool slot, then continues. */
|
||||||
|
|
||||||
akerr_ErrorContext *boom(void)
|
akerr_ErrorContext *boom(void)
|
||||||
{
|
{
|
||||||
@@ -18,10 +19,14 @@ int main(void)
|
|||||||
(void)e;
|
(void)e;
|
||||||
|
|
||||||
/* More failures than the pool has slots must remain safe: a leaking
|
/* More failures than the pool has slots must remain safe: a leaking
|
||||||
* IGNORE used to exhaust the pool and terminate the process here. */
|
* IGNORE used to exhaust the pool and terminate the process here. The
|
||||||
|
* copied snapshot must also survive the slot being reused on the next
|
||||||
|
* iteration. */
|
||||||
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR + 1; i++ ) {
|
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR + 1; i++ ) {
|
||||||
IGNORE(boom());
|
IGNORE(boom());
|
||||||
AKERR_CHECK(__akerr_last_ignored == NULL);
|
AKERR_CHECK(__akerr_last_ignored.status == AKERR_VALUE);
|
||||||
|
AKERR_CHECK(strcmp(__akerr_last_ignored.message,
|
||||||
|
"this error is ignored on purpose") == 0);
|
||||||
AKERR_CHECK(akerr_slots_in_use() == 0);
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
}
|
}
|
||||||
reached_after_ignore = 1;
|
reached_after_ignore = 1;
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ static void one_checkout(akerr_ThreadArg *arg)
|
|||||||
static void *pool_body(void *raw)
|
static void *pool_body(void *raw)
|
||||||
{
|
{
|
||||||
akerr_ThreadArg *arg = raw;
|
akerr_ThreadArg *arg = raw;
|
||||||
|
char expected[64];
|
||||||
|
|
||||||
|
snprintf(expected, sizeof(expected), "ignored by thread %d", arg->id);
|
||||||
pthread_barrier_wait(arg->barrier);
|
pthread_barrier_wait(arg->barrier);
|
||||||
|
|
||||||
for ( int i = 0; i < ITERATIONS; i++ ) {
|
for ( int i = 0; i < ITERATIONS; i++ ) {
|
||||||
@@ -97,10 +100,21 @@ static void *pool_body(void *raw)
|
|||||||
one_checkout(arg);
|
one_checkout(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* IGNORE's scratch pointer is thread-local while logging and cleared after
|
/* IGNORE's snapshot is thread-local while logging and remains valid after
|
||||||
* release. Concurrent ignored errors must all return their pool slots. */
|
* release. Concurrent ignored errors must all return their pool slots. */
|
||||||
IGNORE(ignorable(arg));
|
IGNORE(ignorable(arg));
|
||||||
AKERR_TCHECK(arg, __akerr_last_ignored == NULL);
|
AKERR_TCHECK(arg, __akerr_last_ignored.status == AKERR_IO);
|
||||||
|
AKERR_TCHECK(arg, strcmp(__akerr_last_ignored.message, expected) == 0);
|
||||||
|
|
||||||
|
/* Reuse a slot after IGNORE and prove that the copied snapshot did not
|
||||||
|
* become an alias for the newly acquired context. */
|
||||||
|
akerr_ErrorContext *reused = akerr_next_error();
|
||||||
|
AKERR_TCHECK(arg, reused != NULL);
|
||||||
|
if ( reused != NULL ) {
|
||||||
|
RELEASE_ERROR(reused);
|
||||||
|
}
|
||||||
|
AKERR_TCHECK(arg, __akerr_last_ignored.status == AKERR_IO);
|
||||||
|
AKERR_TCHECK(arg, strcmp(__akerr_last_ignored.message, expected) == 0);
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
andrew marked this conversation as resolved
Outdated
andrew
commented
IGNORE() retained a reference to the last ignored error on purpose, so that subsequent errors that may be related could reference it. Instead of completely abandoning it (and the pattern that allows it), why not turn __akerr_last_ignored into a static variable, IGNORE() does a IGNORE() retained a reference to the last ignored error on purpose, so that subsequent errors that may be related could reference it. Instead of completely abandoning it (and the pattern that allows it), why not turn __akerr_last_ignored into a static variable, IGNORE() does a `memcpy()` into it from the exception being ignored, then releases the exception? That seems like it would give us the best of both worlds.
andrew
commented
@logikoma ^ implement this > IGNORE() retained a reference to the last ignored error on purpose, so that subsequent errors that may be related could reference it. Instead of completely abandoning it (and the pattern that allows it), why not turn __akerr_last_ignored into a static variable, IGNORE() does a `memcpy()` into it from the exception being ignored, then releases the exception? That seems like it would give us the best of both worlds.
@logikoma ^ implement this
|
|||||||
|
|||||||
@logikoma the
__akerr_last_ignoredvariable is only really useful to users of the public facing API, so I'm not sure it makes sense to prefix it with__as part of the private API. Let's remove the__prefix on this variable and make it public.