Compare commits
7 Commits
status-cod
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
5695061130
|
|||
|
9bf8bcd357
|
|||
|
caef63826f
|
|||
|
076b80c846
|
|||
|
5eaa956f50
|
|||
|
756933c600
|
|||
|
be24f80022
|
@@ -67,6 +67,34 @@ jobs:
|
|||||||
fail_on_failure: 'false'
|
fail_on_failure: 'false'
|
||||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
|
thread_sanitizer:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y cmake gcc moreutils
|
||||||
|
# The thread tests assert exclusive ownership of pool slots and reserved
|
||||||
|
# ranges, which is checkable without tooling and runs in the job above.
|
||||||
|
# This is the run that proves there is no data race underneath them.
|
||||||
|
# libtsan arrives with gcc (libgcc-N-dev depends on it); the script
|
||||||
|
# disables ASLR because TSan aborts on kernels with vm.mmap_rnd_bits > 28.
|
||||||
|
- name: thread sanitizer
|
||||||
|
run: |
|
||||||
|
scripts/thread_test.sh build/tsan --output-junit "$(pwd)/tsan-junit.xml"
|
||||||
|
- name: publish thread sanitizer results
|
||||||
|
if: always()
|
||||||
|
uses: mikepenz/action-junit-report@v4
|
||||||
|
with:
|
||||||
|
report_paths: 'tsan-junit.xml'
|
||||||
|
annotate_only: true
|
||||||
|
detailed_summary: true
|
||||||
|
include_passed: true
|
||||||
|
fail_on_failure: 'true'
|
||||||
|
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||||
|
|
||||||
mutation_test:
|
mutation_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
78
AGENTS.md
78
AGENTS.md
@@ -3,11 +3,20 @@
|
|||||||
## Project Structure & Module Organization
|
## Project Structure & Module Organization
|
||||||
|
|
||||||
This is a small C library built with CMake. Core implementation lives in
|
This is a small C library built with CMake. Core implementation lives in
|
||||||
`src/error.c`. The public header is generated at build time from
|
`src/error.c`. `src/lock.h` is private: it selects the threading backend
|
||||||
`include/akerror.tmpl.h` by `scripts/generrno.sh`, which also generates
|
(pthread or none) and is not installed. The public header is generated at build
|
||||||
`src/errno.c` under the build directory. CMake package and pkg-config templates
|
time from `include/akerror.tmpl.h` by `scripts/generrno.sh`, which also
|
||||||
are in `cmake/` and `akerror.pc.in`. Tests are one-file C programs in `tests/`;
|
generates `src/errno.c` under the build directory and stamps in whether the
|
||||||
shared test helpers live beside them, such as `tests/err_capture.h`.
|
build is thread safe. CMake package and pkg-config templates are in `cmake/` and
|
||||||
|
`akerror.pc.in`. Tests are one-file C programs in `tests/`; shared test helpers
|
||||||
|
live beside them, such as `tests/err_capture.h` and `tests/err_threads.h`.
|
||||||
|
|
||||||
|
Prose documentation lives in `docs/`, one file per topic — `architecture.md`,
|
||||||
|
`usage.md`, `status-codes.md`, `uncaught-errors.md`, `exit-status.md`,
|
||||||
|
`thread-safety.md`, `building.md`. `README.md` is deliberately short: summary,
|
||||||
|
installation, quickstart, and an index into `docs/`. Add new documentation to
|
||||||
|
the `docs/` file that owns the topic and link it from the README's index rather
|
||||||
|
than growing the README back.
|
||||||
|
|
||||||
## Build, Test, and Development Commands
|
## Build, Test, and Development Commands
|
||||||
|
|
||||||
@@ -27,6 +36,23 @@ runs the registered unit tests. To emit CI-style results, use:
|
|||||||
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The suite includes thread tests. The run that proves there is no data race under
|
||||||
|
them is ThreadSanitizer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/thread_test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
which configures `build/tsan` with `-DAKERR_SANITIZE=thread`, builds the library
|
||||||
|
*and* the tests with it, and runs CTest with ASLR disabled (TSan aborts with an
|
||||||
|
"unexpected memory mapping" on kernels with `vm.mmap_rnd_bits` above 28).
|
||||||
|
`AKERR_SANITIZE` takes any sanitizer list, so `-DAKERR_SANITIZE=address,undefined`
|
||||||
|
works the same way. CTest gives each test `halt_on_error=1`, so a report fails
|
||||||
|
the test rather than being printed and passed over — running a sanitized test
|
||||||
|
binary by hand does **not** inherit that. Set it yourself
|
||||||
|
(`TSAN_OPTIONS=halt_on_error=1 ./build/tsan/test_err_threads_pool`) or the
|
||||||
|
binary can print a race and still exit 0.
|
||||||
|
|
||||||
Mutation testing is available through:
|
Mutation testing is available through:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -44,9 +70,13 @@ scripts/coverage.py --threshold 90 --branch-threshold 50
|
|||||||
`scripts/coverage.py` configures its own instrumented build tree (default
|
`scripts/coverage.py` configures its own instrumented build tree (default
|
||||||
`build/coverage`, `-DAKERR_COVERAGE=ON`), runs the CTest suite there, and
|
`build/coverage`, `-DAKERR_COVERAGE=ON`), runs the CTest suite there, and
|
||||||
reports gcov line/branch coverage per library source. Thresholds gate each
|
reports gcov line/branch coverage per library source. Thresholds gate each
|
||||||
file as well as the total. Coverage measures `src/error.c` and the generated
|
file as well as the total. Coverage measures the library's own sources only --
|
||||||
`src/errno.c` only; the public header's macros expand at their call sites, so
|
`src/error.c`, `src/lock.h` and the generated `src/errno.c`; the public header's
|
||||||
mutation testing is what checks those.
|
macros expand at their call sites, so mutation testing is what checks those.
|
||||||
|
|
||||||
|
To build without threads (no locking, no thread-local storage, thread tests not
|
||||||
|
registered), configure with `-DAKERR_THREADS=none`. `auto` is the default and
|
||||||
|
fails the configure rather than falling back.
|
||||||
|
|
||||||
## Coding Style & Naming Conventions
|
## Coding Style & Naming Conventions
|
||||||
|
|
||||||
@@ -56,6 +86,25 @@ such as `PREPARE_ERROR`. Keep generated-code changes in templates or generator
|
|||||||
scripts, not in build outputs. Preserve concise comments for invariants,
|
scripts, not in build outputs. Preserve concise comments for invariants,
|
||||||
macro constraints, and non-obvious error lifecycle behavior.
|
macro constraints, and non-obvious error lifecycle behavior.
|
||||||
|
|
||||||
|
**Locking.** One recursive lock (`akerr_state_lock`, see `src/lock.h`) covers
|
||||||
|
both the error pool and the status registry. A function named `*_locked` is
|
||||||
|
called with that lock already held; a function without the suffix takes it.
|
||||||
|
Never take it in a body written with the `FAIL_*_RETURN` macros — those return
|
||||||
|
from the middle of the function and would skip the unlock. Split it instead: the
|
||||||
|
locked body does the work, and a thin wrapper takes the lock, calls it, and
|
||||||
|
releases it on the single return path. Do not call consumer code
|
||||||
|
(`akerr_log_method`, `akerr_handler_unhandled_error`) while holding it.
|
||||||
|
|
||||||
|
**Exiting.** Never call `exit()` with a status value. Call `akerr_exit()`, which
|
||||||
|
owns the one mapping from an akerr status to an exit code: an exit status is a
|
||||||
|
byte, and every consumer status starts at 256, so passing a status through
|
||||||
|
`exit()` silently truncates it — status 256 exits 0 and reports success. The
|
||||||
|
only exits that bypass it are the two that have no status to map:
|
||||||
|
`ENSURE_ERROR_READY`'s pool-exhaustion abort in `include/akerror.tmpl.h`, and the
|
||||||
|
NULL-context case in `akerr_default_handler_unhandled_error()` in `src/error.c`. This rule applies to test
|
||||||
|
programs too, except where the test's whole point is to observe the raw
|
||||||
|
truncation.
|
||||||
|
|
||||||
## Testing Guidelines
|
## Testing Guidelines
|
||||||
|
|
||||||
Add tests as `tests/err_<behavior>.c`. Register each new test in the
|
Add tests as `tests/err_<behavior>.c`. Register each new test in the
|
||||||
@@ -66,6 +115,19 @@ CTest suite before submitting changes, and run mutation testing and coverage
|
|||||||
when changing core control-flow, reference counting, stack-trace, or handler
|
when changing core control-flow, reference counting, stack-trace, or handler
|
||||||
behavior.
|
behavior.
|
||||||
|
|
||||||
|
Tests that drive the library from several threads go in the `AKERR_THREAD_SAFE`
|
||||||
|
branch of the `AKERR_TESTS` list — an `-DAKERR_THREADS=none` build has no
|
||||||
|
threading to test — and use `tests/err_threads.h`, which runs a body on
|
||||||
|
`AKERR_TEST_THREADS` threads that meet at a barrier first.
|
||||||
|
Count failures per thread with `AKERR_TCHECK` rather than returning early: a
|
||||||
|
thread that abandons its work leaves the others holding pool slots and turns one
|
||||||
|
failure into a cascade. Anything the test shares between its own threads must go
|
||||||
|
through `__atomic` builtins — a race in the test is still a race, and
|
||||||
|
ThreadSanitizer cannot tell you whose it is. The capturing logger in
|
||||||
|
`err_capture.h` is single-threaded (shared buffer, shared length); use
|
||||||
|
`akerr_thread_logger` instead. Run `scripts/thread_test.sh` when changing
|
||||||
|
anything that touches the pool, the registry, initialization, or the lock.
|
||||||
|
|
||||||
## Commit & Pull Request Guidelines
|
## Commit & Pull Request Guidelines
|
||||||
|
|
||||||
Recent commits use short, imperative, sentence-case subjects, for example
|
Recent commits use short, imperative, sentence-case subjects, for example
|
||||||
|
|||||||
121
CMakeLists.txt
121
CMakeLists.txt
@@ -1,9 +1,15 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.10)
|
||||||
# The status-code registry replaced the consumer-sized __AKERR_ERROR_NAMES array
|
# 1.0.0 replaced the consumer-sized __AKERR_ERROR_NAMES array with private
|
||||||
# with private storage, which is a source and ABI break for anything built
|
# storage. 2.0.0 makes the library thread safe, which is a second ABI break in
|
||||||
# against an earlier header -- hence 1.0.0 and a SOVERSION, so a stale installed
|
# the same places: __akerr_last_ignored became thread-local storage, and
|
||||||
# libakerror.so can no longer be silently paired with new headers.
|
# ENSURE_ERROR_READY no longer takes the pool reference that akerr_next_error()
|
||||||
project(akerror VERSION 1.0.0 LANGUAGES C)
|
# now takes for it. Consumer code compiled against a 1.x header would
|
||||||
|
# double-count every reference. Hence the major bump and the SOVERSION, so a
|
||||||
|
# stale installed libakerror.so cannot be silently paired with new headers.
|
||||||
|
# 2.0.1 fixes the unhandled-error exit code, which reported success for any
|
||||||
|
# status whose low byte was zero. It adds akerr_exit() but breaks nothing: the
|
||||||
|
# soname is unchanged and no existing entry point changed shape.
|
||||||
|
project(akerror VERSION 2.0.1 LANGUAGES C)
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
include(CMakePackageConfigHelpers)
|
include(CMakePackageConfigHelpers)
|
||||||
@@ -11,6 +17,39 @@ include(CTest)
|
|||||||
|
|
||||||
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
set(AKERR_USE_STDLIB 1 CACHE BOOL "Use the C standard library")
|
||||||
set(AKERR_COVERAGE 0 CACHE BOOL "Instrument the build with gcov coverage counters")
|
set(AKERR_COVERAGE 0 CACHE BOOL "Instrument the build with gcov coverage counters")
|
||||||
|
set(AKERR_SANITIZE "" CACHE STRING
|
||||||
|
"Sanitizers to build the library and tests with, e.g. thread or address,undefined")
|
||||||
|
|
||||||
|
# Threading backend for the library's global state (the error pool and the
|
||||||
|
# status registry). "auto" takes POSIX threads when they exist and fails the
|
||||||
|
# configure when they do not: a build that silently fell back to no locking
|
||||||
|
# would produce a library that reports itself thread safe and is not. Say
|
||||||
|
# -DAKERR_THREADS=none to mean it on purpose.
|
||||||
|
set(AKERR_THREADS "auto" CACHE STRING "Threading backend: auto, pthread, or none")
|
||||||
|
set_property(CACHE AKERR_THREADS PROPERTY STRINGS auto pthread none)
|
||||||
|
|
||||||
|
if(AKERR_THREADS STREQUAL "auto" OR AKERR_THREADS STREQUAL "pthread")
|
||||||
|
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||||
|
find_package(Threads)
|
||||||
|
if(CMAKE_USE_PTHREADS_INIT)
|
||||||
|
set(AKERR_THREAD_SAFE 1)
|
||||||
|
elseif(AKERR_THREADS STREQUAL "pthread")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"-DAKERR_THREADS=pthread was requested but no POSIX thread library "
|
||||||
|
"was found.")
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"No POSIX thread library was found. libakerror serializes its "
|
||||||
|
"global state with a recursive pthread mutex; without one it "
|
||||||
|
"cannot be thread safe. Configure with -DAKERR_THREADS=none to "
|
||||||
|
"build a deliberately single-threaded library instead.")
|
||||||
|
endif()
|
||||||
|
elseif(AKERR_THREADS STREQUAL "none")
|
||||||
|
set(AKERR_THREAD_SAFE 0)
|
||||||
|
else()
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"AKERR_THREADS must be auto, pthread or none, not '${AKERR_THREADS}'")
|
||||||
|
endif()
|
||||||
|
|
||||||
# Size of the private status-name hash table. Must be a power of two; usable
|
# Size of the private status-name hash table. Must be a power of two; usable
|
||||||
# capacity is 75% of it (src/error.c asserts both). The host's errno list
|
# capacity is 75% of it (src/error.c asserts both). The host's errno list
|
||||||
@@ -59,6 +98,25 @@ function(akerr_instrument_for_coverage _target)
|
|||||||
endif()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
# Sanitizers. Unlike coverage these go on the tests as well as the library:
|
||||||
|
# ThreadSanitizer only sees a race if every thread that touches the memory was
|
||||||
|
# compiled with it, and the threads live in the test programs.
|
||||||
|
# cmake -S . -B build/tsan -DAKERR_SANITIZE=thread
|
||||||
|
if(AKERR_SANITIZE AND NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"AKERR_SANITIZE requires GCC or Clang, not ${CMAKE_C_COMPILER_ID}")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
function(akerr_instrument_for_sanitizers _target)
|
||||||
|
if(AKERR_SANITIZE)
|
||||||
|
target_compile_options(${_target} PRIVATE
|
||||||
|
-fsanitize=${AKERR_SANITIZE}
|
||||||
|
-fno-omit-frame-pointer -g -O1)
|
||||||
|
set_property(TARGET ${_target} APPEND_STRING
|
||||||
|
PROPERTY LINK_FLAGS " -fsanitize=${AKERR_SANITIZE}")
|
||||||
|
endif()
|
||||||
|
endfunction()
|
||||||
|
|
||||||
set(SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generrno.sh)
|
set(SCRIPT ${CMAKE_CURRENT_SOURCE_DIR}/scripts/generrno.sh)
|
||||||
set(INFILE ${CMAKE_CURRENT_SOURCE_DIR}/include/akerror.tmpl.h)
|
set(INFILE ${CMAKE_CURRENT_SOURCE_DIR}/include/akerror.tmpl.h)
|
||||||
|
|
||||||
@@ -67,6 +125,14 @@ set(GENERATED_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated)
|
|||||||
set(GENERATED_ERRNO_C ${GENERATED_DIR}/src/errno.c)
|
set(GENERATED_ERRNO_C ${GENERATED_DIR}/src/errno.c)
|
||||||
set(GENERATED_AKERROR_H ${GENERATED_DIR}/include/akerror.h)
|
set(GENERATED_AKERROR_H ${GENERATED_DIR}/include/akerror.h)
|
||||||
|
|
||||||
|
# The threading decision is stamped into the generated header, so the header has
|
||||||
|
# to be regenerated when it changes. Makefile generators compare timestamps
|
||||||
|
# rather than command lines, so carry the value through a file: configure_file
|
||||||
|
# rewrites it only when the content differs, which is exactly the trigger we
|
||||||
|
# want and no trigger at all on an unchanged reconfigure.
|
||||||
|
set(GENERATED_THREAD_STAMP ${CMAKE_CURRENT_BINARY_DIR}/akerr_thread_safe.stamp)
|
||||||
|
configure_file(cmake/thread_safe.stamp.in ${GENERATED_THREAD_STAMP} @ONLY)
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT ${GENERATED_ERRNO_C} ${GENERATED_AKERROR_H}
|
OUTPUT ${GENERATED_ERRNO_C} ${GENERATED_AKERROR_H}
|
||||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${GENERATED_DIR}
|
COMMAND ${CMAKE_COMMAND} -E make_directory ${GENERATED_DIR}
|
||||||
@@ -74,7 +140,8 @@ add_custom_command(
|
|||||||
${SCRIPT}
|
${SCRIPT}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
${GENERATED_DIR}
|
${GENERATED_DIR}
|
||||||
DEPENDS ${SCRIPT} ${INFILE}
|
${AKERR_THREAD_SAFE}
|
||||||
|
DEPENDS ${SCRIPT} ${INFILE} ${GENERATED_THREAD_STAMP}
|
||||||
VERBATIM
|
VERBATIM
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,18 +158,34 @@ target_include_directories(akerror PUBLIC
|
|||||||
find_package(PkgConfig REQUIRED)
|
find_package(PkgConfig REQUIRED)
|
||||||
add_library(akerror::akerror ALIAS akerror)
|
add_library(akerror::akerror ALIAS akerror)
|
||||||
|
|
||||||
|
# The threading backend is PRIVATE: src/lock.h is not installed, so which
|
||||||
|
# primitive the library locks with is invisible to a consumer. What a consumer
|
||||||
|
# does see -- whether the library locks at all -- travels in the generated
|
||||||
|
# header instead, where it cannot disagree with this build.
|
||||||
|
if(AKERR_THREAD_SAFE)
|
||||||
|
set(AKERR_THREADS_DEFINE AKERR_THREADS_PTHREAD=1)
|
||||||
|
else()
|
||||||
|
set(AKERR_THREADS_DEFINE AKERR_THREADS_NONE=1)
|
||||||
|
endif()
|
||||||
|
|
||||||
target_compile_definitions(akerror
|
target_compile_definitions(akerror
|
||||||
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
|
PUBLIC AKERR_USE_STDLIB=${AKERR_USE_STDLIB}
|
||||||
PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS}
|
PRIVATE AKERR_STATUS_NAME_SLOTS=${AKERR_STATUS_NAME_SLOTS}
|
||||||
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES}
|
PRIVATE AKERR_MAX_RESERVED_STATUS_RANGES=${AKERR_MAX_RESERVED_STATUS_RANGES}
|
||||||
|
PRIVATE ${AKERR_THREADS_DEFINE}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(AKERR_THREAD_SAFE)
|
||||||
|
target_link_libraries(akerror PRIVATE Threads::Threads)
|
||||||
|
endif()
|
||||||
|
|
||||||
set_target_properties(akerror PROPERTIES
|
set_target_properties(akerror PROPERTIES
|
||||||
VERSION ${PROJECT_VERSION}
|
VERSION ${PROJECT_VERSION}
|
||||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||||
)
|
)
|
||||||
|
|
||||||
akerr_instrument_for_coverage(akerror)
|
akerr_instrument_for_coverage(akerror)
|
||||||
|
akerr_instrument_for_sanitizers(akerror)
|
||||||
|
|
||||||
# Each test is one source file in tests/ built into test_<name> and registered
|
# Each test is one source file in tests/ built into test_<name> and registered
|
||||||
# as CTest <name>. Tests expected to abort (unhandled error / contract
|
# as CTest <name>. Tests expected to abort (unhandled error / contract
|
||||||
@@ -137,10 +220,24 @@ set(AKERR_TESTS
|
|||||||
err_name_bounds
|
err_name_bounds
|
||||||
err_format_string
|
err_format_string
|
||||||
err_unhandled_null
|
err_unhandled_null
|
||||||
|
err_exit_status
|
||||||
err_release_null
|
err_release_null
|
||||||
err_release_refcount
|
err_release_refcount
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# These drive the library from many threads at once. They are worth running on
|
||||||
|
# their own -- they assert exclusive ownership of pool slots and of reserved
|
||||||
|
# ranges, which is checkable without a sanitizer -- but the run that proves the
|
||||||
|
# absence of a race is the one under -DAKERR_SANITIZE=thread.
|
||||||
|
if(AKERR_THREAD_SAFE)
|
||||||
|
list(APPEND AKERR_TESTS
|
||||||
|
err_threads_init
|
||||||
|
err_threads_pool
|
||||||
|
err_threads_registry
|
||||||
|
err_threads_handoff
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
|
||||||
set(AKERR_WILL_FAIL_TESTS
|
set(AKERR_WILL_FAIL_TESTS
|
||||||
err_trace
|
err_trace
|
||||||
err_improper_closure
|
err_improper_closure
|
||||||
@@ -151,7 +248,19 @@ foreach(_test IN LISTS AKERR_TESTS)
|
|||||||
add_executable(test_${_test} tests/${_test}.c)
|
add_executable(test_${_test} tests/${_test}.c)
|
||||||
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
target_include_directories(test_${_test} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests)
|
||||||
target_link_libraries(test_${_test} PRIVATE akerror)
|
target_link_libraries(test_${_test} PRIVATE akerror)
|
||||||
|
if(AKERR_THREAD_SAFE)
|
||||||
|
target_link_libraries(test_${_test} PRIVATE Threads::Threads)
|
||||||
|
endif()
|
||||||
|
akerr_instrument_for_sanitizers(test_${_test})
|
||||||
add_test(NAME ${_test} COMMAND test_${_test})
|
add_test(NAME ${_test} COMMAND test_${_test})
|
||||||
|
# A sanitizer report is a test failure. Without halt_on_error the runtime
|
||||||
|
# prints and continues, which leaves a race to be noticed in the log by
|
||||||
|
# somebody reading it -- and under a race storm the reporting itself is slow
|
||||||
|
# enough to look like a hang.
|
||||||
|
if(AKERR_SANITIZE)
|
||||||
|
set_tests_properties(${_test} PROPERTIES ENVIRONMENT
|
||||||
|
"TSAN_OPTIONS=halt_on_error=1;ASAN_OPTIONS=halt_on_error=1;UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1")
|
||||||
|
endif()
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|
||||||
set_tests_properties(
|
set_tests_properties(
|
||||||
|
|||||||
476
README.md
476
README.md
@@ -4,14 +4,6 @@ This library provides a TRY/CATCH style exception handling mechanism for C.
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Upgrading from a pre-1.0.0 release
|
|
||||||
|
|
||||||
1.0.0 replaced the consumer-sized status-name array with a private, ownership-
|
|
||||||
enforced registry. That is a source and ABI break: see
|
|
||||||
[UPGRADING.md](UPGRADING.md) for what was removed, how to migrate, the capacity
|
|
||||||
limits and how to raise them, and the thread-safety rules.
|
|
||||||
|
|
||||||
|
|
||||||
# Why?
|
# Why?
|
||||||
|
|
||||||
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
There is nothing wrong with C as it is. This library does not claim to fix some problem with C.
|
||||||
@@ -20,10 +12,6 @@ Instead, this library implements a pragmatic and stylistic choice to assist the
|
|||||||
|
|
||||||
Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.
|
Why? Because some programmers prefer to have the power of C with just a little bit of help in managing their errors.
|
||||||
|
|
||||||
# Library Architecture
|
|
||||||
|
|
||||||
## Philosophy of Use
|
|
||||||
|
|
||||||
This library has 6 guiding principles:
|
This library has 6 guiding principles:
|
||||||
|
|
||||||
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
|
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
|
||||||
@@ -33,86 +21,19 @@ This library has 6 guiding principles:
|
|||||||
* Manipulating the call stack directly is error prone and dangerous
|
* Manipulating the call stack directly is error prone and dangerous
|
||||||
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
|
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
|
||||||
|
|
||||||
## Lifecycle of an error in the AKError library
|
# Documentation
|
||||||
|
|
||||||
TL;DR - `akerr_ErrorContext` objects are filled with error context information and bubbled up through nested control structures until they are handled or reach the top level, where an unhandled error halts program termination with a stack trace
|
|
||||||
|
|
||||||
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
|
|
||||||
2. The akerr_ErrorContext is returned from the scope where the error was detected
|
|
||||||
3. The akerr_ErrorContext enters a control structure provided by the AKError library through a series of macros that examine `akerr_ErrorContext` objects as they pass through
|
|
||||||
4. The control structure checks to see if the `akerr_ErrorContext` has an error set, and if so, if there are any handlers in the current control structure that can handle it
|
|
||||||
5. If the current control structure can handle the `akerr_ErrorContext`, it does so
|
|
||||||
6. If the current control structure can not handle the `akerr_ErrorContext`, then the current control structure's cleanup code (if any) is executed, and the `akerr_ErrorContext` object is passed out of the current control structure to the parent control structure
|
|
||||||
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
|
|
||||||
8. When the first level of the control structure is reached, if the `akerr_ErrorContext` has an error set in it, then the stack trace information in the `akerr_ErrorContext` object is used to print a stack trace using the configured logging function, and program termination is halted
|
|
||||||
|
|
||||||
## What is in an Error Context
|
|
||||||
|
|
||||||
The Error Context object is a simple object which contains a few things:
|
|
||||||
|
|
||||||
* A numeric error code
|
|
||||||
* The name of the file in which the error occurred
|
|
||||||
* The name of the function in which the error occurred
|
|
||||||
* The line number in the file at which the error occurred
|
|
||||||
* A character buffer containing a message about the error in question
|
|
||||||
|
|
||||||
The structure also contains housekeeping information for the library which are of no specific interest to the user. See [include/akerror.h](include/akerror.h) for more details.
|
|
||||||
|
|
||||||
## What are the control structures
|
|
||||||
|
|
||||||
The library is structured around a series of macros that construct `switch` statements that perform logic against an `akerr_ErrorContext` which exists in the current scope and has been initialized. These macros must be assembled in a specific order to produce a syntactically correct `switch` statement which performs correct operations against the `akerr_ErrorContext` to attempt operations, detect failures, perform cleanup operations, handle errors, and then exit a given scope in a success or failure state.
|
|
||||||
|
|
||||||
## Functions and Return Codes
|
|
||||||
|
|
||||||
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *`.
|
|
||||||
|
|
||||||
Any function which uses the `PREPARE_ERROR` macro should have a return type of `akerr_ErrorContext *`. The macros within this library, when they detect an unhandled error, will attempt to pass up the unhandled error to the context of the previous function in the call stack. This allows for errors to propagate up through the call stack in the same way as exceptions. (For example, if you use traditional C error handling in a call stack of `a() -> b() -> c()`, and `c()` fails because it runs out of memory, `b()` will likely detect that error and return some error to `a()`, but it may or may not return the context of what failed and why. With this, you get that context all the way up in `a()` without knowing anything about `c()`.
|
|
||||||
|
|
||||||
## Error codes
|
|
||||||
|
|
||||||
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
|
|
||||||
|
|
||||||
You can define additional error types as integer constants. Values 0 through 255
|
|
||||||
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
|
|
||||||
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
|
|
||||||
names are stored sparsely, so any `int` is a legal status and no compile
|
|
||||||
definition is needed to use large values.
|
|
||||||
|
|
||||||
Every library that may coexist in one process must reserve its range during
|
|
||||||
initialization. `akerr_reserve_status_range()` and
|
|
||||||
`akerr_register_status_name()` report failure the way everything else in this
|
|
||||||
library does — they return `akerr_ErrorContext *`, and they are marked
|
|
||||||
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
|
|
||||||
`PASS` up out of your initialization:
|
|
||||||
|
|
||||||
```c
|
|
||||||
#define MYLIB_OWNER "my-library"
|
|
||||||
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
|
||||||
{
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
|
|
||||||
/* Another component owning part of the range propagates to our caller. */
|
|
||||||
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
|
||||||
|
|
||||||
/* Then name each code, quoting the owner you reserved with. */
|
|
||||||
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
|
|
||||||
"Some Error Code Description"));
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Reservations are process-local, fixed-capacity, and idempotent when the same
|
|
||||||
owner repeats the exact same range. They preserve compile-time integer constants
|
|
||||||
so `HANDLE` still works, since `case` labels require them.
|
|
||||||
|
|
||||||
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
|
|
||||||
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
|
|
||||||
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
|
|
||||||
owner. See [UPGRADING.md](UPGRADING.md) for the full list of statuses these two
|
|
||||||
functions raise, the capacity limits and how to raise them, and thread-safety
|
|
||||||
rules.
|
|
||||||
|
|
||||||
|
| Document | What it answers |
|
||||||
|
| -------- | --------------- |
|
||||||
|
| [docs/architecture.md](docs/architecture.md) | What an error context is, how one travels up the call stack, and what the macros build |
|
||||||
|
| [docs/usage.md](docs/usage.md) | The macro reference: `ATTEMPT`/`CLEANUP`/`PROCESS`/`FINISH`, `CATCH`, `FAIL_*`, `PASS`, `HANDLE`, `SUCCEED_RETURN` |
|
||||||
|
| [docs/status-codes.md](docs/status-codes.md) | Defining your own status codes, and reserving a range so two libraries cannot collide |
|
||||||
|
| [docs/uncaught-errors.md](docs/uncaught-errors.md) | `AKERR_NOIGNORE`, what a stack trace looks like, and how to read one |
|
||||||
|
| [docs/exit-status.md](docs/exit-status.md) | Why you call `akerr_exit()` and never `exit()`, and how to replace the unhandled-error handler |
|
||||||
|
| [docs/thread-safety.md](docs/thread-safety.md) | What thread safety here covers, what it does not, and how to hand an error to another thread |
|
||||||
|
| [docs/building.md](docs/building.md) | Configure options, the generated header, and building without stdlib |
|
||||||
|
| [UPGRADING.md](UPGRADING.md) | What changed in 1.0.0, 2.0.0 and 2.0.1, and how to migrate |
|
||||||
|
| [TODO.md](TODO.md) | Known defects, ordered by blast radius |
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
@@ -122,38 +43,11 @@ cmake --build build
|
|||||||
cmake --install build
|
cmake --install build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Templating and autogenerated code
|
The library depends on `stdlib` and on POSIX threads. Both are optional at the
|
||||||
|
cost of some functionality — see [docs/building.md](docs/building.md) for
|
||||||
The build process relies upon `scripts/generrno.sh` which performs the following:
|
`-DAKERR_USE_STDLIB=OFF` and
|
||||||
|
[docs/thread-safety.md](docs/thread-safety.md#building-single-threaded) for
|
||||||
1. Executes `errno --list` and gathers up the output
|
`-DAKERR_THREADS=none`.
|
||||||
1. Templates `include/akerror.tmpl.h` into `include/akerror.h` to set the `AKERR_LAST_ERRNO_VALUE` equal to the highest integer defined by `errno`
|
|
||||||
2. Generates `src/errno.c` which contains a function called by `akerr_init` which initializes all of the status names for the previously defined values of `errno`.
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
This library depends upon `stdlib`. If you don't want to link against stdlib, you must modify the library code to include headers and link against a library that provides the following:
|
|
||||||
|
|
||||||
- `memset` function
|
|
||||||
- `strncpy` function
|
|
||||||
- `strlen` function
|
|
||||||
- `strcmp` function
|
|
||||||
- `sprintf` function
|
|
||||||
- `exit` function
|
|
||||||
- `bool` type
|
|
||||||
- `NULL` type
|
|
||||||
- `INT_MAX` constant
|
|
||||||
- `PATH_MAX` constant
|
|
||||||
|
|
||||||
... then you can compile it thusly:
|
|
||||||
|
|
||||||
```
|
|
||||||
cmake -S . -B build -DAKERR_USE_STDLIB=OFF
|
|
||||||
cmake --build build
|
|
||||||
cmake --install build
|
|
||||||
```
|
|
||||||
|
|
||||||
# Using the library
|
|
||||||
|
|
||||||
## Setting up your project
|
## Setting up your project
|
||||||
|
|
||||||
@@ -192,291 +86,93 @@ add_subdirectory(deps/libakerror EXCLUDE_FROM_ALL)
|
|||||||
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
|
target_link_libraries(YOUR_PROJECT PRIVATE akerror::akerror)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# Quickstart
|
||||||
|
|
||||||
## (Optional) Configuring the logging function
|
A function that can fail returns an `akerr_ErrorContext *` instead of a value,
|
||||||
|
and moves its real output to a pointer parameter. `AKERR_NOIGNORE` makes the
|
||||||
The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls `fprintf(stderr, f, ...)`. If you want to override this behavior, then set the error handler to a function with a printf-style signature:
|
compiler complain if a caller throws that return away.
|
||||||
|
|
||||||
```
|
|
||||||
void my_logger(const char *fmt, ...)
|
|
||||||
{
|
|
||||||
/* ... do something */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* set your custom error handler */
|
|
||||||
akerr_log_method = &my_logger;
|
|
||||||
|
|
||||||
/* proceed to use the library */
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setting Up the Error Context
|
|
||||||
|
|
||||||
Before you can use any of these macros you must set up an error context inside of the current scope.
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
PREPARE_ERROR(errctx);
|
#include <akerror.h>
|
||||||
```
|
#include <stdio.h>
|
||||||
|
|
||||||
This will create a akerr_ErrorContext structure inside of the current scope named `errctx` and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.
|
/* Fails with a message; the caller finds out what and where. */
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *open_config(const char *path, FILE **dest)
|
||||||
## Attempting an Operation
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
// ... code
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true)
|
|
||||||
```
|
|
||||||
|
|
||||||
`ATTEMPT { ... }` is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.
|
|
||||||
|
|
||||||
`CLEANUP { ... }` is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.
|
|
||||||
|
|
||||||
`PROCESS(errctx) { ... }` is the block within which you will handle any errors that were caught inside of the `ATTEMPT` block. See "Handling Errors" below.
|
|
||||||
|
|
||||||
`FINISH(errctx, true)` terminates the attempt operation. The `FINISH` macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your `main()` method, this should be true. Inside of your `main()` method, call `FINISH_NORExbTURN(errctx)` instead.
|
|
||||||
|
|
||||||
|
|
||||||
# Capturing errors
|
|
||||||
|
|
||||||
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
|
|
||||||
|
|
||||||
## Capturing errors from functions which return akerr_ErrorContext *
|
|
||||||
|
|
||||||
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(errctx, errorGeneratingFunction())
|
|
||||||
} // ...
|
|
||||||
```
|
|
||||||
|
|
||||||
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
|
||||||
|
|
||||||
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
|
|
||||||
|
|
||||||
## Setting errors from functions or expressions returning integer
|
|
||||||
|
|
||||||
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
|
|
||||||
|
|
||||||
Here is an example of checking for a NULL pointer
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_ZERO_BREAK(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
|
||||||
} // ...
|
|
||||||
```
|
|
||||||
|
|
||||||
Here is an example of checking for two strings that are not equal
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
|
||||||
} // ...
|
|
||||||
```
|
|
||||||
|
|
||||||
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
|
||||||
|
|
||||||
## Important: do not use CATCH or FAIL_*_BREAK inside a loop
|
|
||||||
|
|
||||||
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
|
|
||||||
|
|
||||||
```c
|
|
||||||
ATTEMPT {
|
|
||||||
for ( int i = 0; i < n; i++ ) {
|
|
||||||
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
|
|
||||||
}
|
|
||||||
// ... this code still executes, with errctx already in an error state ...
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
|
|
||||||
|
|
||||||
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
|
|
||||||
|
|
||||||
```c
|
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
|
|
||||||
{
|
{
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
for ( int i = 0; i < n; i++ ) {
|
|
||||||
PASS(errctx, process(items[i])); // returns from process_all on the first error
|
FAIL_ZERO_RETURN(errctx, (path != NULL), AKERR_NULLPOINTER,
|
||||||
}
|
"no config path was given");
|
||||||
|
|
||||||
|
*dest = fopen(path, "r");
|
||||||
|
FAIL_ZERO_RETURN(errctx, (*dest != NULL), AKERR_IO,
|
||||||
|
"could not open %s", path);
|
||||||
|
|
||||||
SUCCEED_RETURN(errctx);
|
SUCCEED_RETURN(errctx);
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
FILE *config = NULL;
|
||||||
|
|
||||||
```c
|
PREPARE_ERROR(errctx);
|
||||||
ATTEMPT {
|
ATTEMPT {
|
||||||
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
|
FAIL_ZERO_BREAK(errctx, (argc == 2), AKERR_VALUE,
|
||||||
} CLEANUP {
|
"usage: %s <config>", argv[0]);
|
||||||
// ... always runs ...
|
CATCH(errctx, open_config(argv[1], &config));
|
||||||
} PROCESS(errctx) {
|
/* ... read the config ... */
|
||||||
} HANDLE(errctx, AKERR_VALUE) {
|
} CLEANUP {
|
||||||
// ... handle a failure from any iteration ...
|
/* Runs whether or not anything failed. */
|
||||||
} FINISH(errctx, true);
|
if ( config != NULL ) {
|
||||||
```
|
fclose(config);
|
||||||
|
}
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_VALUE) {
|
||||||
|
/* A usage error is ours to handle, so handle it and carry on. */
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
|
||||||
# Passing errors
|
return 0;
|
||||||
|
|
||||||
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.
|
|
||||||
|
|
||||||
```
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
PASS(e, some_method_that_may_fail());
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
```
|
|
||||||
|
|
||||||
This does the same thing as this, but with less code:
|
|
||||||
|
|
||||||
```
|
|
||||||
PREPARE_ERROR(e);
|
|
||||||
ATTEMPT {
|
|
||||||
CATCH(e, some_method_that_may_fail());
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(e) {
|
|
||||||
} FINISH(e, true);
|
|
||||||
SUCCEED_RETURN(e);
|
|
||||||
```
|
|
||||||
|
|
||||||
# Handling errors
|
|
||||||
|
|
||||||
Inside of the `PROCESS { ... }` block, you must handle any errors that occurred during the `ATTEMPT { ... }` block. You do this with `HANDLE`, `HANDLE_GROUP`, and `HANDLE_DEFAULT`.
|
|
||||||
|
|
||||||
## Handling a specific error with HANDLE
|
|
||||||
|
|
||||||
In order to handle a specific error code, use the `HANDLE` macro.
|
|
||||||
|
|
||||||
```c
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
|
||||||
// Something is complaining about a null pointer error. Do something about it.
|
|
||||||
} // ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Handling a group of errors with HANDLE_GROUP
|
|
||||||
|
|
||||||
In order to handle a group of related errors that all require the same failure behavior, use `HANDLE` followed by `HANDLE_GROUP`. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:
|
|
||||||
|
|
||||||
```c
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} HANDLE(errctx, AKERR_IO) {
|
|
||||||
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
|
||||||
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
|
||||||
// error handling code goes here
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's `HANDLE` or `HANDLE_GROUP` block; but this is not required, only the final handler needs to get any code.
|
`ATTEMPT` is where work that can fail goes. `CLEANUP` always runs. `PROCESS`
|
||||||
|
opens the handler section, and each `HANDLE` claims one status. Anything no
|
||||||
|
`HANDLE` claims is still an error when it reaches `FINISH`: inside a function,
|
||||||
|
`FINISH(errctx, true)` returns it to your caller; at the top, as above,
|
||||||
|
`FINISH_NORETURN(errctx)` prints the stack trace and ends the process. You do
|
||||||
|
not need to call `akerr_init()` — every entry point does it for you.
|
||||||
|
|
||||||
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `AKERR_IO`, `AKERR_KEY` and `AKERR_INDEX` are all handled as a group, but `AKERR_RELATIONSHIP` is not.
|
Three things worth knowing before you write much more than that:
|
||||||
|
|
||||||
```c
|
* [The full macro reference](docs/usage.md), including `PASS` for errors you
|
||||||
} PROCESS(errctx) {
|
cannot do anything about, and `HANDLE_GROUP` for several statuses that fail
|
||||||
} HANDLE(errctx, AKERR_IO) {
|
the same way.
|
||||||
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
* [Never use `CATCH` or a `FAIL_*_BREAK` macro inside a loop](docs/usage.md#important-do-not-use-catch-or-fail__break-inside-a-loop).
|
||||||
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
They leave the `ATTEMPT` with a C `break`, which only escapes the innermost
|
||||||
// This code handles 3 error cases
|
loop. This is the first thing that bites people.
|
||||||
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
* [Never call `exit()` with a status. Call `akerr_exit()`](docs/exit-status.md).
|
||||||
// This code handles 1 error case
|
An exit status is a byte and every consumer status starts at 256, so `exit()`
|
||||||
}
|
truncates status 256 to 0 and reports success.
|
||||||
```
|
|
||||||
|
|
||||||
# Returning success or failure from functions returning akerr_ErrorContext *
|
# Thread safety
|
||||||
|
|
||||||
If at all possible, when using this library, your functiions should return `akerr_ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
|
The library is thread safe as built by default: every entry point may be called
|
||||||
|
from any thread at any time, and an error context may be handed from one thread
|
||||||
|
to another and released there. There is one recursive lock covering the pool and
|
||||||
|
the registry, so error construction is serialized — a program that raises errors
|
||||||
|
on its hot path will feel it. See [docs/thread-safety.md](docs/thread-safety.md)
|
||||||
|
for what that covers, what it cannot, and the handoff pattern.
|
||||||
|
|
||||||
## SUCCEED_RETURN
|
# Upgrading
|
||||||
|
|
||||||
This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.
|
2.0.1 fixes an unhandled error killing the process and still reporting success:
|
||||||
|
the exit code was the status truncated to a byte, and every consumer status
|
||||||
```c
|
starts at 256. Use `akerr_exit()` instead of `exit()` — see
|
||||||
PREPARE_ERROR(errctx);
|
[docs/exit-status.md](docs/exit-status.md). No ABI break.
|
||||||
ATTEMPT {
|
|
||||||
// ... stuff
|
|
||||||
} CLEANUP {
|
|
||||||
} PROCESS(errctx) {
|
|
||||||
} FINISH(errctx, true);
|
|
||||||
SUCCEED_RETURN(errctx);
|
|
||||||
```
|
|
||||||
|
|
||||||
## FAIL_RETURN
|
|
||||||
|
|
||||||
If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use `FAIL_RETURN` to accomplish this. Note that this should not be used inside of an `ATTEMPT { ... }` block; this immediately exits the function, preventing a `CLEANUP { ... }` block from executing. This can be safely used from inside of a `CLEANUP` or `PROCESS` block, or from anywhere within the function not inside of an `ATTEMPT { ... }` block.
|
|
||||||
|
|
||||||
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
|
|
||||||
|
|
||||||
```c
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conditionally failing and returning
|
|
||||||
|
|
||||||
In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the `FAIL_ZERO_RETURN` and `FAIL_NONZERO_RETURN` macros for this. These macros can be used anywhere that `FAIL_RETURN` can be used.
|
|
||||||
|
|
||||||
```c
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_ZERO_RETURN(errctx, (somePointer == NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
|
||||||
```
|
|
||||||
|
|
||||||
```c
|
|
||||||
PREPARE_ERROR(errctx);
|
|
||||||
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
|
||||||
```
|
|
||||||
|
|
||||||
# Uncaught errors
|
|
||||||
|
|
||||||
## Misbehaving methods
|
|
||||||
|
|
||||||
Any function which returns `akerr_ErrorContext *` and completes successfully MUST call `SUCCEED_RETURN(errctx)`. Failure to do this may result in an invalid `akerr_ErrorContext *` being returned, which will cause an `AKERR_BEHAVIOR` error to be triggered from your code.
|
|
||||||
|
|
||||||
## Ensuring that all error codes are captured
|
|
||||||
|
|
||||||
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERROR_NOIGNORE`.
|
|
||||||
|
|
||||||
```c
|
|
||||||
akerr_ErrorContext AKERROR_NOIGNORE *f(...);
|
|
||||||
```
|
|
||||||
|
|
||||||
This will cause a compile-time error if the return value of such a function is not used. "Used" here means assigned to a variable - it does not necessarily mean that the value is checked. However assuming that such functions are called inside of `ATTEMPT { ... }` blocks, it is safe to assume that such returns will be caught with `CATCH(...)`; therefore this error is a generally effective safeguard against careless coding where errors are not checked.
|
|
||||||
|
|
||||||
Beware that `AKERROR_NOIGNORE` is not a failsafe - it implements the `warn_unused_result` mechanic. By design users may explicitly ignore an error code from a function marked with `warn_unused_result` by explicitly casting the return to `void`.
|
|
||||||
|
|
||||||
```c
|
|
||||||
#define AKERROR_NOIGNORE __attribute__((warn_unused_result))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Stack Traces
|
|
||||||
|
|
||||||
Whenever an error is captured using the `FAIL_*` or `CATCH` methods, and is unhandled such that it manages to propagate all the way to the top of the caller stack without being managed, the last `FINISH` macro to touch the error will trigger a stack trace and kill the program.
|
|
||||||
|
|
||||||
Consider the `tests/err_trace.c` program which intentionally triggers this behavior. It produces output like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/err_trace.c:func2:7: 1 (Null Pointer Error) : This is a failure in func2
|
|
||||||
tests/err_trace.c:func2:10
|
|
||||||
tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
|
|
||||||
tests/err_trace.c:func1:18
|
|
||||||
tests/err_trace.c:func1:21
|
|
||||||
tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
|
|
||||||
tests/err_trace.c:main:30
|
|
||||||
tests/err_trace.c:main:33: Unhandled Error 1 (Null Pointer Error): This is a failure in func2
|
|
||||||
```
|
|
||||||
|
|
||||||
From bottom to top, we have:
|
|
||||||
|
|
||||||
* The last line printed is the `FINISH` macro call that triggered the stacktrace.
|
|
||||||
* Above that, the `CATCH()` inside of `main()` which caught the exception from `func1()` but did not handle it
|
|
||||||
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
|
||||||
* Above that, the `FINISH()` macro in the `func1` method which detected the presence of an unhandled error and returned it up the calling stack
|
|
||||||
* Above that, the `CATCH()` macro in the `func1` method which caught the error coming out of `func2()`
|
|
||||||
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
|
||||||
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
|
|
||||||
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here
|
|
||||||
|
|
||||||
|
2.0.0 makes the library thread safe. That is an ABI break — `__akerr_last_ignored`
|
||||||
|
became thread-local storage and the pool now takes its own reference — so
|
||||||
|
everything built against a 1.x header must be rebuilt. 1.0.0 replaced the
|
||||||
|
consumer-sized status-name array with a private, ownership-enforced registry.
|
||||||
|
See [UPGRADING.md](UPGRADING.md) for all three, what was removed, how to migrate,
|
||||||
|
the capacity limits and how to raise them, and the thread-safety rules.
|
||||||
|
|||||||
106
TODO.md
106
TODO.md
@@ -2,18 +2,20 @@
|
|||||||
|
|
||||||
Working notes for `libakerror`. Outstanding items only.
|
Working notes for `libakerror`. Outstanding items only.
|
||||||
|
|
||||||
## 1. The test suite has no sanitizer run
|
## 1. Only ThreadSanitizer is wired into CI, not ASan/UBSan
|
||||||
|
|
||||||
Mutation testing caught an out-of-bounds probe in the status-name hash table
|
`AKERR_SANITIZE` builds the library and the tests with any sanitizer list, and
|
||||||
that the suite could not: the failure mode was a write into adjacent BSS, which
|
CI runs `-DAKERR_SANITIZE=thread` through `scripts/thread_test.sh`. Nothing runs
|
||||||
does not crash, so every test still passed. Sharpening one test closed that
|
`address,undefined` yet, and that is the one that covers the original
|
||||||
instance, but ASan would have caught the whole class directly and independently
|
motivation: mutation testing caught an out-of-bounds probe in the status-name
|
||||||
of how sharp the assertions are.
|
hash table that the suite could not, because the failure mode was a write into
|
||||||
|
adjacent BSS, which does not crash. Sharpening one test closed that instance;
|
||||||
|
ASan would catch the whole class regardless of how sharp the assertions are.
|
||||||
|
|
||||||
Add a `-fsanitize=address,undefined` build to `.gitea/workflows/ci.yaml`, or a
|
The machinery is in place — this is one more job in
|
||||||
CMake option alongside `AKERR_COVERAGE`. This is the highest-value item here: it
|
`.gitea/workflows/ci.yaml` running
|
||||||
covers the whole library, not just the registry, and the library's fixed pools
|
`cmake -S . -B build/asan -DAKERR_SANITIZE=address,undefined`. Left separate
|
||||||
and manual buffer arithmetic are exactly what it is good at.
|
because ASan and TSan cannot be combined in one build.
|
||||||
|
|
||||||
## 2. `HANDLE`-level status aliasing is still undetectable
|
## 2. `HANDLE`-level status aliasing is still undetectable
|
||||||
|
|
||||||
@@ -48,12 +50,23 @@ A plugin host that `dlopen`s many distinct plugins over a process lifetime
|
|||||||
accumulates ranges until the table fills. Reloading the *same* plugin is fine —
|
accumulates ranges until the table fills. Reloading the *same* plugin is fine —
|
||||||
an identical repeat by the same owner is idempotent.
|
an identical repeat by the same owner is idempotent.
|
||||||
|
|
||||||
## 5. The registry is not thread safe
|
## 5. Renaming a status is not safe against a concurrent lookup
|
||||||
|
|
||||||
Global mutable state, no locking, and `akerr_status_name_count++` is not atomic.
|
`akerr_name_for_status(status, NULL)` returns a pointer into the registry rather
|
||||||
Currently documented as an initialization-time-only API rather than enforced. If
|
than a copy, which is what makes it usable from inside `FAIL` — it needs no
|
||||||
components start initializing on separate threads this needs either a lock or a
|
buffer and no error context of its own. Registering a *second* name for a status
|
||||||
documented once-per-process init barrier.
|
that already has one (`tests/err_name_ownership.c` covers that it is allowed)
|
||||||
|
overwrites that buffer in place, so a thread reading the name at that moment can
|
||||||
|
see a torn string. Every other registry operation is serialized; this one cannot
|
||||||
|
be, because the reader is outside the lock by the time it reads the characters.
|
||||||
|
|
||||||
|
Documented in docs/thread-safety.md and UPGRADING.md as "register names during
|
||||||
|
initialization". Closing it properly means making a registered name immutable —
|
||||||
|
either refusing a rename outright (a behavior change, and
|
||||||
|
`tests/err_name_ownership.c` asserts the current contract), or copying names
|
||||||
|
into a bump-allocated arena and publishing the pointer with a release store, so
|
||||||
|
a rename allocates new storage instead of rewriting live storage. The arena is
|
||||||
|
the better answer; it costs a second capacity limit and its exhaustion path.
|
||||||
|
|
||||||
## 6. Deprecate the two-argument name-registration path
|
## 6. Deprecate the two-argument name-registration path
|
||||||
|
|
||||||
@@ -72,8 +85,9 @@ build whose tables are too small for the library's own entries, and both sizes
|
|||||||
are `PRIVATE` to the library target, so a test executable cannot set them.
|
are `PRIVATE` to the library target, so a test executable cannot set them.
|
||||||
|
|
||||||
Closing it means a second library target built with tiny tables plus a
|
Closing it means a second library target built with tiny tables plus a
|
||||||
`WILL_FAIL` test linked against it — worth doing when the CMake gains a
|
`WILL_FAIL` test linked against it. Nothing in the CMake does that yet: the
|
||||||
sanitizer variant (item 1), since that adds the same machinery.
|
sanitizer and coverage options vary the *flags* of the one library target, not
|
||||||
|
its compile definitions.
|
||||||
|
|
||||||
Related: branch coverage on `src/error.c` now sits just above its 50% gate.
|
Related: branch coverage on `src/error.c` now sits just above its 50% gate.
|
||||||
Every `FAIL_*` site carries about six branch outcomes of error-construction
|
Every `FAIL_*` site carries about six branch outcomes of error-construction
|
||||||
@@ -83,11 +97,65 @@ fail carries about twenty-five. Validating more inputs therefore lowers the
|
|||||||
ratio by construction. Before adding defensive checks, expect to add a test that
|
ratio by construction. Before adding defensive checks, expect to add a test that
|
||||||
drives them, as `tests/err_copy_string.c` does.
|
drives them, as `tests/err_copy_string.c` does.
|
||||||
|
|
||||||
|
## 8. Mutation testing judges concurrency mutants without a sanitizer
|
||||||
|
|
||||||
|
`scripts/mutation_test.py` configures each mutant build with the default CMake
|
||||||
|
options, so a mutant that only breaks under concurrency is judged by a suite
|
||||||
|
running without ThreadSanitizer. Deleting the pool's `akerr_mutex_lock()` call
|
||||||
|
survives the run even though it is a real race: rebuilt and run directly, that
|
||||||
|
mutant fails `tests/err_threads_pool.c` in 4 of 10 runs, and fails under
|
||||||
|
`scripts/thread_test.sh` in 5 of 5. So 81.2% is a floor for that category, not a
|
||||||
|
verdict.
|
||||||
|
|
||||||
|
Closing it means a `--cmake-arg` passthrough on the harness so the mutant build
|
||||||
|
can be configured with `-DAKERR_SANITIZE=thread`. The whole run then costs a
|
||||||
|
TSan-instrumented suite per mutant (roughly 6s instead of 0.4s), so it belongs
|
||||||
|
behind a flag rather than in the default target or in CI.
|
||||||
|
|
||||||
|
## 9. No way to keep an error context and report it at the same time
|
||||||
|
|
||||||
|
A context can be handed to another thread and released there --
|
||||||
|
`docs/thread-safety.md` now documents that pattern and
|
||||||
|
`tests/err_threads_handoff.c` proves it -- but it is a *move*. A thread that
|
||||||
|
wants to both keep its error and report it upward has to read the fields out
|
||||||
|
into its own record, and it loses the stack trace doing so, because
|
||||||
|
`stacktracebuf` is the one thing that cannot be usefully summarized.
|
||||||
|
|
||||||
|
Copying the struct is not a workaround. `stacktracebufptr` is self-referential
|
||||||
|
(`include/akerror.tmpl.h`), so `akerr_ErrorContext c = *src;` leaves the copy's
|
||||||
|
cursor pointing into the source's buffer -- the copy logs correctly and then
|
||||||
|
corrupts a slot it does not own the first time anything appends to it. `arrayid`
|
||||||
|
is restored after the wipe in `akerr_release_error()` (`src/error.c:395-398`), so
|
||||||
|
a copied id makes the destination impersonate the source's slot for the life of
|
||||||
|
the process.
|
||||||
|
|
||||||
|
If this is ever worth an API, the shape is:
|
||||||
|
|
||||||
|
```c
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_copy_error(akerr_ErrorContext *source,
|
||||||
|
akerr_ErrorContext *destination);
|
||||||
|
```
|
||||||
|
|
||||||
|
taking the destination as a parameter rather than allocating it. An allocating
|
||||||
|
copy could fail on pool exhaustion, and reporting *that* failure needs a pool
|
||||||
|
slot, so it would have to abort -- adding a third `exit()` site to a library that
|
||||||
|
deliberately has two. Caller-allocates puts the pool pressure where it can be
|
||||||
|
managed. The copy must repair four fields: `arrayid` (the destination's own),
|
||||||
|
`refcount` (set to 1, never inherited), `handled` (false -- a copy is a fresh
|
||||||
|
obligation, or `FINISH_NORETURN` on the receiving side drops it silently), and
|
||||||
|
`stacktracebufptr` (re-anchored to the destination's buffer at the *same offset*,
|
||||||
|
so a later append continues the trace instead of overwriting it).
|
||||||
|
|
||||||
|
Not worth building yet: no consumer needs it. The trigger is a consumer that
|
||||||
|
needs a worker's stack *trace*, not just its status and message, at the join
|
||||||
|
point -- `libakstdlib`'s planned `pthread_*` wrappers are the likely first.
|
||||||
|
|
||||||
## Unrelated pre-existing issues
|
## Unrelated pre-existing issues
|
||||||
|
|
||||||
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
- The `AKERR_USE_STDLIB=OFF` build does not compile at all: `bool`, `PATH_MAX`
|
||||||
and `NULL` are used unconditionally but only included under the stdlib branch.
|
and `NULL` are used unconditionally but only included under the stdlib branch.
|
||||||
The README's dependency list states what a replacement must provide, but the
|
`docs/building.md`'s dependency list states what a replacement must provide,
|
||||||
header still needs its includes untangled for that configuration to work.
|
but the header still needs its includes untangled for that configuration to
|
||||||
|
work.
|
||||||
- `CMakeLists.txt` sets `main_lib_dest` from `MY_LIBRARY_VERSION`, which is never
|
- `CMakeLists.txt` sets `main_lib_dest` from `MY_LIBRARY_VERSION`, which is never
|
||||||
defined and never read. Dead line.
|
defined and never read. Dead line.
|
||||||
|
|||||||
157
UPGRADING.md
157
UPGRADING.md
@@ -1,3 +1,152 @@
|
|||||||
|
# Bug fix: unhandled-error exit status (2.0.1)
|
||||||
|
|
||||||
|
An unhandled error could kill the process and still report success.
|
||||||
|
|
||||||
|
`akerr_default_handler_unhandled_error()` ended in `exit(errctx->status)`, and a
|
||||||
|
process exit status is one byte wide — the kernel keeps the low 8 bits of the
|
||||||
|
argument and discards the rest. Consumer statuses start at
|
||||||
|
`AKERR_FIRST_CONSUMER_STATUS` (256), so **the first status any consumer can
|
||||||
|
reserve exited 0**, and a shell or supervisor watching `$?` saw a clean run.
|
||||||
|
Status 300 exited 44, which is some unrelated error's code. No status a consumer
|
||||||
|
owns could ever come out of `$?` intact, and there is no wider `exit()` to reach
|
||||||
|
for: `_exit()`, `_Exit()`, `quick_exit()` and the raw `exit_group` syscall all
|
||||||
|
truncate identically, and even `waitid()`, whose `si_status` is a full `int`,
|
||||||
|
reports the truncated value.
|
||||||
|
|
||||||
|
The mapping now lives in one place, `akerr_exit()`, which the default handler
|
||||||
|
calls:
|
||||||
|
|
||||||
|
```
|
||||||
|
status exit code
|
||||||
|
0 0 (success)
|
||||||
|
1 .. 255 the status
|
||||||
|
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
|
||||||
|
```
|
||||||
|
|
||||||
|
Statuses 0 through 255 are unchanged, which covers every `errno` and every
|
||||||
|
`AKERR_*` code. Only the values that were already being delivered wrong behave
|
||||||
|
differently, and they now exit 125 instead of a truncated byte.
|
||||||
|
|
||||||
|
**What you should change.** Call `akerr_exit()` instead of `exit()` anywhere you
|
||||||
|
leave the process on an akerr status — your own unhandled-error handler, a
|
||||||
|
top-level `HANDLE` block, an init routine that cannot continue — so one mapping
|
||||||
|
covers every exit. It is declared `AKERR_NORETURN`. If you were reading a
|
||||||
|
consumer status out of `$?`, you were never getting it: read the stack trace,
|
||||||
|
which carries the status at full width along with its registered name, or
|
||||||
|
install a handler that maps your own statuses into a byte. See
|
||||||
|
[docs/exit-status.md](docs/exit-status.md).
|
||||||
|
|
||||||
|
No ABI break. The soname stays `libakerror.so.2` and nothing you already call
|
||||||
|
changed shape. `akerr_exit()` is a new exported symbol, so a consumer that
|
||||||
|
starts calling it needs 2.0.1 or later at link time.
|
||||||
|
|
||||||
|
# Upgrade notice: thread safety (2.0.0)
|
||||||
|
|
||||||
|
2.0.0 makes the library thread safe. Every entry point may be called from any
|
||||||
|
thread, `akerr_init()` runs exactly once however many threads race into it, and
|
||||||
|
the error pool and the status registry are serialized.
|
||||||
|
|
||||||
|
This is an ABI break. Rebuild libakerror and everything that includes its
|
||||||
|
header; the soname moved to `libakerror.so.2` so the two cannot be mixed by
|
||||||
|
accident.
|
||||||
|
|
||||||
|
What moved at the ABI:
|
||||||
|
|
||||||
|
* `__akerr_last_ignored` is thread-local storage. An ignored error is a fact
|
||||||
|
about the thread that ignored it, and one shared slot had two threads
|
||||||
|
overwriting each other's. The `IGNORE` macro expands at *your* call site, so
|
||||||
|
your objects reference the symbol under whichever storage model your header
|
||||||
|
said — which is why this cannot be mixed.
|
||||||
|
* `akerr_next_error()` returns a context that **already holds one reference**.
|
||||||
|
Finding a free slot and claiming it has to be one operation under the pool
|
||||||
|
lock, or two threads scanning at once are handed the same slot.
|
||||||
|
`ENSURE_ERROR_READY` therefore no longer increments the count. Code compiled
|
||||||
|
against a 1.x header and linked against 2.x would count every reference twice
|
||||||
|
and never return a slot to the pool.
|
||||||
|
|
||||||
|
What changed in the API: nothing you call, unless you call `akerr_next_error()`
|
||||||
|
yourself. If you do, you now own a reference and must release it — the same
|
||||||
|
thing you were doing already if you were using the context for anything.
|
||||||
|
|
||||||
|
New build options, both on libakerror itself:
|
||||||
|
|
||||||
|
| Option | Default | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `AKERR_THREADS` | `auto` | Threading backend: `auto`, `pthread`, or `none` |
|
||||||
|
| `AKERR_SANITIZE` | (empty) | Sanitizers for the library and its tests, e.g. `thread` |
|
||||||
|
|
||||||
|
`auto` takes POSIX threads and **fails the configure** when it cannot find them,
|
||||||
|
rather than quietly building a library that reports itself thread safe and is
|
||||||
|
not. `-DAKERR_THREADS=none` is how you say you meant it: no locking, no
|
||||||
|
thread-local storage, undefined if you then use more than one thread.
|
||||||
|
|
||||||
|
The generated header records which one you built, as `AKERR_THREAD_SAFE` (`1` or
|
||||||
|
`0`), so a consumer can test what it linked against and cannot disagree with the
|
||||||
|
library about it.
|
||||||
|
|
||||||
|
## What thread safety here does and does not mean
|
||||||
|
|
||||||
|
Safe from any thread, with no coordination on your part:
|
||||||
|
|
||||||
|
* Raising, catching, handling, passing, ignoring and releasing errors.
|
||||||
|
* `akerr_reserve_status_range()` and `akerr_register_status_name()`. Two threads
|
||||||
|
reserving overlapping ranges cannot both succeed: one gets `NULL`, the other
|
||||||
|
gets `AKERR_STATUS_RANGE_OVERLAP` naming the winner.
|
||||||
|
* `akerr_name_for_status(status, NULL)` lookups, concurrently with each other
|
||||||
|
and with registrations of *other* statuses.
|
||||||
|
* `akerr_init()`, from any number of threads at once.
|
||||||
|
* **Handing a context to another thread, and releasing it there.** The reference
|
||||||
|
count is the only field the library reads across threads and it is only ever
|
||||||
|
touched under the pool lock, so `akerr_release_error()` does not care which
|
||||||
|
thread checked the slot out. Contexts live in process-global storage, not
|
||||||
|
thread-local, so one outlives the thread that raised it. What is still yours is
|
||||||
|
the handoff *itself*: it has to carry a happens-before edge, which any mutex,
|
||||||
|
condvar, `pthread_join` or acquire/release atomic gives you.
|
||||||
|
|
||||||
|
Still yours to coordinate:
|
||||||
|
|
||||||
|
* **Two threads in one context at once.** Ownership moves; it does not fork.
|
||||||
|
Hand a context over and stop touching it — the content is written with no
|
||||||
|
lock, so the handoff is what publishes it. See "Handing an error to another
|
||||||
|
thread" in docs/thread-safety.md for the pattern, and for why the queue has
|
||||||
|
to be bounded.
|
||||||
|
* **`akerr_log_method` and `akerr_handler_unhandled_error`** are read on every
|
||||||
|
error and written by nobody but you. Set them during startup, before spawning.
|
||||||
|
* **Renaming a status while another thread looks it up.**
|
||||||
|
`akerr_name_for_status()` returns a pointer into the registry — stable for the
|
||||||
|
life of the process, which is what makes it usable from a stack trace — and
|
||||||
|
registering a second name for the same status overwrites that buffer in place.
|
||||||
|
Register names during initialization. This is the one registry operation the
|
||||||
|
lock cannot make safe, because the reader is outside the lock by the time it
|
||||||
|
reads the string.
|
||||||
|
* **Which unhandled error wins.** Two threads reaching `FINISH_NORETURN` with
|
||||||
|
unhandled errors at the same instant both print a complete stack trace (the
|
||||||
|
buffer belongs to the context, and each line is a single `akerr_log_method`
|
||||||
|
call) and both call `akerr_handler_unhandled_error`. The process exits with
|
||||||
|
whichever status got there first.
|
||||||
|
|
||||||
|
## Cost
|
||||||
|
|
||||||
|
One recursive lock covers both the pool and the registry, so error
|
||||||
|
*construction* is serialized process-wide. Errors are the exceptional path and
|
||||||
|
correctness there is worth more than throughput, but a program that raises
|
||||||
|
errors in a hot loop will feel it.
|
||||||
|
|
||||||
|
The per-thread last-ditch context is a whole `akerr_ErrorContext` (tens of
|
||||||
|
kilobytes) in thread-local storage, allocated per thread on first use of the
|
||||||
|
library from that thread.
|
||||||
|
|
||||||
|
## Proving it
|
||||||
|
|
||||||
|
`tests/err_threads_init.c`, `tests/err_threads_pool.c`,
|
||||||
|
`tests/err_threads_registry.c` and `tests/err_threads_handoff.c` assert the
|
||||||
|
properties directly and run in the normal suite. The run that proves there is no data race underneath them is
|
||||||
|
ThreadSanitizer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/thread_test.sh
|
||||||
|
```
|
||||||
|
|
||||||
# Upgrade notice: custom status codes (1.0.0)
|
# Upgrade notice: custom status codes (1.0.0)
|
||||||
|
|
||||||
Version 1.0.0 replaces the consumer-sized status-name array with a private
|
Version 1.0.0 replaces the consumer-sized status-name array with a private
|
||||||
@@ -202,7 +351,7 @@ Exhausting either table raises an error to the caller; it is never silent.
|
|||||||
|
|
||||||
## Thread safety
|
## Thread safety
|
||||||
|
|
||||||
The registry is process-global mutable state with no locking. Reserve ranges and
|
As of 2.0.0 the registry is serialized: reservations, registrations and lookups
|
||||||
register names during single-threaded initialization, before spawning threads.
|
are all safe to call concurrently. See "What thread safety here does and does
|
||||||
Lookups (`akerr_name_for_status(status, NULL)`) are safe to call concurrently
|
not mean" at the top of this document for the two things that are still yours to
|
||||||
once registration has finished.
|
coordinate.
|
||||||
|
|||||||
1
cmake/thread_safe.stamp.in
Normal file
1
cmake/thread_safe.stamp.in
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@AKERR_THREAD_SAFE@
|
||||||
48
docs/architecture.md
Normal file
48
docs/architecture.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Library Architecture
|
||||||
|
|
||||||
|
## Philosophy of Use
|
||||||
|
|
||||||
|
This library has 6 guiding principles:
|
||||||
|
|
||||||
|
* Manually checking every possible return code for every possible meaning of that return code is tedious and prone to miss unpredicted failure cases
|
||||||
|
* Functions should return rich descriptive error contexts, not values
|
||||||
|
* Uncaught errors should cause program termination with a stacktrace
|
||||||
|
* Dynamic memory allocation is the source of many errors and should be avoided if possible
|
||||||
|
* Manipulating the call stack directly is error prone and dangerous
|
||||||
|
* Declaring, capturing, and reacting to errors should be intuitive and no more difficult than managing return codes
|
||||||
|
|
||||||
|
## Lifecycle of an error in the AKError library
|
||||||
|
|
||||||
|
TL;DR - `akerr_ErrorContext` objects are filled with error context information and bubbled up through nested control structures until they are handled or reach the top level, where an unhandled error halts program termination with a stack trace
|
||||||
|
|
||||||
|
1. At the point where an error occurs, an `akerr_ErrorContext` object is initialized and populated with information regarding the failure
|
||||||
|
2. The akerr_ErrorContext is returned from the scope where the error was detected
|
||||||
|
3. The akerr_ErrorContext enters a control structure provided by the AKError library through a series of macros that examine `akerr_ErrorContext` objects as they pass through
|
||||||
|
4. The control structure checks to see if the `akerr_ErrorContext` has an error set, and if so, if there are any handlers in the current control structure that can handle it
|
||||||
|
5. If the current control structure can handle the `akerr_ErrorContext`, it does so
|
||||||
|
6. If the current control structure can not handle the `akerr_ErrorContext`, then the current control structure's cleanup code (if any) is executed, and the `akerr_ErrorContext` object is passed out of the current control structure to the parent control structure
|
||||||
|
7. Steps 2-6 are repeated through as many control structures as are necessary to reach the first level of the control structure
|
||||||
|
8. When the first level of the control structure is reached, if the `akerr_ErrorContext` has an error set in it, then the stack trace information in the `akerr_ErrorContext` object is used to print a stack trace using the configured logging function, and program termination is halted
|
||||||
|
|
||||||
|
## What is in an Error Context
|
||||||
|
|
||||||
|
The Error Context object is a simple object which contains a few things:
|
||||||
|
|
||||||
|
* A numeric error code
|
||||||
|
* The name of the file in which the error occurred
|
||||||
|
* The name of the function in which the error occurred
|
||||||
|
* The line number in the file at which the error occurred
|
||||||
|
* A character buffer containing a message about the error in question
|
||||||
|
|
||||||
|
The structure also contains housekeeping information for the library which are of no specific interest to the user. See [include/akerror.tmpl.h](../include/akerror.tmpl.h) for more details.
|
||||||
|
|
||||||
|
## What are the control structures
|
||||||
|
|
||||||
|
The library is structured around a series of macros that construct `switch` statements that perform logic against an `akerr_ErrorContext` which exists in the current scope and has been initialized. These macros must be assembled in a specific order to produce a syntactically correct `switch` statement which performs correct operations against the `akerr_ErrorContext` to attempt operations, detect failures, perform cleanup operations, handle errors, and then exit a given scope in a success or failure state.
|
||||||
|
|
||||||
|
## Functions and Return Codes
|
||||||
|
|
||||||
|
This library can catch errors from any function or expression that returns an integer value, or from functions that return `akerr_ErrorContext *`.
|
||||||
|
|
||||||
|
Any function which uses the `PREPARE_ERROR` macro should have a return type of `akerr_ErrorContext *`. The macros within this library, when they detect an unhandled error, will attempt to pass up the unhandled error to the context of the previous function in the call stack. This allows for errors to propagate up through the call stack in the same way as exceptions. (For example, if you use traditional C error handling in a call stack of `a() -> b() -> c()`, and `c()` fails because it runs out of memory, `b()` will likely detect that error and return some error to `a()`, but it may or may not return the context of what failed and why. With this, you get that context all the way up in `a()` without knowing anything about `c()`.
|
||||||
|
|
||||||
61
docs/building.md
Normal file
61
docs/building.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# Building libakerror
|
||||||
|
|
||||||
|
The ordinary build is an out-of-tree CMake build, described in
|
||||||
|
[the README](../README.md#installation). This file covers what the build
|
||||||
|
generates for you, what it links against, and the configure options that change
|
||||||
|
either of those.
|
||||||
|
|
||||||
|
## Configure options
|
||||||
|
|
||||||
|
| Option | Default | What it does |
|
||||||
|
| ------ | ------- | ------------ |
|
||||||
|
| `AKERR_THREADS` | `auto` | Threading backend: `auto`, `pthread`, or `none`. `auto` takes POSIX threads and **fails the configure** if it cannot find them. See [Building single threaded](thread-safety.md#building-single-threaded). |
|
||||||
|
| `AKERR_USE_STDLIB` | `ON` | Link against the C standard library. See [Dependencies](#dependencies) for what you must supply instead when this is `OFF`. |
|
||||||
|
| `AKERR_STATUS_NAME_SLOTS` | `4096` | Slots in the status-name table; 75% of it is usable. |
|
||||||
|
| `AKERR_MAX_RESERVED_STATUS_RANGES` | `64` | How many status ranges may be reserved in one process. |
|
||||||
|
| `AKERR_SANITIZE` | *(empty)* | Sanitizer list applied to the library and the tests, e.g. `thread` or `address,undefined`. |
|
||||||
|
| `AKERR_COVERAGE` | `OFF` | Instrument the library with gcov counters. |
|
||||||
|
|
||||||
|
The two capacity options are applied `PRIVATE`: the tables live entirely in
|
||||||
|
`src/error.c`, so raising them never changes anything a consumer can see. See
|
||||||
|
[UPGRADING.md](../UPGRADING.md) for what happens when you exhaust them.
|
||||||
|
|
||||||
|
## Templating and autogenerated code
|
||||||
|
|
||||||
|
The build process relies upon `scripts/generrno.sh` which performs the following:
|
||||||
|
|
||||||
|
1. Executes `errno --list` and gathers up the output
|
||||||
|
1. Templates `include/akerror.tmpl.h` into `include/akerror.h` to set the `AKERR_LAST_ERRNO_VALUE` equal to the highest integer defined by `errno`
|
||||||
|
2. Generates `src/errno.c` which contains a function called by `akerr_init` which initializes all of the status names for the previously defined values of `errno`.
|
||||||
|
|
||||||
|
Neither output is meant to be edited. Change the template or the generator.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This library depends upon `stdlib`, and upon POSIX threads unless it is built
|
||||||
|
with `-DAKERR_THREADS=none` (see [Thread safety](thread-safety.md)). If you don't want to link against stdlib, you must modify the library code to include headers and link against a library that provides the following:
|
||||||
|
|
||||||
|
- `memset` function
|
||||||
|
- `strncpy` function
|
||||||
|
- `strlen` function
|
||||||
|
- `strcmp` function
|
||||||
|
- `sprintf` function
|
||||||
|
- `exit` function
|
||||||
|
- `bool` type
|
||||||
|
- `NULL` type
|
||||||
|
- `INT_MAX` constant
|
||||||
|
- `PATH_MAX` constant
|
||||||
|
|
||||||
|
... then you can compile it thusly:
|
||||||
|
|
||||||
|
```
|
||||||
|
cmake -S . -B build -DAKERR_USE_STDLIB=OFF
|
||||||
|
cmake --build build
|
||||||
|
cmake --install build
|
||||||
|
```
|
||||||
|
|
||||||
|
**Known defect:** that configuration does not currently compile. `bool`,
|
||||||
|
`PATH_MAX` and `NULL` are used unconditionally but only included under the
|
||||||
|
stdlib branch, so the header's includes need untangling before
|
||||||
|
`-DAKERR_USE_STDLIB=OFF` builds. The list above still states what a replacement
|
||||||
|
must provide. See [TODO.md](../TODO.md).
|
||||||
76
docs/exit-status.md
Normal file
76
docs/exit-status.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Exit status
|
||||||
|
|
||||||
|
**Never call `exit()` with an akerr status. Call `akerr_exit()`.**
|
||||||
|
|
||||||
|
```c
|
||||||
|
void akerr_exit(int status);
|
||||||
|
```
|
||||||
|
|
||||||
|
This applies everywhere you are leaving the process on account of a status, not
|
||||||
|
just in an unhandled-error handler: a CLI's top-level `HANDLE` block, an
|
||||||
|
initialization routine that cannot continue, a `main()` that ends by reporting
|
||||||
|
the status it finished with. One function owns the mapping so that a given
|
||||||
|
status produces the same exit code no matter which of your exits it left by.
|
||||||
|
|
||||||
|
The mapping exists because a process exit status is one byte wide. `exit()`
|
||||||
|
accepts an `int` and the kernel keeps the low 8 bits of it — `_exit()`,
|
||||||
|
`_Exit()`, `quick_exit()` and the raw `exit_group` syscall all behave
|
||||||
|
identically, and even `waitid()`, whose `si_status` is a full `int`, sees the
|
||||||
|
truncated value because the truncation happened before the parent looked. There
|
||||||
|
is no wider `exit()` to reach for.
|
||||||
|
|
||||||
|
That leaves 0 through 255 as the only statuses an exit code can carry, and
|
||||||
|
consumer statuses start at `AKERR_FIRST_CONSUMER_STATUS` (256) — so *no* consumer
|
||||||
|
status can be an exit code:
|
||||||
|
|
||||||
|
```
|
||||||
|
status exit code
|
||||||
|
0 0 (success)
|
||||||
|
1 .. 255 the status
|
||||||
|
negative, or > 255 AKERR_EXIT_STATUS_UNREPRESENTABLE (125)
|
||||||
|
```
|
||||||
|
|
||||||
|
Passing the low byte instead would have made status 256 exit 0 and report
|
||||||
|
success to the shell, and status 300 exit 44 — an unrelated error's code. 125 is
|
||||||
|
the conventional "the tool itself failed" status; 126, 127 and 128+*n* already
|
||||||
|
belong to the shell.
|
||||||
|
|
||||||
|
Status 0 exits 0, because 0 is this library's success status. That is not a hole
|
||||||
|
in the rule that an unhandled error never exits 0: `PROCESS` opens with `case
|
||||||
|
0`, which marks a zero status handled, so a successful context cannot reach
|
||||||
|
`FINISH_NORETURN`'s call to the handler at all.
|
||||||
|
|
||||||
|
Above 255, the exit code tells you the process died of an error, not which one.
|
||||||
|
125 is inside the library's reserved band and so is also some host's `errno`, and
|
||||||
|
every status above 255 collapses onto it. **The stack trace is what identifies
|
||||||
|
the error** — it is printed before the handler runs, carrying the status at full
|
||||||
|
width along with its registered name.
|
||||||
|
|
||||||
|
## Replacing the handler
|
||||||
|
|
||||||
|
After the trace is printed, `FINISH_NORETURN` calls
|
||||||
|
`akerr_handler_unhandled_error`. The default implementation,
|
||||||
|
`akerr_default_handler_unhandled_error()`, hands `errctx->status` to
|
||||||
|
`akerr_exit()` (a NULL context exits 1). Replace it if you need something else
|
||||||
|
to happen first — a core dump, a crash reporter, a flush — and finish by calling
|
||||||
|
`akerr_exit()` so the exit code still means what it means everywhere else:
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void mylib_handler(akerr_ErrorContext *e)
|
||||||
|
{
|
||||||
|
mylib_flush_telemetry();
|
||||||
|
if ( e == NULL ) {
|
||||||
|
akerr_exit(AKERR_API);
|
||||||
|
}
|
||||||
|
akerr_exit(e->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
akerr_handler_unhandled_error = &mylib_handler;
|
||||||
|
```
|
||||||
|
|
||||||
|
`akerr_exit()` is declared `AKERR_NORETURN`, so the compiler knows a handler
|
||||||
|
ending in one of those calls is complete rather than falling off the end.
|
||||||
|
|
||||||
|
Set the handler once, before you start any threads. `tests/err_custom_handler.c`
|
||||||
|
installs one that does not exit at all, which is how the test suite asserts on
|
||||||
|
unhandled errors without dying.
|
||||||
46
docs/status-codes.md
Normal file
46
docs/status-codes.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Error codes
|
||||||
|
|
||||||
|
The library uses integer values to specify error codes inside of its context. These integer return codes are defined in `akerror.h` in the form of `AKERR_xxxxx` where `xxxxx` is the name of the error code in question. See `akerror.h` for a list of defined errors and their descriptions.
|
||||||
|
|
||||||
|
You can define additional error types as integer constants. Values 0 through 255
|
||||||
|
are reserved by libakerror (the host's errno values plus the `AKERR_*` codes);
|
||||||
|
consumers allocate codes starting at `AKERR_FIRST_CONSUMER_STATUS` (256). Status
|
||||||
|
names are stored sparsely, so any `int` is a legal status and no compile
|
||||||
|
definition is needed to use large values. Note that no consumer status can be a
|
||||||
|
process exit code — see [Exit status](exit-status.md).
|
||||||
|
|
||||||
|
Every library that may coexist in one process must reserve its range during
|
||||||
|
initialization. `akerr_reserve_status_range()` and
|
||||||
|
`akerr_register_status_name()` report failure the way everything else in this
|
||||||
|
library does — they return `akerr_ErrorContext *`, and they are marked
|
||||||
|
`AKERR_NOIGNORE` — so a collision is an exception you can `CATCH`, `HANDLE`, or
|
||||||
|
`PASS` up out of your initialization:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define MYLIB_OWNER "my-library"
|
||||||
|
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *mylib_init(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
|
/* Another component owning part of the range propagates to our caller. */
|
||||||
|
PASS(errctx, akerr_reserve_status_range(256, 16, MYLIB_OWNER));
|
||||||
|
|
||||||
|
/* Then name each code, quoting the owner you reserved with. */
|
||||||
|
PASS(errctx, akerr_register_status_name(MYLIB_OWNER, 256,
|
||||||
|
"Some Error Code Description"));
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reservations are process-local, fixed-capacity, and idempotent when the same
|
||||||
|
owner repeats the exact same range. They preserve compile-time integer constants
|
||||||
|
so `HANDLE` still works, since `case` labels require them.
|
||||||
|
|
||||||
|
Naming a status outside your reservation raises `AKERR_STATUS_NAME_FOREIGN`, and
|
||||||
|
naming one nobody reserved raises `AKERR_STATUS_NAME_UNRESERVED`; a colliding
|
||||||
|
reservation raises `AKERR_STATUS_RANGE_OVERLAP`, with a message naming the real
|
||||||
|
owner. See [UPGRADING.md](../UPGRADING.md) for the full list of statuses these two
|
||||||
|
functions raise, the capacity limits and how to raise them, and thread-safety
|
||||||
|
rules.
|
||||||
|
|
||||||
225
docs/thread-safety.md
Normal file
225
docs/thread-safety.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# Thread safety
|
||||||
|
|
||||||
|
The library is thread safe as built by default. Every entry point may be called
|
||||||
|
from any thread at any time, including the first one: `akerr_init()` runs
|
||||||
|
exactly once no matter how many threads race into it.
|
||||||
|
|
||||||
|
What that covers:
|
||||||
|
|
||||||
|
* **The error pool.** Finding a free slot in `AKERR_ARRAY_ERROR` and taking its
|
||||||
|
reference is one operation under a lock, so two threads can never be handed
|
||||||
|
the same context. A context is then owned by exactly one thread at a time, all
|
||||||
|
the way through `CATCH`, `HANDLE`, and release.
|
||||||
|
* **The status registry.** Reservations and name registrations are serialized
|
||||||
|
against each other and against lookups. Two threads reserving the same range
|
||||||
|
cannot both win — exactly one gets `NULL` and the other gets
|
||||||
|
`AKERR_STATUS_RANGE_OVERLAP` naming the winner.
|
||||||
|
* **Per-thread state.** The context behind `IGNORE` (`__akerr_last_ignored`) and
|
||||||
|
the last-ditch context used to report `akerr_release_error(NULL)` are
|
||||||
|
thread-local, so one thread's ignored error is never another's.
|
||||||
|
* **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
|
||||||
|
outlives the thread that raised it. The reference count is the only field the
|
||||||
|
library reads across threads, and it is only ever touched under the pool lock,
|
||||||
|
so `akerr_release_error()` does not care which thread checked the slot out.
|
||||||
|
Raise on a worker, queue it, let the worker exit; the collector still has a
|
||||||
|
whole error with a whole stack trace. See
|
||||||
|
[Handing an error to another thread](#handing-an-error-to-another-thread).
|
||||||
|
|
||||||
|
What it does not cover, and cannot:
|
||||||
|
|
||||||
|
* **Two threads inside one context at the same time.** A context has one owner,
|
||||||
|
and only one. `FAIL` rewrites the message, `HANDLE` rewinds the stack-trace
|
||||||
|
cursor, and none of that is locked — two threads in one context splice their
|
||||||
|
messages together and truncate each other's trace, without crashing. Handing a
|
||||||
|
context *from* one thread *to* another is a different thing, and is supported.
|
||||||
|
* **`akerr_log_method` and `akerr_handler_unhandled_error`.** Set them during
|
||||||
|
startup, before you spawn threads. They are read on every error and the
|
||||||
|
library never writes them after initialization, so setting one while other
|
||||||
|
threads are raising errors is a race the library cannot mediate.
|
||||||
|
* **Renaming a status that other threads are looking up.**
|
||||||
|
`akerr_name_for_status(status, NULL)` returns a pointer into the registry,
|
||||||
|
valid for the life of the process; registering a *second* name for the same
|
||||||
|
status overwrites that buffer in place. Register names during initialization.
|
||||||
|
Registering a *new* status concurrently is fine.
|
||||||
|
* **Which unhandled error terminates the process.** An error that reaches
|
||||||
|
`FINISH_NORETURN` unhandled prints its stack trace and calls
|
||||||
|
`akerr_handler_unhandled_error`, which by default calls `akerr_exit()`. Each
|
||||||
|
thread's trace is whole — the buffer belongs to its context, and each line is
|
||||||
|
one call to `akerr_log_method` — but if two threads get there at the same
|
||||||
|
instant, both traces print and the exit status is whichever one won.
|
||||||
|
|
||||||
|
There is one lock, it is recursive, and it covers both the pool and the
|
||||||
|
registry. That means error construction is serialized across threads: raising an
|
||||||
|
error is the exceptional path, and correctness there is worth more than
|
||||||
|
throughput. A program that raises errors on its hot path will feel it.
|
||||||
|
|
||||||
|
`AKERR_THREAD_SAFE` in the generated header is `1` for a thread-safe build, so a
|
||||||
|
consumer can check what it linked against:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#if AKERR_THREAD_SAFE
|
||||||
|
/* ... start worker threads ... */
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handing an error to another thread
|
||||||
|
|
||||||
|
**Transfer** an error context; never **share** one. One thread owns it at a time,
|
||||||
|
and ownership moves in a single step: the thread giving it up stops touching it
|
||||||
|
in the same act that makes it visible to the thread taking it over.
|
||||||
|
|
||||||
|
This works because a context is not thread state. It lives in
|
||||||
|
`AKERR_ARRAY_ERROR`, which is process-global, so a context outlives the thread
|
||||||
|
that raised it — a worker can raise an error, queue it, and exit, and the
|
||||||
|
collector still has a whole error with a whole stack trace. Nothing in the
|
||||||
|
library reads a context through any pointer but the one its current owner handed
|
||||||
|
in. The one field it reads across threads is the reference count, and that is
|
||||||
|
only ever touched under the pool lock, so `akerr_release_error()` does not care
|
||||||
|
which thread checked the slot out. Release it wherever it ended up.
|
||||||
|
|
||||||
|
**Always** hand it over through something that synchronizes: a mutex, a
|
||||||
|
condition variable, `pthread_join`, or an acquire/release atomic. The content of
|
||||||
|
a context is written with no lock at all — that is deliberate, error
|
||||||
|
construction should not pay for a lock it does not need — so the handoff itself
|
||||||
|
is what publishes those writes. Push the pointer through a relaxed atomic or a
|
||||||
|
plain global and the receiver can read a half-written message, on a machine you
|
||||||
|
did not test on.
|
||||||
|
|
||||||
|
**Never** touch a context after you have given it away. Not a `->status`, not a
|
||||||
|
log line. The receiver may be inside `HANDLE` rewinding the trace cursor, or
|
||||||
|
inside `akerr_release_error()` memsetting the slot.
|
||||||
|
|
||||||
|
**Release it exactly once.** A context released twice from a stale pointer takes
|
||||||
|
the refcount-zero branch a second time and wipes the slot again — which by then
|
||||||
|
holds somebody else's live error. Nothing crashes; a different thread's error
|
||||||
|
just quietly goes blank.
|
||||||
|
|
||||||
|
```c
|
||||||
|
/* Producer. Owns the context until queue_push() returns, and not after. */
|
||||||
|
static void report_unit_failure(int unit)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
|
||||||
|
FAIL(e, MYLIB_UNIT_FAILED, "unit %d stopped responding", unit);
|
||||||
|
/* The last thing this thread does to it, so the trace records the crossing.
|
||||||
|
The buffer belongs to the context, so it travels with it. */
|
||||||
|
AKERR_STACKTRACE_APPEND(e, "%s:%s:%d: queued for the collector\n",
|
||||||
|
__FILE__, __func__, __LINE__);
|
||||||
|
queue_push(e); /* takes the queue mutex; `e` is not ours after this */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collector. Owns it from the moment queue_pop() returns. */
|
||||||
|
static void collect_one(akerr_ErrorContext *e)
|
||||||
|
{
|
||||||
|
ATTEMPT {
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} HANDLE(e, MYLIB_UNIT_FAILED) {
|
||||||
|
restart_unit(e);
|
||||||
|
} HANDLE_DEFAULT(e) {
|
||||||
|
LOG_ERROR_WITH_MESSAGE(e, "collector: unrecognized failure");
|
||||||
|
} FINISH_NORETURN(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *collector(void *unused)
|
||||||
|
{
|
||||||
|
akerr_ErrorContext *e;
|
||||||
|
|
||||||
|
(void)unused;
|
||||||
|
while ( (e = queue_pop()) != NULL ) {
|
||||||
|
collect_one(e);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Four things about the receiving side:
|
||||||
|
|
||||||
|
* **Declare a plain pointer, not `PREPARE_ERROR`.** That macro *declares* a
|
||||||
|
fresh context variable set to `NULL`; it cannot adopt one. For the same
|
||||||
|
reason, never `CATCH` into the variable holding a received context — `CATCH`
|
||||||
|
assigns over it, and the slot you were handed is gone.
|
||||||
|
* **In a `void` helper, use `FINISH_NORETURN`.** `FINISH_LOGIC` decides whether
|
||||||
|
to propagate at run time, so the compiler still *parses* its
|
||||||
|
`return __err_context` even when the second argument is the literal `false`.
|
||||||
|
`FINISH(e, false)` in a function returning void therefore draws
|
||||||
|
`warning: 'return' with a value, in function returning void` from gcc — a
|
||||||
|
constraint violation, and a build failure under `-Werror`. To propagate
|
||||||
|
inside the collector's own call stack, give the
|
||||||
|
helper an `akerr_ErrorContext *` return and use `FINISH(e, true)` as usual —
|
||||||
|
just never let an error propagate out of the thread body itself, whose
|
||||||
|
`void *` return nobody reads. `PASS` has the same problem for the same reason:
|
||||||
|
in a thread body it compiles, hands the pointer back as a `void *`, and leaks
|
||||||
|
the slot.
|
||||||
|
* **`FINISH_NORETURN(e)` on a received context still terminates the process.**
|
||||||
|
That is right — an unhandled error is unhandled wherever it was raised — but
|
||||||
|
it is now the collector's thread deciding the exit status, not the raiser's.
|
||||||
|
* **Give the handler blocks their own function.** `ATTEMPT` is a `switch`, and a
|
||||||
|
`break` inside one written directly in a loop leaves the `switch`, not the
|
||||||
|
loop. Same hazard as
|
||||||
|
[do not use CATCH or FAIL_*_BREAK inside a loop](usage.md#important-do-not-use-catch-or-fail__break-inside-a-loop).
|
||||||
|
|
||||||
|
**Always** bound the queue. A queued error is a checked-out pool slot, and there
|
||||||
|
are `AKERR_MAX_ARRAY_ERROR` (128) of them in the entire process. Size *queue
|
||||||
|
depth + producers with an error in flight* well under that. When the pool runs
|
||||||
|
dry the library logs and calls `exit(1)` from inside `FAIL` — there is no slot
|
||||||
|
left to raise the failure *from*, which is exactly why a collector that stops
|
||||||
|
draining takes the process with it.
|
||||||
|
|
||||||
|
### If you need to keep it as well as report it
|
||||||
|
|
||||||
|
There is no copy or retain call, on purpose: a context is a pool slot, and two
|
||||||
|
owners of one slot is the thing this whole section exists to prevent. So either
|
||||||
|
read out what you want to keep — `status` is an `int`, `message` and
|
||||||
|
`stacktracebuf` are ordinary NUL-terminated strings you can `snprintf` into your
|
||||||
|
own, much smaller, record — or raise two errors and hand one of them over.
|
||||||
|
|
||||||
|
**Never** copy an `akerr_ErrorContext` by assignment and keep the copy:
|
||||||
|
|
||||||
|
```c
|
||||||
|
akerr_ErrorContext snapshot = *failed; /* looks fine. is not. */
|
||||||
|
```
|
||||||
|
|
||||||
|
`stacktracebufptr` points into the context's *own* `stacktracebuf`, so after that
|
||||||
|
assignment `snapshot`'s cursor still points into `failed`'s buffer.
|
||||||
|
`LOG_ERROR(&snapshot)` reads the array and prints correctly, so it looks healthy
|
||||||
|
— and then the first `AKERR_STACKTRACE_APPEND(&snapshot, ...)` writes into a
|
||||||
|
pool slot you no longer own, arbitrarily far from the copy. `arrayid` has the
|
||||||
|
same shape: it is restored after the wipe, so a copied id makes the destination
|
||||||
|
impersonate the source's slot forever. And the copy is not a pool address, so
|
||||||
|
`akerr_valid_error_address()` rejects it, every `CATCH` on it becomes
|
||||||
|
`AKERR_BADEXC`, and releasing it memsets your own storage while the real slot
|
||||||
|
stays checked out for the life of the process.
|
||||||
|
|
||||||
|
## Building single threaded
|
||||||
|
|
||||||
|
The threading backend is chosen when libakerror is configured. `auto` (the
|
||||||
|
default) takes POSIX threads, and **fails the configure** if it cannot find
|
||||||
|
them rather than quietly producing a library that says it is thread safe and is
|
||||||
|
not. To mean it:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build -DAKERR_THREADS=none
|
||||||
|
```
|
||||||
|
|
||||||
|
That builds with no locking and no thread-local storage, stamps
|
||||||
|
`AKERR_THREAD_SAFE 0` into the header, and calling the library from more than
|
||||||
|
one thread is then undefined.
|
||||||
|
|
||||||
|
## Proving it
|
||||||
|
|
||||||
|
The thread tests (`tests/err_threads_*.c`) assert the properties above directly:
|
||||||
|
exclusive ownership of pool slots, exactly one winner for a contested range,
|
||||||
|
every registered name readable back, and — in `err_threads_handoff.c` — an error
|
||||||
|
raised on one thread arriving whole on another and released there. They run in
|
||||||
|
the normal suite. The run that
|
||||||
|
proves the *absence* of a data race underneath them is ThreadSanitizer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scripts/thread_test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
which configures `build/tsan` with `-DAKERR_SANITIZE=thread`, builds the library
|
||||||
|
and every test with it, and runs the suite. Under that build a sanitizer report
|
||||||
|
fails the test rather than being printed and passed over.
|
||||||
50
docs/uncaught-errors.md
Normal file
50
docs/uncaught-errors.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Uncaught errors
|
||||||
|
|
||||||
|
## Misbehaving methods
|
||||||
|
|
||||||
|
Any function which returns `akerr_ErrorContext *` and completes successfully MUST call `SUCCEED_RETURN(errctx)`. Failure to do this may result in an invalid `akerr_ErrorContext *` being returned, which will cause an `AKERR_BEHAVIOR` error to be triggered from your code.
|
||||||
|
|
||||||
|
## Ensuring that all error codes are captured
|
||||||
|
|
||||||
|
Any function which returns `akerr_ErrorContext *` should also be marked with `AKERR_NOIGNORE`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *f(...);
|
||||||
|
```
|
||||||
|
|
||||||
|
This will cause a compile-time error if the return value of such a function is not used. "Used" here means assigned to a variable - it does not necessarily mean that the value is checked. However assuming that such functions are called inside of `ATTEMPT { ... }` blocks, it is safe to assume that such returns will be caught with `CATCH(...)`; therefore this error is a generally effective safeguard against careless coding where errors are not checked.
|
||||||
|
|
||||||
|
Beware that `AKERR_NOIGNORE` is not a failsafe - it implements the `warn_unused_result` mechanic. By design users may explicitly ignore an error code from a function marked with `warn_unused_result` by explicitly casting the return to `void`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stack Traces
|
||||||
|
|
||||||
|
Whenever an error is captured using the `FAIL_*` or `CATCH` methods, and is unhandled such that it manages to propagate all the way to the top of the caller stack without being managed, the last `FINISH` macro to touch the error will trigger a stack trace and kill the program.
|
||||||
|
|
||||||
|
Consider the `tests/err_trace.c` program which intentionally triggers this behavior. It produces output like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/err_trace.c:func2:7: 1 (Null Pointer Error) : This is a failure in func2
|
||||||
|
tests/err_trace.c:func2:10
|
||||||
|
tests/err_trace.c:func1:18: Detected error 0 from array (refcount 1)
|
||||||
|
tests/err_trace.c:func1:18
|
||||||
|
tests/err_trace.c:func1:21
|
||||||
|
tests/err_trace.c:main:30: Detected error 0 from array (refcount 1)
|
||||||
|
tests/err_trace.c:main:30
|
||||||
|
tests/err_trace.c:main:33: Unhandled Error 1 (Null Pointer Error): This is a failure in func2
|
||||||
|
```
|
||||||
|
|
||||||
|
From bottom to top, we have:
|
||||||
|
|
||||||
|
* The last line printed is the `FINISH` macro call that triggered the stacktrace.
|
||||||
|
* Above that, the `CATCH()` inside of `main()` which caught the exception from `func1()` but did not handle it
|
||||||
|
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
||||||
|
* Above that, the `FINISH()` macro in the `func1` method which detected the presence of an unhandled error and returned it up the calling stack
|
||||||
|
* Above that, the `CATCH()` macro in the `func1` method which caught the error coming out of `func2()`
|
||||||
|
* Above that, a statement that the error was detected in the `CATCH()` statement at the same line
|
||||||
|
* Above that, the `FINISH()` macro in `func2()` which detected an unhandled error and passed it out of the function
|
||||||
|
* Above that, a reference to the line where the `FAIL()` macro set the error code and provided the message which is printed here
|
||||||
|
|
||||||
238
docs/usage.md
Normal file
238
docs/usage.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
# Using the library
|
||||||
|
|
||||||
|
## (Optional) Configuring the logging function
|
||||||
|
|
||||||
|
The default logging function (used for logging stack traces on failure) defaults to a wrapper that calls `fprintf(stderr, f, ...)`. If you want to override this behavior, then set the error handler to a function with a printf-style signature:
|
||||||
|
|
||||||
|
```
|
||||||
|
void my_logger(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
/* ... do something */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* set your custom error handler */
|
||||||
|
akerr_log_method = &my_logger;
|
||||||
|
|
||||||
|
/* proceed to use the library */
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setting Up the Error Context
|
||||||
|
|
||||||
|
Before you can use any of these macros you must set up an error context inside of the current scope.
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
This will create a akerr_ErrorContext structure inside of the current scope named `errctx` and initialize it. This structure is used for all operations of the library within the current scope. Attempting to use the library in a given scope before calling this will result in compile-time errors.
|
||||||
|
|
||||||
|
## Attempting an Operation
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
// ... code
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true)
|
||||||
|
```
|
||||||
|
|
||||||
|
`ATTEMPT { ... }` is the block within which you will perform operations which may cause errors that need to be caught. See "Capturing errors", below.
|
||||||
|
|
||||||
|
`CLEANUP { ... }` is the block within which you will perform any code which MUST be executed REGARDLESS of whether or not errors were thrown. Closing open file handles, or releasing memory, for example.
|
||||||
|
|
||||||
|
`PROCESS(errctx) { ... }` is the block within which you will handle any errors that were caught inside of the `ATTEMPT` block. See "Handling Errors" below.
|
||||||
|
|
||||||
|
`FINISH(errctx, true)` terminates the attempt operation. The `FINISH` macro takes two arguments: the name of the akerr_ErrorContext, and a boolean regarding whether or not to pass unhandled errors up to the calling function. Unless you are inside of your `main()` method, this should be true. Inside of your `main()` method, call `FINISH_NORETURN(errctx)` instead.
|
||||||
|
|
||||||
|
|
||||||
|
## Capturing errors
|
||||||
|
|
||||||
|
Inside of an `ATTEMPT` block, any operation which could generate or represent an error should be wrapped in one of several macros.
|
||||||
|
|
||||||
|
### Capturing errors from functions which return akerr_ErrorContext *
|
||||||
|
|
||||||
|
For functions that return `akerr_ErrorContext *`, you should use the `CATCH` macro.
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, errorGeneratingFunction())
|
||||||
|
} // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This will assign the return value of the function in question to the akerr_ErrorContext previously prepared in the current scope. If the function returns an akerr_ErrorContext that indicates any type of error, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||||||
|
|
||||||
|
(One caveat: because this exit is implemented with a C `break`, `CATCH` must not be used inside a loop within the `ATTEMPT` block — see the section "Important: do not use CATCH or FAIL_*_BREAK inside a loop" below.)
|
||||||
|
|
||||||
|
### Setting errors from functions or expressions returning integer
|
||||||
|
|
||||||
|
For functions that return integer, such as logical comparisons or most standard library functions, use the `FAIL_ZERO_BREAK` and `FAIL_NONZERO_BREAK` macros. These macros allow you to capture an integer return code from an expression or function and set an error code in the current context based off that return.
|
||||||
|
|
||||||
|
Here is an example of checking for a NULL pointer
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
FAIL_ZERO_BREAK(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||||||
|
} // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is an example of checking for two strings that are not equal
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
FAIL_NONZERO_BREAK(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||||||
|
} // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
When either of these two macros are used, the `ATTEMPT` block is immediately exited, and the `CLEANUP` block begins.
|
||||||
|
|
||||||
|
### Important: do not use CATCH or FAIL_*_BREAK inside a loop
|
||||||
|
|
||||||
|
`CATCH`, `FAIL_ZERO_BREAK`, `FAIL_NONZERO_BREAK`, and `FAIL_BREAK` leave the `ATTEMPT` block by executing a C `break` statement. In C, `break` only exits the *innermost* enclosing `for`, `while`, `do`, or `switch`. Therefore **these macros must not be used inside a loop (or a nested `switch`) that is itself inside an `ATTEMPT` block.** If you do, the `break` escapes only the loop — not the `ATTEMPT` — and the rest of the `ATTEMPT` body then runs with an error already pending:
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
for ( int i = 0; i < n; i++ ) {
|
||||||
|
CATCH(errctx, process(items[i])); // WRONG: break exits the for loop, not the ATTEMPT
|
||||||
|
}
|
||||||
|
// ... this code still executes, with errctx already in an error state ...
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that moving the loop into a helper function does **not** fix this on its own — if the helper still wraps the loop in an `ATTEMPT` and uses `CATCH`/`FAIL_*_BREAK` inside it, it has the exact same problem. The fix is to iterate with `return`-based macros, which are unaffected by loop nesting. Use one of the two patterns below.
|
||||||
|
|
||||||
|
**Pattern 1 — use `PASS` (or a `FAIL_*_RETURN` macro) inside the loop.** These exit the *enclosing function* with a `return` rather than a `break`, so loop nesting is irrelevant. Use this when the loop should stop and propagate on the first error:
|
||||||
|
|
||||||
|
```c
|
||||||
|
akerr_ErrorContext AKERR_NOIGNORE *process_all(Item *items, int n)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
for ( int i = 0; i < n; i++ ) {
|
||||||
|
PASS(errctx, process(items[i])); // returns from process_all on the first error
|
||||||
|
}
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern 2 — move the loop into a helper and `CATCH` the single call.** When you need a `CLEANUP` block or want to `HANDLE` the error locally, put the loop in its own `akerr_ErrorContext *`-returning function (written per Pattern 1) and `CATCH` that one call. The `CATCH` is then not inside a loop, so its `break` scopes to the `ATTEMPT` correctly:
|
||||||
|
|
||||||
|
```c
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, process_all(items, n)); // a single CATCH, not looped
|
||||||
|
} CLEANUP {
|
||||||
|
// ... always runs ...
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_VALUE) {
|
||||||
|
// ... handle a failure from any iteration ...
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Passing errors
|
||||||
|
|
||||||
|
Sometimes you can't actually do anything about the errors that come out of a given method, but you want that error to be propagated back up the call chain, and to be properly reported. If this is your goal, you can avoid using a `ATTEMPT ... FINISH` block, and simply use the `PASS` macro.
|
||||||
|
|
||||||
|
```
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
PASS(e, some_method_that_may_fail());
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
```
|
||||||
|
|
||||||
|
This does the same thing as this, but with less code:
|
||||||
|
|
||||||
|
```
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(e, some_method_that_may_fail());
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
} FINISH(e, true);
|
||||||
|
SUCCEED_RETURN(e);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Handling errors
|
||||||
|
|
||||||
|
Inside of the `PROCESS { ... }` block, you must handle any errors that occurred during the `ATTEMPT { ... }` block. You do this with `HANDLE`, `HANDLE_GROUP`, and `HANDLE_DEFAULT`.
|
||||||
|
|
||||||
|
### Handling a specific error with HANDLE
|
||||||
|
|
||||||
|
In order to handle a specific error code, use the `HANDLE` macro.
|
||||||
|
|
||||||
|
```c
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_NULLPOINTER) {
|
||||||
|
// Something is complaining about a null pointer error. Do something about it.
|
||||||
|
} // ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling a group of errors with HANDLE_GROUP
|
||||||
|
|
||||||
|
In order to handle a group of related errors that all require the same failure behavior, use `HANDLE` followed by `HANDLE_GROUP`. For example, to handle a scenario where an IO error, key error, and index error all need to be handled the same way:
|
||||||
|
|
||||||
|
```c
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_IO) {
|
||||||
|
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||||||
|
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||||||
|
// error handling code goes here
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a fallthrough mechanism where all 3 errors get the same error handling code. Note that while the cases fall through, you can still (if desired) put some code specific to each error in that error's `HANDLE` or `HANDLE_GROUP` block; but this is not required, only the final handler needs to get any code.
|
||||||
|
|
||||||
|
The fallthrough behavior stops as soon as another `HANDLE` macro is encountered. For example, in this example, `AKERR_IO`, `AKERR_KEY` and `AKERR_INDEX` are all handled as a group, but `AKERR_RELATIONSHIP` is not.
|
||||||
|
|
||||||
|
```c
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} HANDLE(errctx, AKERR_IO) {
|
||||||
|
} HANDLE_GROUP(errctx, AKERR_KEY) {
|
||||||
|
} HANDLE_GROUP(errctx, AKERR_INDEX) {
|
||||||
|
// This code handles 3 error cases
|
||||||
|
} HANDLE(errctx, AKERR_RELATIONSHIP) {
|
||||||
|
// This code handles 1 error case
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Returning success or failure from functions returning akerr_ErrorContext *
|
||||||
|
|
||||||
|
If at all possible, when using this library, your functions should return `akerr_ErrorContext *`. When returning from such functions, you should use the `SUCCEED_RETURN` and `FAIL_RETURN` macros.
|
||||||
|
|
||||||
|
### SUCCEED_RETURN
|
||||||
|
|
||||||
|
This macro is used when your function has reached the end of its happy code path and is prepared to exit successfully. This sets the akerr_ErrorContext to a successful state and exits the function.
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
ATTEMPT {
|
||||||
|
// ... stuff
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH(errctx, true);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
### FAIL_RETURN
|
||||||
|
|
||||||
|
If the code path in the current function reaches a state wherein an error must be set and the function must return early, you can use `FAIL_RETURN` to accomplish this. Note that this should not be used inside of an `ATTEMPT { ... }` block; this immediately exits the function, preventing a `CLEANUP { ... }` block from executing. This can be safely used from inside of a `CLEANUP` or `PROCESS` block, or from anywhere within the function not inside of an `ATTEMPT { ... }` block.
|
||||||
|
|
||||||
|
The function allows you to provide printf-style variable arguments to provide a meaningful failure message.
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_RETURN(AKERR_BEHAVIOR, "Something went horribly wrong!")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Conditionally failing and returning
|
||||||
|
|
||||||
|
In addition to `FAIL_RETURN` you can also test for zero or non-zero conditions, set an error, and return from the function immediately. Use the `FAIL_ZERO_RETURN` and `FAIL_NONZERO_RETURN` macros for this. These macros can be used anywhere that `FAIL_RETURN` can be used.
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_ZERO_RETURN(errctx, (somePointer != NULL), AKERR_NULLPOINTER, "Someone gave me a NULL pointer")
|
||||||
|
```
|
||||||
|
|
||||||
|
```c
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_NONZERO_RETURN(errctx, strcmp("not", "equal"), AKERR_VALUE, "Strings are not equal")
|
||||||
|
```
|
||||||
@@ -9,6 +9,42 @@
|
|||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Threading.
|
||||||
|
*
|
||||||
|
* scripts/generrno.sh stamps this value in at build time from the AKERR_THREADS
|
||||||
|
* build option, the same way it stamps AKERR_LAST_ERRNO_VALUE. It is generated
|
||||||
|
* rather than defined by the consumer on purpose: whether the library
|
||||||
|
* serializes its global state and whether __akerr_last_ignored is a
|
||||||
|
* thread-local are the same decision, and a consumer that disagreed with the
|
||||||
|
* library about it would link against a differently shaped symbol.
|
||||||
|
*
|
||||||
|
* 1 The error pool and the status registry are mutex protected, and the
|
||||||
|
* per-thread state below is thread local. Every entry point may be called
|
||||||
|
* from any thread. See docs/thread-safety.md for what that does and does
|
||||||
|
* not cover.
|
||||||
|
* 0 The library was built -DAKERR_THREADS=none for a single-threaded
|
||||||
|
* process: no locking, no thread-local storage, and calling it from more
|
||||||
|
* than one thread is undefined.
|
||||||
|
*
|
||||||
|
* Consumers can test it: #if AKERR_THREAD_SAFE.
|
||||||
|
*/
|
||||||
|
#define AKERR_THREAD_SAFE AKERR_THREAD_SAFE_SED
|
||||||
|
|
||||||
|
#if AKERR_THREAD_SAFE == 1
|
||||||
|
#if defined(__GNUC__) || defined(__clang__)
|
||||||
|
#define AKERR_THREAD_LOCAL __thread
|
||||||
|
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
|
||||||
|
#define AKERR_THREAD_LOCAL _Thread_local
|
||||||
|
#elif defined(_MSC_VER)
|
||||||
|
#define AKERR_THREAD_LOCAL __declspec(thread)
|
||||||
|
#else
|
||||||
|
#error "libakerror was built thread safe, but this compiler has no thread-local storage specifier that akerror.h knows about. Rebuild libakerror with -DAKERR_THREADS=none, or add the spelling here."
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
#define AKERR_THREAD_LOCAL
|
||||||
|
#endif
|
||||||
|
|
||||||
// FIXME: This is huge now. It used to be 1000 bytes, then I wanted to report errors
|
// FIXME: This is huge now. It used to be 1000 bytes, then I wanted to report errors
|
||||||
// related to filesystem paths, which made it grow beyond PATH_MAX, then I started
|
// related to filesystem paths, which made it grow beyond PATH_MAX, then I started
|
||||||
// reporting messages including 2 file paths (PATH_MAX * 2), so now to make the compiler warnings
|
// reporting messages including 2 file paths (PATH_MAX * 2), so now to make the compiler warnings
|
||||||
@@ -77,6 +113,26 @@
|
|||||||
*/
|
*/
|
||||||
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1];
|
typedef char akerr_assert_codes_within_reserved_band[(AKERR_LAST_LIBRARY_STATUS < 256) ? 1 : -1];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A process exit status is one byte wide. exit() takes an int, but the kernel
|
||||||
|
* keeps only the low 8 bits of it and throws the rest away, so 0 through 255
|
||||||
|
* are the only statuses that can also be exit codes -- and every consumer status
|
||||||
|
* begins at AKERR_FIRST_CONSUMER_STATUS (256). No wider variant exists to reach
|
||||||
|
* for: _exit(), _Exit(), quick_exit() and the raw exit_group syscall all
|
||||||
|
* truncate identically, and even waitid()'s int-wide si_status reports the
|
||||||
|
* truncated value, because the truncation happened before the parent looked.
|
||||||
|
*
|
||||||
|
* akerr_exit() therefore substitutes AKERR_EXIT_STATUS_UNREPRESENTABLE for any
|
||||||
|
* status it cannot deliver intact, rather than passing the low byte -- status
|
||||||
|
* 256 would exit 0 and report success. 125 is the conventional "the tool itself
|
||||||
|
* failed" code (126, 127 and 128+n belong to the shell). It is inside the
|
||||||
|
* library's reserved band, so it is also some host's errno: the exit code says
|
||||||
|
* only that the process died of an error, and the stack trace carries the real
|
||||||
|
* status.
|
||||||
|
*/
|
||||||
|
#define AKERR_EXIT_STATUS_MAX 255
|
||||||
|
#define AKERR_EXIT_STATUS_UNREPRESENTABLE 125
|
||||||
|
|
||||||
#define AKERR_MAX_ARRAY_ERROR 128
|
#define AKERR_MAX_ARRAY_ERROR 128
|
||||||
|
|
||||||
|
|
||||||
@@ -96,16 +152,53 @@ typedef struct
|
|||||||
} akerr_ErrorContext;
|
} akerr_ErrorContext;
|
||||||
|
|
||||||
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
#define AKERR_NOIGNORE __attribute__((warn_unused_result))
|
||||||
|
/* akerr_exit() does not come back, and the compiler should know it: a handler
|
||||||
|
* whose last statement is a call to it is complete, not falling off the end. */
|
||||||
|
#define AKERR_NORETURN __attribute__((noreturn))
|
||||||
|
|
||||||
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
|
typedef void (*akerr_ErrorUnhandledErrorHandler)(akerr_ErrorContext *errctx);
|
||||||
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
|
typedef void (*akerr_ErrorLogFunction)(const char *f, ...);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The pool. Process-global, not thread-local: a context outlives the thread that
|
||||||
|
* raised it, which is what lets one be handed to another thread and released
|
||||||
|
* there.
|
||||||
|
*/
|
||||||
extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
extern akerr_ErrorContext AKERR_ARRAY_ERROR[AKERR_MAX_ARRAY_ERROR];
|
||||||
|
/*
|
||||||
|
* Set these before starting threads. They are read on every error and written
|
||||||
|
* by nothing but your own code, so changing one while other threads are raising
|
||||||
|
* errors is a data race the library cannot mediate.
|
||||||
|
*/
|
||||||
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;
|
||||||
extern akerr_ErrorContext *__akerr_last_ignored;
|
/*
|
||||||
|
* The error IGNORE() last swallowed, per thread: an ignored error is a fact
|
||||||
|
* about the thread that ignored it, and one shared slot would have two threads
|
||||||
|
* overwriting each other's. Thread local only when AKERR_THREAD_SAFE is 1.
|
||||||
|
*/
|
||||||
|
extern AKERR_THREAD_LOCAL akerr_ErrorContext *__akerr_last_ignored;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Drop one reference, returning NULL once the last one is gone so the caller can
|
||||||
|
* null its own pointer.
|
||||||
|
*
|
||||||
|
* This need not be the thread that checked the context out. The reference count
|
||||||
|
* is the only field the library reads across threads, and it is only ever
|
||||||
|
* touched under the pool lock, so a context handed to another thread is released
|
||||||
|
* there. Exactly once, though: releasing a stale pointer takes the
|
||||||
|
* refcount-zero branch a second time and wipes a slot that by then holds
|
||||||
|
* somebody else's live error.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_release_error(akerr_ErrorContext *ptr);
|
||||||
|
/*
|
||||||
|
* Check a context out of the pool. The returned context already carries one
|
||||||
|
* reference: finding a free slot and claiming it is a single operation under
|
||||||
|
* the pool lock, because two threads scanning at once would otherwise be handed
|
||||||
|
* the same slot. Release it with akerr_release_error() (or let RELEASE_ERROR,
|
||||||
|
* SUCCEED_RETURN or FINISH do it for you). Returns NULL when every slot is
|
||||||
|
* checked out.
|
||||||
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_next_error();
|
||||||
/*
|
/*
|
||||||
* Look up (name == NULL) or register (name != NULL) the display name for a
|
* Look up (name == NULL) or register (name != NULL) the display name for a
|
||||||
@@ -136,6 +229,25 @@ akerr_ErrorContext AKERR_NOIGNORE *akerr_register_status_name(const char *owner,
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
akerr_ErrorContext AKERR_NOIGNORE *akerr_reserve_status_range(int first_status, int count, const char *owner);
|
||||||
void akerr_init();
|
void akerr_init();
|
||||||
|
/*
|
||||||
|
* Terminate the process, reporting `status`. Use this instead of exit()
|
||||||
|
* anywhere you are leaving on account of an akerr status -- an unhandled-error
|
||||||
|
* handler of your own, a CLI's top-level HANDLE block, an init routine that
|
||||||
|
* cannot continue -- so that every exit out of the library's status space maps
|
||||||
|
* the same way.
|
||||||
|
*
|
||||||
|
* Exits with `status` when 0 <= status <= AKERR_EXIT_STATUS_MAX, and with
|
||||||
|
* AKERR_EXIT_STATUS_UNREPRESENTABLE otherwise (see above). Status 0 exits 0:
|
||||||
|
* zero is this library's success status, and passing it here says the program
|
||||||
|
* finished, not that it failed with a code that got lost.
|
||||||
|
*/
|
||||||
|
void AKERR_NORETURN akerr_exit(int status);
|
||||||
|
/*
|
||||||
|
* The default akerr_handler_unhandled_error: logs nothing further -- the stack
|
||||||
|
* trace has already been printed by the time it runs -- and hands `ptr->status`
|
||||||
|
* to akerr_exit(), or exits 1 when `ptr` is NULL. Replace it if you need a
|
||||||
|
* different mapping, and call akerr_exit() from your replacement.
|
||||||
|
*/
|
||||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
void akerr_default_handler_unhandled_error(akerr_ErrorContext *ptr);
|
||||||
void akerr_default_logger(const char *f, ...);
|
void akerr_default_logger(const char *f, ...);
|
||||||
int akerr_valid_error_address(akerr_ErrorContext *ptr);
|
int akerr_valid_error_address(akerr_ErrorContext *ptr);
|
||||||
@@ -173,6 +285,11 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
|
|||||||
akerr_init(); \
|
akerr_init(); \
|
||||||
akerr_ErrorContext __attribute__ ((unused)) *__err_context = NULL;
|
akerr_ErrorContext __attribute__ ((unused)) *__err_context = NULL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* akerr_next_error() hands back a context that already holds one reference --
|
||||||
|
* it has to, or a second thread could be given the same slot between the scan
|
||||||
|
* and the increment. There is nothing to increment here.
|
||||||
|
*/
|
||||||
#define ENSURE_ERROR_READY(__err_context) \
|
#define ENSURE_ERROR_READY(__err_context) \
|
||||||
if ( __err_context == NULL ) { \
|
if ( __err_context == NULL ) { \
|
||||||
__err_context = akerr_next_error(); \
|
__err_context = akerr_next_error(); \
|
||||||
@@ -180,7 +297,6 @@ akerr_ErrorContext AKERR_NOIGNORE *__akerr_copy_string(char *destination, int ca
|
|||||||
akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \
|
akerr_log_method("%s:%s:%d: Unable to pull an error context from the array!", __FILE__, (char *)__func__, __LINE__); \
|
||||||
exit(1); \
|
exit(1); \
|
||||||
} \
|
} \
|
||||||
__err_context->refcount += 1; \
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
srcdir=$1
|
srcdir=$1
|
||||||
outdir=$2
|
outdir=$2
|
||||||
|
# 1 when the library was configured with a threading backend, 0 for
|
||||||
|
# -DAKERR_THREADS=none. Stamped into the header so a consumer cannot disagree
|
||||||
|
# with the library about whether it locks and whether its per-thread state is
|
||||||
|
# thread local. Defaults to 1 for a hand-run of this script.
|
||||||
|
thread_safe=${3:-1}
|
||||||
|
|
||||||
|
if [ "${thread_safe}" != "0" ] && [ "${thread_safe}" != "1" ]; then
|
||||||
|
echo "$0: thread-safe argument must be 0 or 1, got '${thread_safe}'" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p ${outdir}/src
|
mkdir -p ${outdir}/src
|
||||||
mkdir -p ${outdir}/include
|
mkdir -p ${outdir}/include
|
||||||
@@ -28,4 +38,6 @@ errno --list | while read LINE; do
|
|||||||
echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
echo " __akerr_name_library_status(${define}, \"${desc}\");" >> ${outdir}/src/errno.c ;
|
||||||
done;
|
done;
|
||||||
echo "}" >> ${outdir}/src/errno.c
|
echo "}" >> ${outdir}/src/errno.c
|
||||||
sed "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" ${srcdir}/include/akerror.tmpl.h > ${outdir}/include/akerror.h
|
sed -e "s/#define AKERR_LAST_ERRNO_VALUE .*/#define AKERR_LAST_ERRNO_VALUE ${maxval}/" \
|
||||||
|
-e "s/#define AKERR_THREAD_SAFE .*/#define AKERR_THREAD_SAFE ${thread_safe}/" \
|
||||||
|
${srcdir}/include/akerror.tmpl.h > ${outdir}/include/akerror.h
|
||||||
|
|||||||
45
scripts/thread_test.sh
Executable file
45
scripts/thread_test.sh
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Run the test suite under ThreadSanitizer.
|
||||||
|
#
|
||||||
|
# The thread tests assert properties -- exclusive ownership of pool slots and of
|
||||||
|
# reserved status ranges -- that hold or fail without any tooling. This is the
|
||||||
|
# run that proves the absence of a data race underneath them, so it is the one
|
||||||
|
# that has to be easy to type.
|
||||||
|
#
|
||||||
|
# Usage: scripts/thread_test.sh [build directory]
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
BUILD_DIR=${1:-build/tsan}
|
||||||
|
SANITIZE=${AKERR_SANITIZE:-thread}
|
||||||
|
|
||||||
|
# Work from the repository root whatever directory this was invoked from, so a
|
||||||
|
# relative build directory always lands in the same place.
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
for tool in cmake ctest; do
|
||||||
|
if ! command -v "${tool}" >/dev/null 2>&1; then
|
||||||
|
echo "$0: ${tool} is required and was not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ThreadSanitizer maps its shadow memory at fixed addresses and aborts with
|
||||||
|
# "FATAL: ThreadSanitizer: unexpected memory mapping" on kernels configured for
|
||||||
|
# more ASLR entropy than it allows (vm.mmap_rnd_bits > 28, the default on
|
||||||
|
# several recent distributions). Running with ASLR disabled sidesteps it without
|
||||||
|
# needing root, which "sysctl -w vm.mmap_rnd_bits=28" would.
|
||||||
|
RUNNER=()
|
||||||
|
if command -v setarch >/dev/null 2>&1; then
|
||||||
|
RUNNER=(setarch -R)
|
||||||
|
else
|
||||||
|
echo "$0: setarch not found; if ThreadSanitizer aborts with an unexpected" \
|
||||||
|
"memory mapping, lower vm.mmap_rnd_bits to 28" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmake -S . -B "${BUILD_DIR}" -DAKERR_SANITIZE="${SANITIZE}"
|
||||||
|
cmake --build "${BUILD_DIR}"
|
||||||
|
"${RUNNER[@]}" ctest --test-dir "${BUILD_DIR}" --output-on-failure "${@:2}"
|
||||||
270
src/error.c
270
src/error.c
@@ -1,15 +1,44 @@
|
|||||||
#include "akerror.h"
|
#include "akerror.h"
|
||||||
|
#include "lock.h"
|
||||||
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
|
#if defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#endif // AKERR_USE_STDLIB
|
#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_ErrorUnhandledErrorHandler akerr_handler_unhandled_error;
|
||||||
akerr_ErrorLogFunction akerr_log_method = NULL;
|
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.
|
* Status-name registry.
|
||||||
*
|
*
|
||||||
@@ -97,6 +126,14 @@ akerr_ErrorContext *__akerr_copy_string(char *destination, int capacity,
|
|||||||
* A range test also accepts pointers into the *interior* of an element, which
|
* 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
|
* 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.
|
* 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.
|
||||||
|
*
|
||||||
|
* NULL reads back as valid, because the caller is VALID() asking whether a
|
||||||
|
* function returned something it has no business returning, and NULL is how a
|
||||||
|
* function says it succeeded. Anything reading this as "is this a pool slot"
|
||||||
|
* must check for NULL itself.
|
||||||
*/
|
*/
|
||||||
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
int akerr_valid_error_address(akerr_ErrorContext *ptr)
|
||||||
{
|
{
|
||||||
@@ -156,25 +193,48 @@ void __akerr_name_library_status(int status, const char *name)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Idempotent. `inited` is set before any work so that the registry calls below
|
* The calling thread's last-ditch context.
|
||||||
* -- and the public registry entry points, which all call akerr_init() so that
|
*
|
||||||
* a consumer reserving its range before anything else touches the library
|
* Thread-local storage starts zeroed, so a NULL stack-trace cursor means this
|
||||||
* cannot have that reservation wiped by a later first-use of the pool -- see
|
* thread has not used its copy yet. Checking the cursor rather than a separate
|
||||||
* themselves as already initialized instead of recursing.
|
* 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.
|
||||||
*/
|
*/
|
||||||
void akerr_init()
|
static akerr_ErrorContext *akerr_last_ditch_context(void)
|
||||||
{
|
{
|
||||||
static int inited = 0;
|
if ( __akerr_last_ditch.stacktracebufptr == NULL ) {
|
||||||
if ( inited == 0 ) {
|
memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext));
|
||||||
inited = 1;
|
__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++ ) {
|
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||||
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
|
memset((void *)&AKERR_ARRAY_ERROR[i], 0x00, sizeof(akerr_ErrorContext));
|
||||||
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;
|
__akerr_last_ignored = NULL;
|
||||||
memset((void *)&__akerr_last_ditch, 0x00, sizeof(akerr_ErrorContext));
|
(void)akerr_last_ditch_context();
|
||||||
__akerr_last_ditch.stacktracebufptr = (char *)&__akerr_last_ditch.stacktracebuf;
|
|
||||||
if ( akerr_log_method == NULL ) {
|
if ( akerr_log_method == NULL ) {
|
||||||
akerr_log_method = &akerr_default_logger;
|
akerr_log_method = &akerr_default_logger;
|
||||||
}
|
}
|
||||||
@@ -226,34 +286,113 @@ void akerr_init()
|
|||||||
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
#if (defined(AKERR_USE_STDLIB) && AKERR_USE_STDLIB == 1) || (!defined(AKERR_USE_STDLIB))
|
||||||
akerr_init_errno();
|
akerr_init_errno();
|
||||||
#endif
|
#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()
|
||||||
|
{
|
||||||
|
if ( akerr_initializing != 0 ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
akerr_once(&akerr_state_once, &akerr_init_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Every way out of the library's status space goes through here, so that a
|
||||||
|
* status becomes an exit code exactly one way no matter who is leaving.
|
||||||
|
*
|
||||||
|
* Only 0 through AKERR_EXIT_STATUS_MAX survive the trip -- see the note on
|
||||||
|
* AKERR_EXIT_STATUS_UNREPRESENTABLE in the header for why there is no wider
|
||||||
|
* exit() to reach for. Everything else exits with that sentinel instead of its
|
||||||
|
* low byte, because the low byte is either a lie (status 300 exiting 44, which
|
||||||
|
* is some other error's code) or a disaster (status 256, the first status a
|
||||||
|
* consumer can own, exiting 0 and telling the shell the program succeeded).
|
||||||
|
*
|
||||||
|
* Status 0 exits 0, because 0 is this library's success status and an exit code
|
||||||
|
* of 0 is what success is called out here. What keeps an *unhandled* error from
|
||||||
|
* exiting 0 is not this function: PROCESS opens with `case 0`, which marks a
|
||||||
|
* zero status handled, so a successful context can never reach
|
||||||
|
* FINISH_NORETURN's call to the handler in the first place.
|
||||||
|
*
|
||||||
|
* Nothing is logged here. Callers arrive from a position that has already
|
||||||
|
* reported -- FINISH_NORETURN logs the stack trace, carrying the status at full
|
||||||
|
* width, before it calls the handler -- and a second line naming a number the
|
||||||
|
* trace already gave would only invite the reader to trust the exit code.
|
||||||
|
*/
|
||||||
|
void akerr_exit(int status)
|
||||||
|
{
|
||||||
|
if ( status < 0 || status > AKERR_EXIT_STATUS_MAX ) {
|
||||||
|
exit(AKERR_EXIT_STATUS_UNREPRESENTABLE);
|
||||||
|
}
|
||||||
|
exit(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The last stop for an unhandled error. A handler invoked with no error at all
|
||||||
|
* has no status to report and nothing akerr_exit() could map, so it exits 1
|
||||||
|
* directly: a plain generic failure.
|
||||||
|
*/
|
||||||
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
void akerr_default_handler_unhandled_error(akerr_ErrorContext *errctx)
|
||||||
{
|
{
|
||||||
if ( errctx == NULL ) {
|
if ( errctx == NULL ) {
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
exit(errctx->status);
|
akerr_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 *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++ ) {
|
for (int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||||
if ( AKERR_ARRAY_ERROR[i].refcount == 0 ) {
|
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)
|
akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
||||||
{
|
{
|
||||||
int oldid = 0;
|
int oldid = 0;
|
||||||
|
akerr_ErrorContext *remaining = err;
|
||||||
|
|
||||||
|
akerr_init();
|
||||||
if ( err == NULL ) {
|
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");
|
FAIL_RETURN(errctx, AKERR_NULLPOINTER, "akerr_release_error got NULL context pointer");
|
||||||
}
|
}
|
||||||
|
akerr_mutex_lock(&akerr_state_lock);
|
||||||
if ( err->refcount > 0 ) {
|
if ( err->refcount > 0 ) {
|
||||||
err->refcount -= 1;
|
err->refcount -= 1;
|
||||||
}
|
}
|
||||||
@@ -262,9 +401,10 @@ akerr_ErrorContext *akerr_release_error(akerr_ErrorContext *err)
|
|||||||
memset(err, 0x00, sizeof(akerr_ErrorContext));
|
memset(err, 0x00, sizeof(akerr_ErrorContext));
|
||||||
err->stacktracebufptr = (char *)&err->stacktracebuf;
|
err->stacktracebufptr = (char *)&err->stacktracebuf;
|
||||||
err->arrayid = oldid;
|
err->arrayid = oldid;
|
||||||
return NULL;
|
remaining = NULL;
|
||||||
}
|
}
|
||||||
return err;
|
akerr_mutex_unlock(&akerr_state_lock);
|
||||||
|
return remaining;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -288,7 +428,7 @@ static unsigned akerr_status_hash(int status)
|
|||||||
/*
|
/*
|
||||||
* Find the slot holding `status`. With create != 0, claim a free slot for it if
|
* 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
|
* 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
|
* The `& (AKERR_STATUS_NAME_SLOTS - 1)` below is load-bearing and fails
|
||||||
* silently: off by one in either direction and the probe indexes past
|
* silently: off by one in either direction and the probe indexes past
|
||||||
@@ -328,7 +468,8 @@ static akerr_StatusName *akerr_status_slot(int status, int create)
|
|||||||
return NULL;
|
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)
|
static akerr_StatusRange *akerr_range_for_status(int status)
|
||||||
{
|
{
|
||||||
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
for ( int i = 0; i < akerr_status_range_count; i++ ) {
|
||||||
@@ -348,8 +489,12 @@ static akerr_StatusRange *akerr_range_for_status(int status)
|
|||||||
* fails to register degrades into "Unknown Error" in stack traces, which is
|
* 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
|
* 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.
|
* 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,
|
static akerr_ErrorContext AKERR_NOIGNORE *akerr_store_status_name_locked(const char *owner,
|
||||||
int status,
|
int status,
|
||||||
const char *name)
|
const char *name)
|
||||||
{
|
{
|
||||||
@@ -394,12 +539,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
|
* 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
|
* are checked here rather than only inside akerr_store_status_name_locked(): the
|
||||||
* accepts a NULL owner for the legacy akerr_name_for_status() path, so a NULL
|
* store accepts a NULL owner for the legacy akerr_name_for_status() path, so a
|
||||||
* arriving through *this* entry point would be read as "caller did not identify
|
* NULL arriving through *this* entry point would be read as "caller did not
|
||||||
* itself" and skip the ownership check entirely.
|
* 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);
|
PREPARE_ERROR(errctx);
|
||||||
|
|
||||||
@@ -410,10 +559,21 @@ akerr_ErrorContext *akerr_register_status_name(const char *owner, int status, co
|
|||||||
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
FAIL_NONZERO_RETURN(errctx, (name == NULL), AKERR_STATUS_NAME_INVALID,
|
||||||
"Refusing to name status %d for %s: the name is NULL",
|
"Refusing to name status %d for %s: the name is NULL",
|
||||||
status, owner);
|
status, owner);
|
||||||
PASS(errctx, akerr_store_status_name(owner, status, name));
|
PASS(errctx, akerr_store_status_name_locked(owner, status, name));
|
||||||
SUCCEED_RETURN(errctx);
|
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.
|
* Return or set a name. Status magnitude is unrelated to storage size.
|
||||||
*
|
*
|
||||||
@@ -426,16 +586,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
|
* 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
|
* calls it to render a status into a stack trace, so it must not itself need an
|
||||||
* error context.
|
* 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)
|
char *akerr_name_for_status(int status, char *name)
|
||||||
{
|
{
|
||||||
akerr_StatusName *entry;
|
akerr_StatusName *entry;
|
||||||
|
char *found = "Unknown Error";
|
||||||
|
|
||||||
akerr_init();
|
akerr_init();
|
||||||
if ( name != NULL ) {
|
if ( name != NULL ) {
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
int refused = 0;
|
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 {
|
ATTEMPT {
|
||||||
CATCH(errctx, akerr_store_status_name(NULL, status, name));
|
CATCH(errctx, akerr_store_status_name(NULL, status, name));
|
||||||
} CLEANUP {
|
} CLEANUP {
|
||||||
@@ -449,15 +633,22 @@ char *akerr_name_for_status(int status, char *name)
|
|||||||
return "Unknown Error";
|
return "Unknown Error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
akerr_mutex_lock(&akerr_state_lock);
|
||||||
entry = akerr_status_slot(status, 0);
|
entry = akerr_status_slot(status, 0);
|
||||||
if ( entry == NULL ) {
|
if ( entry != NULL ) {
|
||||||
return "Unknown Error";
|
found = entry->name;
|
||||||
}
|
}
|
||||||
return entry->name;
|
akerr_mutex_unlock(&akerr_state_lock);
|
||||||
|
return found;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reserve an inclusive status interval and reject collisions. */
|
/* Reserve an inclusive status interval and reject collisions. Caller holds
|
||||||
akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, const char *owner)
|
* 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;
|
int last_status;
|
||||||
PREPARE_ERROR(errctx);
|
PREPARE_ERROR(errctx);
|
||||||
@@ -510,3 +701,14 @@ akerr_ErrorContext *akerr_reserve_status_range(int first_status, int count, cons
|
|||||||
akerr_status_range_count++;
|
akerr_status_range_count++;
|
||||||
SUCCEED_RETURN(errctx);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
121
src/lock.h
Normal file
121
src/lock.h
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#ifndef _AKERR_LOCK_H_
|
||||||
|
#define _AKERR_LOCK_H_
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Serialization for the library's process-global state: the error pool
|
||||||
|
* (AKERR_ARRAY_ERROR) and the status registry. Private to the library -- none
|
||||||
|
* of this appears in the installed header, so the backend is not part of the
|
||||||
|
* ABI and can be changed without touching a consumer.
|
||||||
|
*
|
||||||
|
* The backend is chosen at configure time by the AKERR_THREADS build option and
|
||||||
|
* never by autodetection here. A build that quietly decided it did not need
|
||||||
|
* locking is exactly the failure this has to prevent: it would produce a
|
||||||
|
* library that reports itself thread safe and is not.
|
||||||
|
*
|
||||||
|
* AKERR_THREADS_PTHREAD POSIX threads.
|
||||||
|
* AKERR_THREADS_NONE No locking at all, for a build that has declared
|
||||||
|
* itself single threaded (-DAKERR_THREADS=none).
|
||||||
|
*
|
||||||
|
* One lock covers both tables, and it is recursive. Both are deliberate:
|
||||||
|
*
|
||||||
|
* - Raising an error re-enters the library. FAIL() calls
|
||||||
|
* akerr_name_for_status() to render the status into the stack trace and
|
||||||
|
* ENSURE_ERROR_READY() to check a context out of the pool, so a refusal
|
||||||
|
* raised from inside a locked registry operation takes the lock again on
|
||||||
|
* the same thread. A non-recursive mutex deadlocks there.
|
||||||
|
* - With a single lock there is no lock ordering to get wrong, and no way for
|
||||||
|
* a future caller to acquire the pool and the registry in the opposite
|
||||||
|
* order from this file.
|
||||||
|
*
|
||||||
|
* The cost is that error *construction* is serialized across threads. Errors
|
||||||
|
* are the exceptional path; correctness is worth more there than throughput.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* PTHREAD_MUTEX_RECURSIVE is XSI, so glibc hides it under a strict -std=c99
|
||||||
|
* without _XOPEN_SOURCE. No feature-test macro is defined here, because the
|
||||||
|
* public header already needs the same one for PATH_MAX: a build strict enough
|
||||||
|
* to lose one has already lost the other. Build with -D_XOPEN_SOURCE=700 if you
|
||||||
|
* need strict C99.
|
||||||
|
*/
|
||||||
|
#if defined(AKERR_THREADS_PTHREAD) && AKERR_THREADS_PTHREAD == 1
|
||||||
|
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
typedef pthread_mutex_t akerr_Mutex;
|
||||||
|
typedef pthread_once_t akerr_Once;
|
||||||
|
#define AKERR_ONCE_INIT PTHREAD_ONCE_INIT
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Terminal on failure. There is no error context to raise into: the pool one
|
||||||
|
* would come from is the thing this lock protects, and every path that could
|
||||||
|
* report the failure needs the lock to do it. A process whose error library
|
||||||
|
* silently stopped locking is worse than one that stops here.
|
||||||
|
*/
|
||||||
|
static void akerr_mutex_init(akerr_Mutex *mutex)
|
||||||
|
{
|
||||||
|
pthread_mutexattr_t attr;
|
||||||
|
|
||||||
|
if ( pthread_mutexattr_init(&attr) != 0 ||
|
||||||
|
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) != 0 ||
|
||||||
|
pthread_mutex_init(mutex, &attr) != 0 ) {
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
pthread_mutexattr_destroy(&attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void akerr_mutex_lock(akerr_Mutex *mutex)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void akerr_mutex_unlock(akerr_Mutex *mutex)
|
||||||
|
{
|
||||||
|
pthread_mutex_unlock(mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void akerr_once(akerr_Once *once, void (*routine)(void))
|
||||||
|
{
|
||||||
|
pthread_once(once, routine);
|
||||||
|
}
|
||||||
|
|
||||||
|
#elif defined(AKERR_THREADS_NONE) && AKERR_THREADS_NONE == 1
|
||||||
|
|
||||||
|
typedef char akerr_Mutex;
|
||||||
|
typedef int akerr_Once;
|
||||||
|
#define AKERR_ONCE_INIT 0
|
||||||
|
|
||||||
|
static void akerr_mutex_init(akerr_Mutex *mutex)
|
||||||
|
{
|
||||||
|
(void)mutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void akerr_mutex_lock(akerr_Mutex *mutex)
|
||||||
|
{
|
||||||
|
(void)mutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void akerr_mutex_unlock(akerr_Mutex *mutex)
|
||||||
|
{
|
||||||
|
(void)mutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The flag is raised before the routine runs, so a routine that calls back into
|
||||||
|
* akerr_init() sees initialization already in progress and does not recurse --
|
||||||
|
* the same short-circuit the pthread backend gets from akerr_initializing.
|
||||||
|
*/
|
||||||
|
static void akerr_once(akerr_Once *once, void (*routine)(void))
|
||||||
|
{
|
||||||
|
if ( *once == 0 ) {
|
||||||
|
*once = 1;
|
||||||
|
routine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
#error "No threading backend selected. Build libakerror through its CMake, which defines AKERR_THREADS_PTHREAD or AKERR_THREADS_NONE from the AKERR_THREADS option."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // _AKERR_LOCK_H_
|
||||||
1
test.sh
1
test.sh
@@ -1,5 +1,6 @@
|
|||||||
cmake -S . -B build
|
cmake -S . -B build
|
||||||
cmake --build build
|
cmake --build build
|
||||||
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
ctest --test-dir build --output-on-failure --output-junit "$(pwd)/ctest-junit.xml"
|
||||||
|
scripts/thread_test.sh build/tsan --output-junit "$(pwd)/tsan-junit.xml"
|
||||||
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65
|
python3 scripts/mutation_test.py --target src/error.c --junit mutation-junit.xml --threshold 65
|
||||||
python3 scripts/coverage.py --junit coverage-junit.xml --threshold 90 --branch-threshold 50
|
python3 scripts/coverage.py --junit coverage-junit.xml --threshold 90 --branch-threshold 50
|
||||||
|
|||||||
@@ -94,18 +94,45 @@ Re-run after adding tests and confirm the score went up.
|
|||||||
|
|
||||||
## Current status
|
## Current status
|
||||||
|
|
||||||
`src/error.c` scores ~77% (the CI gate is set to 65% for headroom). The
|
`src/error.c` scores 81.4% — 245 of 301 mutants killed (211 by a failing test,
|
||||||
remaining survivors are dominated by:
|
24 by failing to compile, 10 by hanging the suite), 56 surviving. The CI gate is
|
||||||
|
set to 65% for headroom.
|
||||||
|
|
||||||
|
The ten timeout kills are all in the locking: deleting `akerr_mutex_init()` or
|
||||||
|
the `akerr_initializing` re-entry guard deadlocks the very first test, which is
|
||||||
|
the correct behaviour for a broken lock and is why the harness counts a hang as
|
||||||
|
a kill.
|
||||||
|
|
||||||
|
The remaining survivors are dominated by:
|
||||||
|
|
||||||
* **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of
|
* **Equivalent mutants** in `akerr_init`: deleting the `memset`/`NULL` setup of
|
||||||
file-scope statics (`AKERR_ARRAY_ERROR`, `__akerr_last_ditch`,
|
file-scope statics (`AKERR_ARRAY_ERROR`, `__akerr_last_ditch`,
|
||||||
`__akerr_last_ignored`) changes nothing, because C already zero-initializes
|
`__akerr_last_ignored`) changes nothing, because C already zero-initializes
|
||||||
objects with static storage duration. `int oldid = 0;` → `1` is likewise
|
objects with static storage duration. `int oldid = 0;` → `1` is likewise
|
||||||
dead: it is overwritten before use.
|
dead: it is overwritten before use, and so is clearing `akerr_initializing`
|
||||||
* **Default logger / handler internals** (`vfprintf`, `va_end`, the
|
at the end of initialization — nothing reads that flag once the once-routine
|
||||||
`errctx == NULL` branch, `exit(1)`): killing these needs a subprocess-based
|
has returned.
|
||||||
test that captures a child's stderr and exit code, rather than the in-process
|
* **Lock acquisition** (`akerr_mutex_lock`/`unlock` deletions, and the
|
||||||
capturing logger the other tests use.
|
`akerr_init()` call at the head of an entry point). These are the one category
|
||||||
|
where a survivor does *not* mean the mutant is harmless. Removing a lock
|
||||||
|
leaves a real race, and the assertions in `tests/err_threads_pool.c` only fire
|
||||||
|
when the race actually loses: rebuilding the surviving mutant and running that
|
||||||
|
test ten times caught it **four** times. The same mutant under
|
||||||
|
`scripts/thread_test.sh` failed **five of five**, with no false positive on
|
||||||
|
the unmutated library — but the mutation harness builds without sanitizers, so
|
||||||
|
it never sees that. Deleting an `akerr_init()` call survives for a duller
|
||||||
|
reason: something else has always initialized the library by the time that
|
||||||
|
line runs.
|
||||||
|
* **The default logger** (`vfprintf`, `va_end`, and the `return` in the
|
||||||
|
no-stdlib branch): the other tests replace `akerr_log_method` with the
|
||||||
|
in-process capturing logger, so nothing observes what the default one writes
|
||||||
|
to a real stderr. Killing these needs a test that captures a child's stderr.
|
||||||
|
The *handler* internals next to them are no longer in this category:
|
||||||
|
`tests/err_unhandled_null.c` and `tests/err_exit_status.c` read a forked
|
||||||
|
child's exit code, which kills every mutant in `akerr_exit()` and in
|
||||||
|
`akerr_default_handler_unhandled_error()` — all twelve of them, including the
|
||||||
|
`status < 0` → `status < 1` variant that only a test asserting
|
||||||
|
`akerr_exit(0)` exits 0 can distinguish.
|
||||||
* **Static assertions** (`akerr_assert_name_slots_pow2` and the occupancy cap
|
* **Static assertions** (`akerr_assert_name_slots_pow2` and the occupancy cap
|
||||||
it guards): a mutated compile-time assertion that still compiles has no
|
it guards): a mutated compile-time assertion that still compiles has no
|
||||||
runtime behavior to observe. Unkillable by construction — the assertion is
|
runtime behavior to observe. Unkillable by construction — the assertion is
|
||||||
@@ -119,6 +146,13 @@ remaining survivors are dominated by:
|
|||||||
|
|
||||||
Findings surfaced by mutation testing:
|
Findings surfaced by mutation testing:
|
||||||
|
|
||||||
|
* **Open:** the harness builds every mutant with the default CMake options, so a
|
||||||
|
mutant that only breaks under concurrency is judged by a suite running without
|
||||||
|
ThreadSanitizer. Mutating under `-DAKERR_SANITIZE=thread` would close that,
|
||||||
|
and needs a way to pass CMake options through to the mutant build. See
|
||||||
|
"Mutation testing judges concurrency mutants without a sanitizer" in
|
||||||
|
`TODO.md`.
|
||||||
|
|
||||||
* **Superseded:** status names now use a private sparse registry, so the old
|
* **Superseded:** status names now use a private sparse registry, so the old
|
||||||
public `AKERR_MAX_ERR_VALUE` ceiling and its consumer ABI mismatch no longer
|
public `AKERR_MAX_ERR_VALUE` ceiling and its consumer ABI mismatch no longer
|
||||||
exist. `tests/err_maxval.c` covers arbitrary `int` values and registry
|
exist. `tests/err_maxval.c` covers arbitrary `int` values and registry
|
||||||
|
|||||||
206
tests/err_exit_status.c
Normal file
206
tests/err_exit_status.c
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#include "akerror.h"
|
||||||
|
#include "err_capture.h"
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* An unhandled error must never leave the process looking like a success.
|
||||||
|
*
|
||||||
|
* The default handler used to exit(errctx->status) unconditionally, and an exit
|
||||||
|
* status is one byte wide: status 256 -- AKERR_FIRST_CONSUMER_STATUS, the very
|
||||||
|
* first code any consumer can reserve -- exited 0 and told the shell the
|
||||||
|
* program succeeded. Status 300 exited 44, which is some unrelated error's code.
|
||||||
|
*
|
||||||
|
* akerr_exit() now owns that mapping, and the default handler is one of its
|
||||||
|
* callers. The same table therefore drives both: a status must produce the same
|
||||||
|
* exit code whether a consumer calls akerr_exit() from their own handler or
|
||||||
|
* lets the library's handler run.
|
||||||
|
*
|
||||||
|
* akerr_exit(0) exits 0 -- zero is the library's success status. The thing that
|
||||||
|
* keeps an unhandled error off that path is PROCESS's `case 0`, which marks a
|
||||||
|
* zero status handled before FINISH_NORETURN can reach the handler, and the
|
||||||
|
* last three cases assert that, plus that the status an exit code could not
|
||||||
|
* carry is still recoverable from the stack trace.
|
||||||
|
*
|
||||||
|
* Neither exit path returns, so those cases run in forked children.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define TEST_OWNER "err_exit_status"
|
||||||
|
#define TEST_STATUS AKERR_FIRST_CONSUMER_STATUS
|
||||||
|
#define TEST_STATUS_NAME "Consumer Status Two Fifty Six"
|
||||||
|
|
||||||
|
static const struct {
|
||||||
|
int status;
|
||||||
|
int expect;
|
||||||
|
} exit_cases[] = {
|
||||||
|
/* status expected exit */
|
||||||
|
{ 0, 0 }, /* Zero is the library's success status, and exit code 0 is what that is called out here */
|
||||||
|
{ 1, 1 }, /* Lowest status an exit code can carry */
|
||||||
|
{ AKERR_VALUE, AKERR_VALUE }, /* An ordinary library status, delivered intact */
|
||||||
|
{ AKERR_EXIT_STATUS_MAX, AKERR_EXIT_STATUS_MAX }, /* Highest status an exit code can carry */
|
||||||
|
{ AKERR_FIRST_CONSUMER_STATUS, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* 256: low byte 0, the case that used to exit success */
|
||||||
|
{ 300, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Low byte 44 would alias an unrelated status */
|
||||||
|
{ 65536, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Low byte 0 again, further out */
|
||||||
|
{ -1, AKERR_EXIT_STATUS_UNREPRESENTABLE }, /* Negative: low byte 255 */
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run body in a child and return its exit status, or -1 if it did not exit
|
||||||
|
* normally. The 99 sentinel catches a body that returns instead of terminating.
|
||||||
|
*/
|
||||||
|
static int child_exit_status(void (*body)(void))
|
||||||
|
{
|
||||||
|
pid_t pid = fork();
|
||||||
|
if ( pid == 0 ) {
|
||||||
|
body();
|
||||||
|
_exit(99);
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
if ( pid < 0 || waitpid(pid, &status, 0) != pid || !WIFEXITED(status) ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return WEXITSTATUS(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The status the next child leaves with. Set before each fork. */
|
||||||
|
static int pending_status;
|
||||||
|
|
||||||
|
/* The way a consumer's own handler is expected to leave. */
|
||||||
|
static void run_akerr_exit(void)
|
||||||
|
{
|
||||||
|
akerr_exit(pending_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The way the library leaves when nothing handled the error. */
|
||||||
|
static void run_default_handler(void)
|
||||||
|
{
|
||||||
|
akerr_ErrorContext *slot = akerr_next_error();
|
||||||
|
if ( slot == NULL ) {
|
||||||
|
_exit(98);
|
||||||
|
}
|
||||||
|
slot->status = pending_status;
|
||||||
|
akerr_default_handler_unhandled_error(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *raise_consumer_error(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
FAIL_RETURN(errctx, TEST_STATUS, "consumer status, deliberately unhandled");
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext AKERR_NOIGNORE *raise_nothing(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
SUCCEED_RETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A full propagation to the top of the stack, with nothing handling it. */
|
||||||
|
static void unhandled_consumer_error(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, raise_consumer_error());
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
/* no HANDLE for TEST_STATUS -> stays unhandled */
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reached through FINISH_NORETURN rather than by calling the handler directly,
|
||||||
|
* so the child exercises the path a consumer actually takes. The handler is set
|
||||||
|
* explicitly because an earlier case in this process may have replaced it.
|
||||||
|
*/
|
||||||
|
static void unhandled_consumer_error_fatal(void)
|
||||||
|
{
|
||||||
|
akerr_handler_unhandled_error = &akerr_default_handler_unhandled_error;
|
||||||
|
unhandled_consumer_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int trace_fired = -2;
|
||||||
|
|
||||||
|
static void nonfatal_handler(akerr_ErrorContext *e)
|
||||||
|
{
|
||||||
|
trace_fired = (e != NULL) ? e->status : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The same shape as unhandled_consumer_error(), except nothing fails. PROCESS
|
||||||
|
* opens with `case 0`, so a zero status is handled and the handler must not
|
||||||
|
* run -- which is what keeps akerr_exit(0) exiting 0 from being a hole in the
|
||||||
|
* "an unhandled error never exits 0" rule.
|
||||||
|
*/
|
||||||
|
static void successful_operation(void)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(errctx);
|
||||||
|
ATTEMPT {
|
||||||
|
CATCH(errctx, raise_nothing());
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(errctx) {
|
||||||
|
} FINISH_NORETURN(errctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
akerr_capture_install();
|
||||||
|
akerr_init();
|
||||||
|
|
||||||
|
/* The three outcomes have to stay distinguishable from each other. */
|
||||||
|
AKERR_CHECK(AKERR_EXIT_STATUS_UNREPRESENTABLE != 0);
|
||||||
|
AKERR_CHECK(AKERR_EXIT_STATUS_UNREPRESENTABLE != 1);
|
||||||
|
|
||||||
|
AKERR_CHECK_SUCCEEDS(akerr_reserve_status_range(TEST_STATUS, 1, TEST_OWNER));
|
||||||
|
AKERR_CHECK_SUCCEEDS(akerr_register_status_name(TEST_OWNER, TEST_STATUS,
|
||||||
|
TEST_STATUS_NAME));
|
||||||
|
|
||||||
|
for ( size_t i = 0; i < sizeof(exit_cases) / sizeof(exit_cases[0]); i++ ) {
|
||||||
|
pending_status = exit_cases[i].status;
|
||||||
|
|
||||||
|
int direct = child_exit_status(&run_akerr_exit);
|
||||||
|
if ( direct != exit_cases[i].expect ) {
|
||||||
|
fprintf(stderr, "akerr_exit(%d) exited %d, want %d\n",
|
||||||
|
exit_cases[i].status, direct, exit_cases[i].expect);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The handler must not carry a mapping of its own. */
|
||||||
|
int handled = child_exit_status(&run_default_handler);
|
||||||
|
if ( handled != direct ) {
|
||||||
|
fprintf(stderr, "default handler on status %d exited %d,"
|
||||||
|
" but akerr_exit(%d) exited %d\n",
|
||||||
|
exit_cases[i].status, handled, exit_cases[i].status, direct);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The NULL-context exit is asserted by tests/err_unhandled_null.c. */
|
||||||
|
|
||||||
|
/* End to end: an unhandled consumer error kills the process non-zero. */
|
||||||
|
AKERR_CHECK(child_exit_status(&unhandled_consumer_error_fatal)
|
||||||
|
== AKERR_EXIT_STATUS_UNREPRESENTABLE);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* And the status the exit code could not carry is in the trace. Run with a
|
||||||
|
* handler that returns so the assertions happen in this process, where the
|
||||||
|
* captured log lives.
|
||||||
|
*/
|
||||||
|
akerr_handler_unhandled_error = &nonfatal_handler;
|
||||||
|
akerr_capture_reset();
|
||||||
|
unhandled_consumer_error();
|
||||||
|
AKERR_CHECK(trace_fired == TEST_STATUS);
|
||||||
|
AKERR_CHECK_CONTAINS("Unhandled Error");
|
||||||
|
AKERR_CHECK_CONTAINS("256");
|
||||||
|
AKERR_CHECK_CONTAINS(TEST_STATUS_NAME);
|
||||||
|
|
||||||
|
/* A zero status is handled by PROCESS and never reaches the handler. */
|
||||||
|
trace_fired = -2;
|
||||||
|
akerr_capture_reset();
|
||||||
|
successful_operation();
|
||||||
|
AKERR_CHECK(trace_fired == -2);
|
||||||
|
AKERR_CHECK_NOT_CONTAINS("Unhandled Error");
|
||||||
|
|
||||||
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
|
|
||||||
|
fprintf(stderr, "err_exit_status ok\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -15,11 +15,12 @@ int main(void)
|
|||||||
|
|
||||||
akerr_ErrorContext *slots[AKERR_MAX_ARRAY_ERROR];
|
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++ ) {
|
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||||
slots[i] = akerr_next_error();
|
slots[i] = akerr_next_error();
|
||||||
AKERR_CHECK(slots[i] != NULL);
|
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. */
|
/* 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
|
* - refcount == 0: releasing a context nobody holds must not underflow the
|
||||||
* count; it wipes and returns NULL like any other fully-released slot.
|
* 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
|
* The macro API never produces a refcount above 1 (akerr_next_error() takes the
|
||||||
* increments when it acquires a fresh slot), so this test sets the count
|
* one reference a fresh slot gets), so this test sets the count directly to
|
||||||
* directly to model a caller that took an extra reference.
|
* model a caller that took an extra reference, and clears it to model a slot
|
||||||
|
* nobody holds.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
int main(void)
|
int main(void)
|
||||||
@@ -56,7 +57,8 @@ int main(void)
|
|||||||
*/
|
*/
|
||||||
akerr_ErrorContext *unheld = akerr_next_error();
|
akerr_ErrorContext *unheld = akerr_next_error();
|
||||||
AKERR_CHECK(unheld != NULL);
|
AKERR_CHECK(unheld != NULL);
|
||||||
AKERR_CHECK(unheld->refcount == 0);
|
AKERR_CHECK(unheld->refcount == 1);
|
||||||
|
unheld->refcount = 0;
|
||||||
ret = akerr_release_error(unheld);
|
ret = akerr_release_error(unheld);
|
||||||
AKERR_CHECK(ret == NULL);
|
AKERR_CHECK(ret == NULL);
|
||||||
AKERR_CHECK(unheld->refcount == 0);
|
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
|
||||||
254
tests/err_threads_handoff.c
Normal file
254
tests/err_threads_handoff.c
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
#include "akerror.h"
|
||||||
|
#include "err_capture.h"
|
||||||
|
#include "err_threads.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Handing an error context from one thread to another.
|
||||||
|
*
|
||||||
|
* A context is not thread state. It lives in AKERR_ARRAY_ERROR, which is
|
||||||
|
* process-global, and the only field of it the library reads across an
|
||||||
|
* ownership boundary is the reference count -- which is only ever touched under
|
||||||
|
* the pool lock. So a context can be raised on one thread, handed to another,
|
||||||
|
* and handled and released there, and akerr_release_error() does not care which
|
||||||
|
* thread checked the slot out. That is a property this library promises, and it
|
||||||
|
* is what makes the worker/collector shape usable at all.
|
||||||
|
*
|
||||||
|
* The other half of the promise is what it does *not* cover: two threads inside
|
||||||
|
* one context at once. Ownership moves, it does not fork. This test asserts the
|
||||||
|
* supported half; the unsupported half cannot be asserted without deliberately
|
||||||
|
* racing, which ThreadSanitizer would then correctly fail.
|
||||||
|
*
|
||||||
|
* The queue below is a plain mutex and two condition variables rather than the
|
||||||
|
* __atomic builtins the rest of these tests use, and that is deliberate: the
|
||||||
|
* mutex *is* the thing under test. Context content is written with no lock at
|
||||||
|
* all, so the handoff itself is what publishes those writes to the receiver.
|
||||||
|
*
|
||||||
|
* The claim is proved from four directions:
|
||||||
|
*
|
||||||
|
* 1. The context is still a live pool slot after it crosses, holding exactly
|
||||||
|
* the one reference it was checked out with.
|
||||||
|
* 2. Its message and its whole stack trace -- producer frame and all -- arrive
|
||||||
|
* intact, and in each producer's own order.
|
||||||
|
* 3. The slot is never recycled underneath the transfer: akerr_slot_owner[]
|
||||||
|
* still names the producer when the collector picks it up.
|
||||||
|
* 4. A context outlives the thread that raised it (see main()).
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define ITERATIONS 500
|
||||||
|
#define AKERR_HANDOFF_DEPTH 32
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The sizing rule docs/thread-safety.md gives, made executable. Every queued
|
||||||
|
* error is a checked-out pool slot, and so is every producer's error in flight.
|
||||||
|
* Outrun the pool and ENSURE_ERROR_READY exits the process from inside FAIL,
|
||||||
|
* with no slot left to raise the failure from.
|
||||||
|
*/
|
||||||
|
typedef char akerr_assert_handoff_fits_pool[
|
||||||
|
(AKERR_HANDOFF_DEPTH + AKERR_TEST_THREADS < AKERR_MAX_ARRAY_ERROR) ? 1 : -1];
|
||||||
|
|
||||||
|
static struct
|
||||||
|
{
|
||||||
|
pthread_mutex_t lock;
|
||||||
|
pthread_cond_t not_full;
|
||||||
|
pthread_cond_t not_empty;
|
||||||
|
akerr_ErrorContext *slot[AKERR_HANDOFF_DEPTH];
|
||||||
|
int head;
|
||||||
|
int count;
|
||||||
|
} queue;
|
||||||
|
|
||||||
|
/* Bounded on purpose: an unbounded queue of errors is an unbounded number of
|
||||||
|
* checked-out pool slots. Blocking the producer is the backpressure. */
|
||||||
|
static void queue_push(akerr_ErrorContext *errctx)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&queue.lock);
|
||||||
|
while ( queue.count == AKERR_HANDOFF_DEPTH ) {
|
||||||
|
pthread_cond_wait(&queue.not_full, &queue.lock);
|
||||||
|
}
|
||||||
|
queue.slot[(queue.head + queue.count) % AKERR_HANDOFF_DEPTH] = errctx;
|
||||||
|
queue.count += 1;
|
||||||
|
pthread_cond_signal(&queue.not_empty);
|
||||||
|
pthread_mutex_unlock(&queue.lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
static akerr_ErrorContext *queue_pop(void)
|
||||||
|
{
|
||||||
|
akerr_ErrorContext *errctx;
|
||||||
|
|
||||||
|
pthread_mutex_lock(&queue.lock);
|
||||||
|
while ( queue.count == 0 ) {
|
||||||
|
pthread_cond_wait(&queue.not_empty, &queue.lock);
|
||||||
|
}
|
||||||
|
errctx = queue.slot[queue.head];
|
||||||
|
queue.head = (queue.head + 1) % AKERR_HANDOFF_DEPTH;
|
||||||
|
queue.count -= 1;
|
||||||
|
pthread_cond_signal(&queue.not_full);
|
||||||
|
pthread_mutex_unlock(&queue.lock);
|
||||||
|
return errctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Raise an error and give it away. The stack-trace frame is appended before the
|
||||||
|
* push so the trace records the crossing, and it is the last thing this thread
|
||||||
|
* does to the context: after queue_push() returns, `e` belongs to the collector
|
||||||
|
* and reading even e->status here would be the unsupported half of the rule.
|
||||||
|
*/
|
||||||
|
static void produce_one(akerr_ThreadArg *arg, int seq)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
|
||||||
|
FAIL(e, AKERR_VALUE, "thread %d seq %d", arg->id, seq);
|
||||||
|
AKERR_TCHECK(arg, akerr_slot_claim(e->arrayid, arg->id) == 0);
|
||||||
|
AKERR_STACKTRACE_APPEND(e, "queued by thread %d\n", arg->id);
|
||||||
|
queue_push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* One received error, handled and released on a thread that never called
|
||||||
|
* akerr_next_error(). That release is the whole claim.
|
||||||
|
*
|
||||||
|
* `seen` is the collector's own per-producer sequence counter. Collector-local
|
||||||
|
* means no atomics: keeping the producers in order is the queue's job, and
|
||||||
|
* checking it is this thread's.
|
||||||
|
*/
|
||||||
|
static void collect_one(akerr_ThreadArg *arg, akerr_ErrorContext *e, int *seen)
|
||||||
|
{
|
||||||
|
char expected[64];
|
||||||
|
int producer = 0;
|
||||||
|
int seq = 0;
|
||||||
|
|
||||||
|
AKERR_TCHECK(arg, akerr_valid_error_address(e) == 1);
|
||||||
|
/* It crossed holding exactly the reference it was checked out with. */
|
||||||
|
AKERR_TCHECK(arg, e->refcount == 1);
|
||||||
|
AKERR_TCHECK(arg, sscanf(e->message, "thread %d seq %d", &producer, &seq) == 2);
|
||||||
|
AKERR_TCHECK(arg, producer >= 2 && producer <= AKERR_TEST_THREADS);
|
||||||
|
if ( producer >= 2 && producer <= AKERR_TEST_THREADS ) {
|
||||||
|
AKERR_TCHECK(arg, seq == seen[producer]);
|
||||||
|
seen[producer] += 1;
|
||||||
|
}
|
||||||
|
/* The slot still belongs to the producer, so nothing recycled it while it
|
||||||
|
* was in flight. */
|
||||||
|
AKERR_TCHECK(arg, akerr_slot_holder(e->arrayid) == producer);
|
||||||
|
|
||||||
|
snprintf(expected, sizeof(expected), "thread %d seq %d", producer, seq);
|
||||||
|
/* Nothing to attempt -- the error is already in hand. The blocks are here
|
||||||
|
* because this is the assembly the macros require, and because a real
|
||||||
|
* collector reads exactly like this. */
|
||||||
|
ATTEMPT {
|
||||||
|
} CLEANUP {
|
||||||
|
} PROCESS(e) {
|
||||||
|
/* case 0: a handed-off error that arrives with no status means somebody
|
||||||
|
* wrote over the context after the producer let it go. */
|
||||||
|
int error_was_lost = 1;
|
||||||
|
AKERR_TCHECK(arg, error_was_lost == 0);
|
||||||
|
} HANDLE(e, AKERR_VALUE) {
|
||||||
|
/* HANDLE rewinds the cursor, but the bytes are still there: the whole
|
||||||
|
* trace crossed with the context, producer frame and handoff frame. */
|
||||||
|
AKERR_TCHECK(arg, strstr(e->stacktracebuf, expected) != NULL);
|
||||||
|
AKERR_TCHECK(arg, strstr(e->stacktracebuf, "queued by thread") != 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);
|
||||||
|
/* FINISH_NORETURN, not FINISH(e, false): FINISH_LOGIC decides whether to
|
||||||
|
* propagate at run time, so the compiler still parses its
|
||||||
|
* `return __err_context` and diagnoses it in a function returning void,
|
||||||
|
* whatever __pass_up says. An error this collector did not handle takes the
|
||||||
|
* process down from here, which is right -- but note it is now the
|
||||||
|
* collector's thread deciding the exit status. */
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Drain exactly what the producers will send. A fixed count rather than a
|
||||||
|
* sentinel: a miscounted handoff should fail the test, not hang it.
|
||||||
|
*/
|
||||||
|
static void collect_all(akerr_ThreadArg *arg)
|
||||||
|
{
|
||||||
|
int seen[AKERR_TEST_THREADS + 1] = { 0 };
|
||||||
|
int total = (AKERR_TEST_THREADS - 1) * ITERATIONS;
|
||||||
|
|
||||||
|
for ( int i = 0; i < total; i++ ) {
|
||||||
|
collect_one(arg, queue_pop(), seen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *handoff_body(void *raw)
|
||||||
|
{
|
||||||
|
akerr_ThreadArg *arg = raw;
|
||||||
|
|
||||||
|
pthread_barrier_wait(arg->barrier);
|
||||||
|
|
||||||
|
if ( arg->id == 1 ) {
|
||||||
|
collect_all(arg);
|
||||||
|
} else {
|
||||||
|
for ( int i = 0; i < ITERATIONS; i++ ) {
|
||||||
|
produce_one(arg, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Written by the raising thread, read by main() after pthread_join(). The join
|
||||||
|
* is the happens-before edge, which is the same thing the queue's mutex does
|
||||||
|
* above -- a plain global needs no atomics once something orders it.
|
||||||
|
*/
|
||||||
|
static akerr_ErrorContext *parked;
|
||||||
|
|
||||||
|
static void *raise_and_exit(void *unused)
|
||||||
|
{
|
||||||
|
PREPARE_ERROR(e);
|
||||||
|
|
||||||
|
(void)unused;
|
||||||
|
FAIL(e, AKERR_IO, "raised on a thread that exited");
|
||||||
|
parked = e;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
pthread_t raiser;
|
||||||
|
int failures = 0;
|
||||||
|
|
||||||
|
akerr_log_method = &akerr_thread_logger;
|
||||||
|
akerr_init();
|
||||||
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
|
AKERR_CHECK(pthread_mutex_init(&queue.lock, NULL) == 0);
|
||||||
|
AKERR_CHECK(pthread_cond_init(&queue.not_full, NULL) == 0);
|
||||||
|
AKERR_CHECK(pthread_cond_init(&queue.not_empty, NULL) == 0);
|
||||||
|
|
||||||
|
failures = akerr_run_threads(&handoff_body);
|
||||||
|
AKERR_CHECK(failures == 0);
|
||||||
|
/* Every handed-off context was released by the thread that received it. */
|
||||||
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A context outlives the thread that raised it: the pool is process-global,
|
||||||
|
* not thread-local storage. By the time these checks run, the thread that
|
||||||
|
* called FAIL() no longer exists.
|
||||||
|
*/
|
||||||
|
AKERR_CHECK(pthread_create(&raiser, NULL, &raise_and_exit, NULL) == 0);
|
||||||
|
AKERR_CHECK(pthread_join(raiser, NULL) == 0);
|
||||||
|
AKERR_CHECK(parked != NULL);
|
||||||
|
AKERR_CHECK(akerr_valid_error_address(parked) == 1);
|
||||||
|
AKERR_CHECK(parked->status == AKERR_IO);
|
||||||
|
AKERR_CHECK(parked->refcount == 1);
|
||||||
|
AKERR_CHECK(strstr(parked->stacktracebuf, "raised on a thread that exited") != NULL);
|
||||||
|
RELEASE_ERROR(parked);
|
||||||
|
AKERR_CHECK(parked == NULL);
|
||||||
|
|
||||||
|
AKERR_CHECK(akerr_slots_in_use() == 0);
|
||||||
|
for ( int i = 0; i < AKERR_MAX_ARRAY_ERROR; i++ ) {
|
||||||
|
AKERR_CHECK(akerr_slot_holder(i) == 0);
|
||||||
|
}
|
||||||
|
/* Nothing here reports through the log method: a handoff is not an error. */
|
||||||
|
AKERR_CHECK(akerr_thread_logs() == 0);
|
||||||
|
|
||||||
|
pthread_cond_destroy(&queue.not_empty);
|
||||||
|
pthread_cond_destroy(&queue.not_full);
|
||||||
|
pthread_mutex_destroy(&queue.lock);
|
||||||
|
|
||||||
|
fprintf(stderr, "err_threads_handoff ok (%d producers x %d errors)\n",
|
||||||
|
AKERR_TEST_THREADS - 1, ITERATIONS);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@
|
|||||||
/*
|
/*
|
||||||
* The default unhandled-error handler is the library's last stop: it exits the
|
* The default unhandled-error handler is the library's last stop: it exits the
|
||||||
* process. Both of its exits were untested -- exit(1) for a NULL context (a
|
* process. Both of its exits were untested -- exit(1) for a NULL context (a
|
||||||
* handler invoked with no error at all) and exit(errctx->status) for a real one.
|
* handler invoked with no error at all) and, for a real one, the status handed
|
||||||
|
* to akerr_exit(). tests/err_exit_status.c covers what akerr_exit() does with a
|
||||||
|
* status; this covers that the handler reaches it, and the NULL case, which
|
||||||
|
* never gets that far.
|
||||||
*
|
*
|
||||||
* The handler never returns, so each case runs in a forked child and the test
|
* The handler never returns, so each case runs in a forked child and the test
|
||||||
* asserts the exact exit status. That is stricter than a WILL_FAIL test, which
|
* asserts the exact exit status. That is stricter than a WILL_FAIL test, which
|
||||||
|
|||||||
Reference in New Issue
Block a user