From 21c966d8a9dd5942edca90ddb4b9b3ef15465287 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 17 Jul 2026 16:51:35 +0200 Subject: [PATCH 01/20] feat(internal): add thread pool Add a bounded thread pool that runs tasks in parallel and invokes completion callbacks in submission order. Add unit coverage for ordered parallel execution. --- src/sentry_sync.c | 275 +++++++++++++++++++++++++++++++++++++++++ src/sentry_sync.h | 52 ++++++++ tests/unit/test_sync.c | 102 +++++++++++++++ tests/unit/tests.inc | 2 + 4 files changed, 431 insertions(+) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index b63698396a..17d4ba01a6 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -1,6 +1,7 @@ #include "sentry_sync.h" #include "sentry_alloc.h" #include "sentry_core.h" +#include "sentry_cpu_relax.h" #include "sentry_string.h" #include "sentry_utils.h" #include @@ -107,6 +108,280 @@ sentry__thread_setname(sentry_threadid_t thread_id, const char *thread_name) return thread_setname(thread_id, thread_name); } +typedef struct sentry_threadpool_task_s { + struct sentry_threadpool_task_s *next; + void (*exec_func)(void *task_data); + void (*complete_func)(void *task_data); + void (*cleanup_func)(void *task_data); + void *task_data; + bool done; +} sentry_threadpool_task_t; + +struct sentry_threadpool_s { + sentry_threadid_t *threads; + char *thread_name; + size_t thread_count; + size_t started_threads; + sentry_mutex_t lock; + sentry_cond_t work_signal; + sentry_cond_t state_signal; + sentry_threadpool_task_t *first_task; + sentry_threadpool_task_t *last_task; + sentry_threadpool_task_t *next_task; + long pending; + long index; + bool running; + bool stopping; + bool committing; +}; + +static void +threadpool_task_free(sentry_threadpool_task_t *task) +{ + if (task->cleanup_func) { + task->cleanup_func(task->task_data); + } + sentry_free(task); +} + +static void +threadpool_wake_all(sentry_threadpool_t *pool) +{ + for (size_t i = 0; i < pool->started_threads; i++) { + sentry__cond_wake(&pool->work_signal); + } +} + +static void +threadpool_commit_ready(sentry_threadpool_t *pool) +{ + if (pool->committing) { + return; + } + pool->committing = true; + + while (pool->first_task && pool->first_task->done) { + sentry_threadpool_task_t *task = pool->first_task; + pool->first_task = task->next; + if (!pool->first_task) { + pool->last_task = NULL; + } + + sentry__mutex_unlock(&pool->lock); + if (task->complete_func) { + task->complete_func(task->task_data); + } + threadpool_task_free(task); + sentry__mutex_lock(&pool->lock); + + sentry__atomic_fetch_and_add(&pool->pending, -1); + sentry__cond_wake(&pool->state_signal); + } + + pool->committing = false; + if (sentry__atomic_fetch(&pool->pending) == 0) { + threadpool_wake_all(pool); + } +} + +SENTRY_THREAD_FN +threadpool_worker(void *data) +{ + sentry_threadpool_t *pool = data; + if (pool->thread_name) { + const long index = sentry__atomic_fetch_and_add(&pool->index, 1); + char thread_name[16]; + snprintf(thread_name, sizeof(thread_name), "%s-%ld", pool->thread_name, + index); + sentry__thread_setname(sentry__current_thread(), thread_name); + } + + while (true) { + sentry__mutex_lock(&pool->lock); + sentry_threadpool_task_t *task = pool->next_task; + while (!task) { + if (pool->stopping && sentry__atomic_fetch(&pool->pending) == 0) { + sentry__mutex_unlock(&pool->lock); + return 0; + } + sentry__cond_wait(&pool->work_signal, &pool->lock); + task = pool->next_task; + } + pool->next_task = task->next; + sentry__mutex_unlock(&pool->lock); + + task->exec_func(task->task_data); + + sentry__mutex_lock(&pool->lock); + task->done = true; + threadpool_commit_ready(pool); + sentry__cond_wake(&pool->work_signal); + sentry__mutex_unlock(&pool->lock); + } +} + +sentry_threadpool_t * +sentry__threadpool_new(size_t thread_count) +{ + if (thread_count == 0) { + return NULL; + } + sentry_threadpool_t *pool = SENTRY_MAKE(sentry_threadpool_t); + if (!pool) { + return NULL; + } + pool->threads = sentry__calloc(thread_count, sizeof(sentry_threadid_t)); + if (!pool->threads) { + sentry_free(pool); + return NULL; + } + pool->thread_count = thread_count; + sentry__mutex_init(&pool->lock); + sentry__cond_init(&pool->work_signal); + sentry__cond_init(&pool->state_signal); + for (size_t i = 0; i < thread_count; i++) { + sentry__thread_init(&pool->threads[i]); + } + return pool; +} + +void +sentry__threadpool_setname(sentry_threadpool_t *pool, const char *thread_name) +{ + if (!pool) { + return; + } + sentry_free(pool->thread_name); + pool->thread_name = sentry__string_clone(thread_name); +} + +int +sentry__threadpool_start(sentry_threadpool_t *pool) +{ + if (!pool || pool->running) { + return pool ? 0 : 1; + } + pool->running = true; + pool->stopping = false; + for (size_t i = 0; i < pool->thread_count; i++) { + if (sentry__thread_spawn(&pool->threads[i], threadpool_worker, pool) + != 0) { + sentry__mutex_lock(&pool->lock); + pool->stopping = true; + threadpool_wake_all(pool); + sentry__mutex_unlock(&pool->lock); + for (size_t j = 0; j < pool->started_threads; j++) { + sentry__thread_join(pool->threads[j]); + } + pool->started_threads = 0; + pool->running = false; + return 1; + } + pool->started_threads++; + } + return 0; +} + +int +sentry__threadpool_submit(sentry_threadpool_t *pool, + void (*exec_func)(void *task_data), void (*complete_func)(void *task_data), + void (*cleanup_func)(void *task_data), void *task_data) +{ + if (!pool || !exec_func) { + if (cleanup_func) { + cleanup_func(task_data); + } + return 1; + } + sentry_threadpool_task_t *task = SENTRY_MAKE(sentry_threadpool_task_t); + if (!task) { + if (cleanup_func) { + cleanup_func(task_data); + } + return 1; + } + task->exec_func = exec_func; + task->complete_func = complete_func; + task->cleanup_func = cleanup_func; + task->task_data = task_data; + + sentry__mutex_lock(&pool->lock); + if (!pool->running || pool->stopping) { + sentry__mutex_unlock(&pool->lock); + threadpool_task_free(task); + return 1; + } + + if (pool->last_task) { + pool->last_task->next = task; + } else { + pool->first_task = task; + } + pool->last_task = task; + if (!pool->next_task) { + pool->next_task = task; + } + sentry__atomic_fetch_and_add(&pool->pending, 1); + sentry__cond_wake(&pool->work_signal); + sentry__mutex_unlock(&pool->lock); + return 0; +} + +void +sentry__threadpool_flush(sentry_threadpool_t *pool) +{ + if (!pool || !pool->running) { + return; + } + sentry__mutex_lock(&pool->lock); + while (sentry__atomic_fetch(&pool->pending) > 0) { + sentry__cond_wait(&pool->state_signal, &pool->lock); + } + sentry__mutex_unlock(&pool->lock); +} + +void +sentry__threadpool_shutdown(sentry_threadpool_t *pool) +{ + if (!pool || !pool->running) { + return; + } + sentry__mutex_lock(&pool->lock); + pool->stopping = true; + threadpool_wake_all(pool); + sentry__cond_wake(&pool->state_signal); + sentry__mutex_unlock(&pool->lock); + + for (size_t i = 0; i < pool->started_threads; i++) { + sentry__thread_join(pool->threads[i]); + } + pool->started_threads = 0; + pool->running = false; + pool->index = 0; +} + +void +sentry__threadpool_free(sentry_threadpool_t *pool) +{ + if (!pool) { + return; + } + sentry__threadpool_shutdown(pool); + sentry_threadpool_task_t *task = pool->first_task; + while (task) { + sentry_threadpool_task_t *next = task->next; + threadpool_task_free(task); + task = next; + } + for (size_t i = 0; i < pool->thread_count; i++) { + sentry__thread_free(&pool->threads[i]); + } + sentry_free(pool->thread_name); + sentry__mutex_free(&pool->lock); + sentry_free(pool->threads); + sentry_free(pool); +} + /** * Queue operations, locking and Reference counting: * diff --git a/src/sentry_sync.h b/src/sentry_sync.h index 1a9c48a6c6..cba287100c 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -3,6 +3,7 @@ #include "sentry_boot.h" #include "sentry_core.h" +#include "sentry_cpu_relax.h" #include #include @@ -452,6 +453,35 @@ sentry__atomic_compare_swap(volatile long *val, long expected, long desired) #endif } +typedef bool (*sentry_spin_wait_func_t)(int attempt, void *data); + +static inline void +sentry__spin_lock(volatile long *lock) +{ + while (!sentry__atomic_compare_swap(lock, 0, 1)) { + sentry__cpu_relax(); + } +} + +static inline bool +sentry__spin_lock_wait( + volatile long *lock, sentry_spin_wait_func_t wait_func, void *data) +{ + int attempts = 0; + while (!sentry__atomic_compare_swap(lock, 0, 1)) { + if (!wait_func || !wait_func(++attempts, data)) { + return false; + } + } + return true; +} + +static inline void +sentry__spin_unlock(volatile long *lock) +{ + sentry__atomic_store(lock, 0); +} + /** * 64-bit variants of the atomic helpers above. The `long`-based helpers are * only 32 bits wide on Windows and 32-bit POSIX targets, so callers that need @@ -492,8 +522,30 @@ int sentry__thread_setname( struct sentry_bgworker_s; typedef struct sentry_bgworker_s sentry_bgworker_t; +struct sentry_threadpool_s; +typedef struct sentry_threadpool_s sentry_threadpool_t; + typedef void (*sentry_task_exec_func_t)(void *task_data, void *state); +/** + * Creates a thread pool. Tasks execute in parallel, while completion callbacks + * run in submission order. + */ +sentry_threadpool_t *sentry__threadpool_new(size_t thread_count); +void sentry__threadpool_setname( + sentry_threadpool_t *pool, const char *thread_name); +int sentry__threadpool_start(sentry_threadpool_t *pool); +/** + * Takes ownership of `task_data`, freeing it using `cleanup_func` when the + * task is completed, cancelled, or rejected. + */ +int sentry__threadpool_submit(sentry_threadpool_t *pool, + void (*exec_func)(void *task_data), void (*complete_func)(void *task_data), + void (*cleanup_func)(void *task_data), void *task_data); +void sentry__threadpool_flush(sentry_threadpool_t *pool); +void sentry__threadpool_shutdown(sentry_threadpool_t *pool); +void sentry__threadpool_free(sentry_threadpool_t *pool); + /** * Creates a new background worker thread. * diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index be9e4b657e..3032169503 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -582,6 +582,108 @@ SENTRY_TEST(bgworker_delayed_shutdown) sentry__bgworker_decref(bgw); } +struct threadpool_test_state { + volatile long first_started; + volatile long release_first; + volatile long second_ran; + bool first_timed_out; + int completion_order[2]; + int completion_count; + int cleanup_count; +}; + +struct threadpool_test_task { + struct threadpool_test_state *state; + int id; +}; + +static void +threadpool_test_exec(void *data) +{ + struct threadpool_test_task *task = data; + struct threadpool_test_state *state = task->state; + const uint64_t deadline = sentry__monotonic_time() + 1000; + + if (task->id == 0) { + sentry__atomic_store(&state->first_started, 1); + while (!sentry__atomic_fetch(&state->release_first)) { + if (sentry__monotonic_time() >= deadline) { + state->first_timed_out = true; + break; + } + sleep_ms(1); + } + } else { + while (!sentry__atomic_fetch(&state->first_started) + && sentry__monotonic_time() < deadline) { + sleep_ms(1); + } + sentry__atomic_store(&state->second_ran, 1); + sentry__atomic_store(&state->release_first, 1); + } +} + +static void +threadpool_test_complete(void *data) +{ + struct threadpool_test_task *task = data; + struct threadpool_test_state *state = task->state; + state->completion_order[state->completion_count++] = task->id; +} + +static void +threadpool_test_cleanup(void *data) +{ + struct threadpool_test_task *task = data; + task->state->cleanup_count++; +} + +SENTRY_TEST(threadpool_ordered_parallel) +{ + struct threadpool_test_state state = { 0 }; + struct threadpool_test_task tasks[] = { + { &state, 0 }, + { &state, 1 }, + }; + sentry_threadpool_t *pool = sentry__threadpool_new(2); + TEST_ASSERT(!!pool); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + + for (size_t i = 0; i < 2; i++) { + TEST_ASSERT( + sentry__threadpool_submit(pool, threadpool_test_exec, + threadpool_test_complete, threadpool_test_cleanup, &tasks[i]) + == 0); + } + sentry__threadpool_flush(pool); + + TEST_CHECK(sentry__atomic_fetch(&state.second_ran)); + TEST_CHECK(!state.first_timed_out); + TEST_CHECK_INT_EQUAL(state.completion_count, 2); + TEST_CHECK_INT_EQUAL(state.completion_order[0], 0); + TEST_CHECK_INT_EQUAL(state.completion_order[1], 1); + TEST_CHECK_INT_EQUAL(state.cleanup_count, 2); + + sentry__threadpool_shutdown(pool); + sentry__threadpool_free(pool); +} + +SENTRY_TEST(threadpool_rejected_submit_cleans_up) +{ + struct threadpool_test_state state = { 0 }; + struct threadpool_test_task task = { &state, 0 }; + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + + TEST_CHECK(sentry__threadpool_submit(pool, threadpool_test_exec, + threadpool_test_complete, threadpool_test_cleanup, &task) + != 0); + TEST_CHECK_INT_EQUAL(state.cleanup_count, 1); + TEST_CHECK_INT_EQUAL(state.completion_count, 0); + + sentry__threadpool_free(pool); +} + #define COND_WAKE_ALL_THREADS 2 struct cond_wake_all_state { diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index bc50aec76d..6020bfc1c5 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -387,6 +387,8 @@ XX(stringbuilder_reserve_overflow) XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) +XX(threadpool_ordered_parallel) +XX(threadpool_rejected_submit_cleans_up) XX(trace_continuation_truth_table) XX(trace_finish) XX(traceparent_header_disabled_by_default) From 225e0260f9ecfdc3d241bfea2c7e64b21e083f72 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 4 Aug 2026 11:53:00 +0200 Subject: [PATCH 02/20] free --- src/sentry_sync.c | 2 + tests/unit/test_sync.c | 143 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 146 insertions(+) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 17d4ba01a6..70cc0cc288 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -272,6 +272,7 @@ sentry__threadpool_start(sentry_threadpool_t *pool) sentry__mutex_unlock(&pool->lock); for (size_t j = 0; j < pool->started_threads; j++) { sentry__thread_join(pool->threads[j]); + sentry__thread_free(&pool->threads[j]); } pool->started_threads = 0; pool->running = false; @@ -354,6 +355,7 @@ sentry__threadpool_shutdown(sentry_threadpool_t *pool) for (size_t i = 0; i < pool->started_threads; i++) { sentry__thread_join(pool->threads[i]); + sentry__thread_free(&pool->threads[i]); } pool->started_threads = 0; pool->running = false; diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index 3032169503..0b71e9ae16 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -668,6 +668,149 @@ SENTRY_TEST(threadpool_ordered_parallel) sentry__threadpool_free(pool); } +struct threadpool_restart_state { + int executed; + int completed; + int cleaned_up; +}; + +static void +threadpool_restart_exec(void *data) +{ + struct threadpool_restart_state *state = data; + state->executed++; +} + +static void +threadpool_restart_complete(void *data) +{ + struct threadpool_restart_state *state = data; + state->completed++; +} + +static void +threadpool_restart_cleanup(void *data) +{ + struct threadpool_restart_state *state = data; + state->cleaned_up++; +} + +SENTRY_TEST(threadpool_restart) +{ + struct threadpool_restart_state state = { 0 }; + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + + for (int i = 0; i < 2; i++) { + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + TEST_ASSERT( + sentry__threadpool_submit(pool, threadpool_restart_exec, + threadpool_restart_complete, threadpool_restart_cleanup, &state) + == 0); + sentry__threadpool_flush(pool); + sentry__threadpool_shutdown(pool); + } + + TEST_CHECK_INT_EQUAL(state.executed, 2); + TEST_CHECK_INT_EQUAL(state.completed, 2); + TEST_CHECK_INT_EQUAL(state.cleaned_up, 2); + + sentry__threadpool_free(pool); +} + +#define THREADPOOL_FLUSH_THREADS 2 + +struct threadpool_flush_test_state { + sentry_threadpool_t *pool; + volatile long flush_started; + volatile long flush_done; +}; + +static void +threadpool_flush_test_exec(void *UNUSED(data)) +{ +} + +static void +threadpool_flush_test_complete(void *data) +{ + struct threadpool_flush_test_state *state = data; + const uint64_t deadline = sentry__monotonic_time() + 2000; + + while (sentry__monotonic_time() < deadline) { + if (sentry__atomic_fetch(&state->flush_started) + >= THREADPOOL_FLUSH_THREADS) { + break; + } + sleep_ms(1); + } + sleep_ms(100); +} + +SENTRY_THREAD_FN +threadpool_flush_test_thread(void *data) +{ + struct threadpool_flush_test_state *state = data; + sentry__atomic_fetch_and_add(&state->flush_started, 1); + sentry__threadpool_flush(state->pool); + sentry__atomic_fetch_and_add(&state->flush_done, 1); + return 0; +} + +static bool +wait_for_atomic_count(volatile long *value, long count, uint64_t timeout) +{ + const uint64_t deadline = sentry__monotonic_time() + timeout; + while (sentry__atomic_fetch(value) < count + && sentry__monotonic_time() < deadline) { + sleep_ms(1); + } + return sentry__atomic_fetch(value) == count; +} + +SENTRY_TEST(threadpool_concurrent_flush) +{ + struct threadpool_flush_test_state state = { 0 }; + sentry_threadid_t threads[THREADPOOL_FLUSH_THREADS]; + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + state.pool = pool; + + TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_flush_test_exec, + threadpool_flush_test_complete, NULL, &state) + == 0); + + for (int i = 0; i < THREADPOOL_FLUSH_THREADS; i++) { + sentry__thread_init(&threads[i]); + TEST_ASSERT(sentry__thread_spawn( + &threads[i], threadpool_flush_test_thread, &state) + == 0); + } + + bool all_flushers_returned = wait_for_atomic_count( + &state.flush_done, THREADPOOL_FLUSH_THREADS, 2000); + + if (!all_flushers_returned) { + for (int i = 0; i < THREADPOOL_FLUSH_THREADS; i++) { + TEST_ASSERT(sentry__threadpool_submit( + pool, threadpool_flush_test_exec, NULL, NULL, NULL) + == 0); + } + TEST_CHECK(wait_for_atomic_count( + &state.flush_done, THREADPOOL_FLUSH_THREADS, 2000)); + } + + for (int i = 0; i < THREADPOOL_FLUSH_THREADS; i++) { + sentry__thread_join(threads[i]); + } + + TEST_CHECK(all_flushers_returned); + + sentry__threadpool_shutdown(pool); + sentry__threadpool_free(pool); +} + SENTRY_TEST(threadpool_rejected_submit_cleans_up) { struct threadpool_test_state state = { 0 }; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 6020bfc1c5..bb423fd199 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -389,6 +389,7 @@ XX(task_queue) XX(thread_without_name_still_valid) XX(threadpool_ordered_parallel) XX(threadpool_rejected_submit_cleans_up) +XX(threadpool_restart) XX(trace_continuation_truth_table) XX(trace_finish) XX(traceparent_header_disabled_by_default) From 6b1dacf798bcdfb3045014efb5f4f06a1a6d3a75 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 4 Aug 2026 13:04:16 +0200 Subject: [PATCH 03/20] extend tests --- tests/unit/test_sync.c | 398 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 7 + 2 files changed, 405 insertions(+) diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index 0b71e9ae16..3d01400251 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -768,6 +768,33 @@ wait_for_atomic_count(volatile long *value, long count, uint64_t timeout) return sentry__atomic_fetch(value) == count; } +struct threadpool_count_state { + volatile long executed; + volatile long completed; + volatile long cleaned_up; +}; + +static void +threadpool_count_exec(void *data) +{ + struct threadpool_count_state *state = data; + sentry__atomic_fetch_and_add(&state->executed, 1); +} + +static void +threadpool_count_complete(void *data) +{ + struct threadpool_count_state *state = data; + sentry__atomic_fetch_and_add(&state->completed, 1); +} + +static void +threadpool_count_cleanup(void *data) +{ + struct threadpool_count_state *state = data; + sentry__atomic_fetch_and_add(&state->cleaned_up, 1); +} + SENTRY_TEST(threadpool_concurrent_flush) { struct threadpool_flush_test_state state = { 0 }; @@ -811,6 +838,377 @@ SENTRY_TEST(threadpool_concurrent_flush) sentry__threadpool_free(pool); } +struct threadpool_reentry_state { + volatile long first_complete_entered; + volatile long second_exec_done; + volatile long first_complete_left; + volatile long second_complete_ran; + volatile long second_complete_before_first_left; + volatile long completion_count; + volatile long cleanup_count; + volatile long first_complete_timed_out; + volatile long second_exec_timed_out; + int completion_order[2]; +}; + +struct threadpool_reentry_task { + struct threadpool_reentry_state *state; + int id; +}; + +static void +threadpool_reentry_exec(void *data) +{ + struct threadpool_reentry_task *task = data; + struct threadpool_reentry_state *state = task->state; + + if (task->id != 1) { + return; + } + + const uint64_t deadline = sentry__monotonic_time() + 2000; + while (!sentry__atomic_fetch(&state->first_complete_entered) + && sentry__monotonic_time() < deadline) { + sleep_ms(1); + } + if (!sentry__atomic_fetch(&state->first_complete_entered)) { + sentry__atomic_store(&state->second_exec_timed_out, 1); + } + sentry__atomic_store(&state->second_exec_done, 1); +} + +static void +threadpool_reentry_complete(void *data) +{ + struct threadpool_reentry_task *task = data; + struct threadpool_reentry_state *state = task->state; + long pos = sentry__atomic_fetch_and_add(&state->completion_count, 1); + if (pos < 2) { + state->completion_order[pos] = task->id; + } + + if (task->id == 0) { + sentry__atomic_store(&state->first_complete_entered, 1); + + const uint64_t deadline = sentry__monotonic_time() + 2000; + while (!sentry__atomic_fetch(&state->second_exec_done) + && sentry__monotonic_time() < deadline) { + sleep_ms(1); + } + if (!sentry__atomic_fetch(&state->second_exec_done)) { + sentry__atomic_store(&state->first_complete_timed_out, 1); + } + + sleep_ms(200); + sentry__atomic_store(&state->first_complete_left, 1); + } else if (!sentry__atomic_fetch(&state->first_complete_left)) { + sentry__atomic_store(&state->second_complete_before_first_left, 1); + } + + if (task->id == 1) { + sentry__atomic_store(&state->second_complete_ran, 1); + } +} + +static void +threadpool_reentry_cleanup(void *data) +{ + struct threadpool_reentry_task *task = data; + sentry__atomic_fetch_and_add(&task->state->cleanup_count, 1); +} + +SENTRY_TEST(threadpool_commit_reentry) +{ + struct threadpool_reentry_state state = { 0 }; + struct threadpool_reentry_task tasks[] = { + { &state, 0 }, + { &state, 1 }, + }; + sentry_threadpool_t *pool = sentry__threadpool_new(2); + TEST_ASSERT(!!pool); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + + for (size_t i = 0; i < 2; i++) { + TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_reentry_exec, + threadpool_reentry_complete, threadpool_reentry_cleanup, + &tasks[i]) + == 0); + } + sentry__threadpool_flush(pool); + + TEST_CHECK(!sentry__atomic_fetch(&state.first_complete_timed_out)); + TEST_CHECK(!sentry__atomic_fetch(&state.second_exec_timed_out)); + TEST_CHECK(sentry__atomic_fetch(&state.second_complete_ran)); + TEST_CHECK(!sentry__atomic_fetch(&state.second_complete_before_first_left)); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.completion_count), 2); + TEST_CHECK_INT_EQUAL(state.completion_order[0], 0); + TEST_CHECK_INT_EQUAL(state.completion_order[1], 1); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleanup_count), 2); + + sentry__threadpool_shutdown(pool); + sentry__threadpool_free(pool); +} + +SENTRY_TEST(threadpool_invalid_args) +{ + struct threadpool_count_state state = { 0 }; + + TEST_CHECK_PTR_EQUAL(sentry__threadpool_new(0), NULL); + if (sizeof(sentry_threadid_t) > 1) { + TEST_CHECK_PTR_EQUAL( + sentry__threadpool_new(SIZE_MAX / sizeof(sentry_threadid_t) + 1), + NULL); + } + + TEST_CHECK(sentry__threadpool_start(NULL) != 0); + TEST_CHECK(sentry__threadpool_submit(NULL, threadpool_count_exec, + threadpool_count_complete, threadpool_count_cleanup, &state) + != 0); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleaned_up), 1); + sentry__threadpool_setname(NULL, "ignored"); + sentry__threadpool_flush(NULL); + sentry__threadpool_shutdown(NULL); + sentry__threadpool_free(NULL); + + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + TEST_CHECK_INT_EQUAL(sentry__threadpool_start(pool), 0); + TEST_CHECK(sentry__threadpool_submit(pool, NULL, threadpool_count_complete, + threadpool_count_cleanup, &state) + != 0); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleaned_up), 2); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.completed), 0); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.executed), 0); + + sentry__threadpool_shutdown(pool); + sentry__threadpool_shutdown(pool); + sentry__threadpool_free(pool); +} + +SENTRY_TEST(threadpool_no_completion_callback_cleans_up) +{ + struct threadpool_count_state state = { 0 }; + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + + TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_count_exec, NULL, + threadpool_count_cleanup, &state) + == 0); + sentry__threadpool_flush(pool); + + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.executed), 1); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.completed), 0); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleaned_up), 1); + + sentry__threadpool_shutdown(pool); + sentry__threadpool_free(pool); +} + +struct threadpool_blocking_state { + sentry_threadpool_t *pool; + sentry_mutex_t lock; + sentry_cond_t cond; + volatile long started; + volatile long executed; + volatile long completed; + volatile long cleaned_up; + volatile long release; + volatile long shutdown_done; +}; + +static void +threadpool_blocking_state_init(struct threadpool_blocking_state *state) +{ + sentry__mutex_init(&state->lock); + sentry__cond_init(&state->cond); +} + +static void +threadpool_blocking_state_free(struct threadpool_blocking_state *state) +{ +#ifndef SENTRY_PLATFORM_WINDOWS + pthread_cond_destroy(&state->cond); +#endif + sentry__mutex_free(&state->lock); +} + +static void +threadpool_blocking_release(struct threadpool_blocking_state *state) +{ + sentry__mutex_lock(&state->lock); + sentry__atomic_store(&state->release, 1); + sentry__cond_wake_all(&state->cond); + sentry__mutex_unlock(&state->lock); +} + +static void +threadpool_blocking_exec(void *data) +{ + struct threadpool_blocking_state *state = data; + + sentry__atomic_fetch_and_add(&state->started, 1); + sentry__mutex_lock(&state->lock); + while (!sentry__atomic_fetch(&state->release)) { + sentry__cond_wait_timeout(&state->cond, &state->lock, 100); + } + sentry__mutex_unlock(&state->lock); + sentry__atomic_fetch_and_add(&state->executed, 1); +} + +static void +threadpool_blocking_complete(void *data) +{ + struct threadpool_blocking_state *state = data; + sentry__atomic_fetch_and_add(&state->completed, 1); +} + +static void +threadpool_blocking_cleanup(void *data) +{ + struct threadpool_blocking_state *state = data; + sentry__atomic_fetch_and_add(&state->cleaned_up, 1); +} + +SENTRY_THREAD_FN +threadpool_shutdown_test_thread(void *data) +{ + struct threadpool_blocking_state *state = data; + sentry__threadpool_shutdown(state->pool); + sentry__atomic_fetch_and_add(&state->shutdown_done, 1); + return 0; +} + +SENTRY_TEST(threadpool_shutdown_drains) +{ + enum { TASKS = 4, THREADS = 2 }; + struct threadpool_blocking_state state = { 0 }; + sentry_threadid_t shutdown_thread; + sentry_threadpool_t *pool = sentry__threadpool_new(THREADS); + TEST_ASSERT(!!pool); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + state.pool = pool; + threadpool_blocking_state_init(&state); + + for (int i = 0; i < TASKS; i++) { + TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_blocking_exec, + threadpool_blocking_complete, + threadpool_blocking_cleanup, &state) + == 0); + } + TEST_ASSERT(wait_for_atomic_count(&state.started, THREADS, 2000)); + + sentry__thread_init(&shutdown_thread); + TEST_ASSERT(sentry__thread_spawn( + &shutdown_thread, threadpool_shutdown_test_thread, &state) + == 0); + sleep_ms(100); + threadpool_blocking_release(&state); + sentry__thread_join(shutdown_thread); + sentry__thread_free(&shutdown_thread); + + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.shutdown_done), 1); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.executed), TASKS); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.completed), TASKS); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleaned_up), TASKS); + + sentry__threadpool_free(pool); + threadpool_blocking_state_free(&state); +} + +SENTRY_TEST(threadpool_submit_rejected_while_stopping) +{ + struct threadpool_blocking_state blocking = { 0 }; + struct threadpool_count_state submitted = { 0 }; + sentry_threadid_t shutdown_thread; + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + blocking.pool = pool; + threadpool_blocking_state_init(&blocking); + + TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_blocking_exec, + threadpool_blocking_complete, threadpool_blocking_cleanup, + &blocking) + == 0); + TEST_ASSERT(wait_for_atomic_count(&blocking.started, 1, 2000)); + + sentry__thread_init(&shutdown_thread); + TEST_ASSERT(sentry__thread_spawn(&shutdown_thread, + threadpool_shutdown_test_thread, &blocking) + == 0); + + bool rejected = false; + long accepted = 0; + const uint64_t deadline = sentry__monotonic_time() + 2000; + while (sentry__monotonic_time() < deadline) { + int rv = sentry__threadpool_submit(pool, threadpool_count_exec, NULL, + threadpool_count_cleanup, &submitted); + if (rv != 0) { + rejected = true; + break; + } + accepted++; + sleep_ms(1); + } + + threadpool_blocking_release(&blocking); + sentry__thread_join(shutdown_thread); + sentry__thread_free(&shutdown_thread); + + TEST_CHECK(rejected); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&blocking.executed), 1); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&blocking.completed), 1); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&blocking.cleaned_up), 1); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&submitted.executed), accepted); + TEST_CHECK_INT_EQUAL( + sentry__atomic_fetch(&submitted.cleaned_up), accepted + 1); + + sentry__threadpool_free(pool); + threadpool_blocking_state_free(&blocking); +} + +struct threadpool_name_state { + char thread_name[16]; + volatile long captured; +}; + +static void +threadpool_name_exec(void *data) +{ + struct threadpool_name_state *state = data; +#if defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) + pthread_getname_np( + pthread_self(), state->thread_name, sizeof(state->thread_name)); +#endif + sentry__atomic_store(&state->captured, 1); +} + +SENTRY_TEST(threadpool_thread_name) +{ +#if !defined(SENTRY_PLATFORM_LINUX) || defined(SENTRY_PLATFORM_ANDROID) + SKIP_TEST(); +#else + struct threadpool_name_state state = { 0 }; + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + + sentry__threadpool_setname(pool, "tp"); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + TEST_ASSERT(sentry__threadpool_submit( + pool, threadpool_name_exec, NULL, NULL, &state) + == 0); + sentry__threadpool_flush(pool); + + TEST_CHECK(sentry__atomic_fetch(&state.captured)); + TEST_CHECK_STRING_EQUAL(state.thread_name, "tp-0"); + + sentry__threadpool_shutdown(pool); + sentry__threadpool_free(pool); +#endif +} + SENTRY_TEST(threadpool_rejected_submit_cleans_up) { struct threadpool_test_state state = { 0 }; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index bb423fd199..41b175176f 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -387,9 +387,16 @@ XX(stringbuilder_reserve_overflow) XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) +XX(threadpool_commit_reentry) +XX(threadpool_concurrent_flush) +XX(threadpool_invalid_args) +XX(threadpool_no_completion_callback_cleans_up) XX(threadpool_ordered_parallel) XX(threadpool_rejected_submit_cleans_up) XX(threadpool_restart) +XX(threadpool_shutdown_drains) +XX(threadpool_submit_rejected_while_stopping) +XX(threadpool_thread_name) XX(trace_continuation_truth_table) XX(trace_finish) XX(traceparent_header_disabled_by_default) From 15f19b795603ed89c8520422b169ae6a63487124 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 4 Aug 2026 13:21:46 +0200 Subject: [PATCH 04/20] stabilize tests --- tests/unit/test_sync.c | 334 +++++++---------------------------------- tests/unit/tests.inc | 2 - 2 files changed, 54 insertions(+), 282 deletions(-) diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index 3d01400251..f787bab4e5 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -583,10 +583,11 @@ SENTRY_TEST(bgworker_delayed_shutdown) } struct threadpool_test_state { + sentry_mutex_t lock; + sentry_cond_t cond; volatile long first_started; volatile long release_first; volatile long second_ran; - bool first_timed_out; int completion_order[2]; int completion_count; int cleanup_count; @@ -602,24 +603,24 @@ threadpool_test_exec(void *data) { struct threadpool_test_task *task = data; struct threadpool_test_state *state = task->state; - const uint64_t deadline = sentry__monotonic_time() + 1000; if (task->id == 0) { + sentry__mutex_lock(&state->lock); sentry__atomic_store(&state->first_started, 1); + sentry__cond_wake(&state->cond); while (!sentry__atomic_fetch(&state->release_first)) { - if (sentry__monotonic_time() >= deadline) { - state->first_timed_out = true; - break; - } - sleep_ms(1); + sentry__cond_wait(&state->cond, &state->lock); } + sentry__mutex_unlock(&state->lock); } else { - while (!sentry__atomic_fetch(&state->first_started) - && sentry__monotonic_time() < deadline) { - sleep_ms(1); + sentry__mutex_lock(&state->lock); + while (!sentry__atomic_fetch(&state->first_started)) { + sentry__cond_wait(&state->cond, &state->lock); } sentry__atomic_store(&state->second_ran, 1); sentry__atomic_store(&state->release_first, 1); + sentry__cond_wake(&state->cond); + sentry__mutex_unlock(&state->lock); } } @@ -647,6 +648,8 @@ SENTRY_TEST(threadpool_ordered_parallel) }; sentry_threadpool_t *pool = sentry__threadpool_new(2); TEST_ASSERT(!!pool); + sentry__mutex_init(&state.lock); + sentry__cond_init(&state.cond); TEST_ASSERT(sentry__threadpool_start(pool) == 0); for (size_t i = 0; i < 2; i++) { @@ -658,7 +661,6 @@ SENTRY_TEST(threadpool_ordered_parallel) sentry__threadpool_flush(pool); TEST_CHECK(sentry__atomic_fetch(&state.second_ran)); - TEST_CHECK(!state.first_timed_out); TEST_CHECK_INT_EQUAL(state.completion_count, 2); TEST_CHECK_INT_EQUAL(state.completion_order[0], 0); TEST_CHECK_INT_EQUAL(state.completion_order[1], 1); @@ -666,6 +668,10 @@ SENTRY_TEST(threadpool_ordered_parallel) sentry__threadpool_shutdown(pool); sentry__threadpool_free(pool); +#ifndef SENTRY_PLATFORM_WINDOWS + pthread_cond_destroy(&state.cond); +#endif + sentry__mutex_free(&state.lock); } struct threadpool_restart_state { @@ -718,56 +724,6 @@ SENTRY_TEST(threadpool_restart) sentry__threadpool_free(pool); } -#define THREADPOOL_FLUSH_THREADS 2 - -struct threadpool_flush_test_state { - sentry_threadpool_t *pool; - volatile long flush_started; - volatile long flush_done; -}; - -static void -threadpool_flush_test_exec(void *UNUSED(data)) -{ -} - -static void -threadpool_flush_test_complete(void *data) -{ - struct threadpool_flush_test_state *state = data; - const uint64_t deadline = sentry__monotonic_time() + 2000; - - while (sentry__monotonic_time() < deadline) { - if (sentry__atomic_fetch(&state->flush_started) - >= THREADPOOL_FLUSH_THREADS) { - break; - } - sleep_ms(1); - } - sleep_ms(100); -} - -SENTRY_THREAD_FN -threadpool_flush_test_thread(void *data) -{ - struct threadpool_flush_test_state *state = data; - sentry__atomic_fetch_and_add(&state->flush_started, 1); - sentry__threadpool_flush(state->pool); - sentry__atomic_fetch_and_add(&state->flush_done, 1); - return 0; -} - -static bool -wait_for_atomic_count(volatile long *value, long count, uint64_t timeout) -{ - const uint64_t deadline = sentry__monotonic_time() + timeout; - while (sentry__atomic_fetch(value) < count - && sentry__monotonic_time() < deadline) { - sleep_ms(1); - } - return sentry__atomic_fetch(value) == count; -} - struct threadpool_count_state { volatile long executed; volatile long completed; @@ -795,59 +751,16 @@ threadpool_count_cleanup(void *data) sentry__atomic_fetch_and_add(&state->cleaned_up, 1); } -SENTRY_TEST(threadpool_concurrent_flush) -{ - struct threadpool_flush_test_state state = { 0 }; - sentry_threadid_t threads[THREADPOOL_FLUSH_THREADS]; - sentry_threadpool_t *pool = sentry__threadpool_new(1); - TEST_ASSERT(!!pool); - TEST_ASSERT(sentry__threadpool_start(pool) == 0); - state.pool = pool; - - TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_flush_test_exec, - threadpool_flush_test_complete, NULL, &state) - == 0); - - for (int i = 0; i < THREADPOOL_FLUSH_THREADS; i++) { - sentry__thread_init(&threads[i]); - TEST_ASSERT(sentry__thread_spawn( - &threads[i], threadpool_flush_test_thread, &state) - == 0); - } - - bool all_flushers_returned = wait_for_atomic_count( - &state.flush_done, THREADPOOL_FLUSH_THREADS, 2000); - - if (!all_flushers_returned) { - for (int i = 0; i < THREADPOOL_FLUSH_THREADS; i++) { - TEST_ASSERT(sentry__threadpool_submit( - pool, threadpool_flush_test_exec, NULL, NULL, NULL) - == 0); - } - TEST_CHECK(wait_for_atomic_count( - &state.flush_done, THREADPOOL_FLUSH_THREADS, 2000)); - } - - for (int i = 0; i < THREADPOOL_FLUSH_THREADS; i++) { - sentry__thread_join(threads[i]); - } - - TEST_CHECK(all_flushers_returned); - - sentry__threadpool_shutdown(pool); - sentry__threadpool_free(pool); -} - struct threadpool_reentry_state { - volatile long first_complete_entered; - volatile long second_exec_done; - volatile long first_complete_left; + sentry_mutex_t lock; + sentry_cond_t cond; + bool first_complete_entered; + bool second_exec_done; + bool first_complete_left; volatile long second_complete_ran; volatile long second_complete_before_first_left; volatile long completion_count; volatile long cleanup_count; - volatile long first_complete_timed_out; - volatile long second_exec_timed_out; int completion_order[2]; }; @@ -866,15 +779,13 @@ threadpool_reentry_exec(void *data) return; } - const uint64_t deadline = sentry__monotonic_time() + 2000; - while (!sentry__atomic_fetch(&state->first_complete_entered) - && sentry__monotonic_time() < deadline) { - sleep_ms(1); - } - if (!sentry__atomic_fetch(&state->first_complete_entered)) { - sentry__atomic_store(&state->second_exec_timed_out, 1); + sentry__mutex_lock(&state->lock); + while (!state->first_complete_entered) { + sentry__cond_wait(&state->cond, &state->lock); } - sentry__atomic_store(&state->second_exec_done, 1); + state->second_exec_done = true; + sentry__cond_wake(&state->cond); + sentry__mutex_unlock(&state->lock); } static void @@ -888,21 +799,21 @@ threadpool_reentry_complete(void *data) } if (task->id == 0) { - sentry__atomic_store(&state->first_complete_entered, 1); - - const uint64_t deadline = sentry__monotonic_time() + 2000; - while (!sentry__atomic_fetch(&state->second_exec_done) - && sentry__monotonic_time() < deadline) { - sleep_ms(1); + sentry__mutex_lock(&state->lock); + state->first_complete_entered = true; + sentry__cond_wake(&state->cond); + while (!state->second_exec_done) { + sentry__cond_wait(&state->cond, &state->lock); } - if (!sentry__atomic_fetch(&state->second_exec_done)) { - sentry__atomic_store(&state->first_complete_timed_out, 1); + state->first_complete_left = true; + sentry__mutex_unlock(&state->lock); + } else { + sentry__mutex_lock(&state->lock); + bool first_complete_left = state->first_complete_left; + sentry__mutex_unlock(&state->lock); + if (!first_complete_left) { + sentry__atomic_store(&state->second_complete_before_first_left, 1); } - - sleep_ms(200); - sentry__atomic_store(&state->first_complete_left, 1); - } else if (!sentry__atomic_fetch(&state->first_complete_left)) { - sentry__atomic_store(&state->second_complete_before_first_left, 1); } if (task->id == 1) { @@ -926,6 +837,8 @@ SENTRY_TEST(threadpool_commit_reentry) }; sentry_threadpool_t *pool = sentry__threadpool_new(2); TEST_ASSERT(!!pool); + sentry__mutex_init(&state.lock); + sentry__cond_init(&state.cond); TEST_ASSERT(sentry__threadpool_start(pool) == 0); for (size_t i = 0; i < 2; i++) { @@ -936,8 +849,6 @@ SENTRY_TEST(threadpool_commit_reentry) } sentry__threadpool_flush(pool); - TEST_CHECK(!sentry__atomic_fetch(&state.first_complete_timed_out)); - TEST_CHECK(!sentry__atomic_fetch(&state.second_exec_timed_out)); TEST_CHECK(sentry__atomic_fetch(&state.second_complete_ran)); TEST_CHECK(!sentry__atomic_fetch(&state.second_complete_before_first_left)); TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.completion_count), 2); @@ -947,6 +858,10 @@ SENTRY_TEST(threadpool_commit_reentry) sentry__threadpool_shutdown(pool); sentry__threadpool_free(pool); +#ifndef SENTRY_PLATFORM_WINDOWS + pthread_cond_destroy(&state.cond); +#endif + sentry__mutex_free(&state.lock); } SENTRY_TEST(threadpool_invalid_args) @@ -1006,167 +921,26 @@ SENTRY_TEST(threadpool_no_completion_callback_cleans_up) sentry__threadpool_free(pool); } -struct threadpool_blocking_state { - sentry_threadpool_t *pool; - sentry_mutex_t lock; - sentry_cond_t cond; - volatile long started; - volatile long executed; - volatile long completed; - volatile long cleaned_up; - volatile long release; - volatile long shutdown_done; -}; - -static void -threadpool_blocking_state_init(struct threadpool_blocking_state *state) -{ - sentry__mutex_init(&state->lock); - sentry__cond_init(&state->cond); -} - -static void -threadpool_blocking_state_free(struct threadpool_blocking_state *state) -{ -#ifndef SENTRY_PLATFORM_WINDOWS - pthread_cond_destroy(&state->cond); -#endif - sentry__mutex_free(&state->lock); -} - -static void -threadpool_blocking_release(struct threadpool_blocking_state *state) -{ - sentry__mutex_lock(&state->lock); - sentry__atomic_store(&state->release, 1); - sentry__cond_wake_all(&state->cond); - sentry__mutex_unlock(&state->lock); -} - -static void -threadpool_blocking_exec(void *data) -{ - struct threadpool_blocking_state *state = data; - - sentry__atomic_fetch_and_add(&state->started, 1); - sentry__mutex_lock(&state->lock); - while (!sentry__atomic_fetch(&state->release)) { - sentry__cond_wait_timeout(&state->cond, &state->lock, 100); - } - sentry__mutex_unlock(&state->lock); - sentry__atomic_fetch_and_add(&state->executed, 1); -} - -static void -threadpool_blocking_complete(void *data) -{ - struct threadpool_blocking_state *state = data; - sentry__atomic_fetch_and_add(&state->completed, 1); -} - -static void -threadpool_blocking_cleanup(void *data) -{ - struct threadpool_blocking_state *state = data; - sentry__atomic_fetch_and_add(&state->cleaned_up, 1); -} - -SENTRY_THREAD_FN -threadpool_shutdown_test_thread(void *data) -{ - struct threadpool_blocking_state *state = data; - sentry__threadpool_shutdown(state->pool); - sentry__atomic_fetch_and_add(&state->shutdown_done, 1); - return 0; -} - SENTRY_TEST(threadpool_shutdown_drains) { - enum { TASKS = 4, THREADS = 2 }; - struct threadpool_blocking_state state = { 0 }; - sentry_threadid_t shutdown_thread; - sentry_threadpool_t *pool = sentry__threadpool_new(THREADS); + enum { TASKS = 16 }; + struct threadpool_count_state state = { 0 }; + sentry_threadpool_t *pool = sentry__threadpool_new(2); TEST_ASSERT(!!pool); TEST_ASSERT(sentry__threadpool_start(pool) == 0); - state.pool = pool; - threadpool_blocking_state_init(&state); for (int i = 0; i < TASKS; i++) { - TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_blocking_exec, - threadpool_blocking_complete, - threadpool_blocking_cleanup, &state) + TEST_ASSERT( + sentry__threadpool_submit(pool, threadpool_count_exec, + threadpool_count_complete, threadpool_count_cleanup, &state) == 0); } - TEST_ASSERT(wait_for_atomic_count(&state.started, THREADS, 2000)); - - sentry__thread_init(&shutdown_thread); - TEST_ASSERT(sentry__thread_spawn( - &shutdown_thread, threadpool_shutdown_test_thread, &state) - == 0); - sleep_ms(100); - threadpool_blocking_release(&state); - sentry__thread_join(shutdown_thread); - sentry__thread_free(&shutdown_thread); + sentry__threadpool_shutdown(pool); - TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.shutdown_done), 1); TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.executed), TASKS); TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.completed), TASKS); TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleaned_up), TASKS); - - sentry__threadpool_free(pool); - threadpool_blocking_state_free(&state); -} - -SENTRY_TEST(threadpool_submit_rejected_while_stopping) -{ - struct threadpool_blocking_state blocking = { 0 }; - struct threadpool_count_state submitted = { 0 }; - sentry_threadid_t shutdown_thread; - sentry_threadpool_t *pool = sentry__threadpool_new(1); - TEST_ASSERT(!!pool); - TEST_ASSERT(sentry__threadpool_start(pool) == 0); - blocking.pool = pool; - threadpool_blocking_state_init(&blocking); - - TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_blocking_exec, - threadpool_blocking_complete, threadpool_blocking_cleanup, - &blocking) - == 0); - TEST_ASSERT(wait_for_atomic_count(&blocking.started, 1, 2000)); - - sentry__thread_init(&shutdown_thread); - TEST_ASSERT(sentry__thread_spawn(&shutdown_thread, - threadpool_shutdown_test_thread, &blocking) - == 0); - - bool rejected = false; - long accepted = 0; - const uint64_t deadline = sentry__monotonic_time() + 2000; - while (sentry__monotonic_time() < deadline) { - int rv = sentry__threadpool_submit(pool, threadpool_count_exec, NULL, - threadpool_count_cleanup, &submitted); - if (rv != 0) { - rejected = true; - break; - } - accepted++; - sleep_ms(1); - } - - threadpool_blocking_release(&blocking); - sentry__thread_join(shutdown_thread); - sentry__thread_free(&shutdown_thread); - - TEST_CHECK(rejected); - TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&blocking.executed), 1); - TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&blocking.completed), 1); - TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&blocking.cleaned_up), 1); - TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&submitted.executed), accepted); - TEST_CHECK_INT_EQUAL( - sentry__atomic_fetch(&submitted.cleaned_up), accepted + 1); - sentry__threadpool_free(pool); - threadpool_blocking_state_free(&blocking); } struct threadpool_name_state { diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 41b175176f..38ec6b68c1 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -388,14 +388,12 @@ XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) XX(threadpool_commit_reentry) -XX(threadpool_concurrent_flush) XX(threadpool_invalid_args) XX(threadpool_no_completion_callback_cleans_up) XX(threadpool_ordered_parallel) XX(threadpool_rejected_submit_cleans_up) XX(threadpool_restart) XX(threadpool_shutdown_drains) -XX(threadpool_submit_rejected_while_stopping) XX(threadpool_thread_name) XX(trace_continuation_truth_table) XX(trace_finish) From 7d3d6ae897eba243fba9e901c72e596667ddf4c2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 4 Aug 2026 14:10:35 +0200 Subject: [PATCH 05/20] fix unused threadpool_name_exec warning --- tests/unit/test_sync.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index f787bab4e5..82cab9d83c 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -943,6 +943,7 @@ SENTRY_TEST(threadpool_shutdown_drains) sentry__threadpool_free(pool); } +#if defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) struct threadpool_name_state { char thread_name[16]; volatile long captured; @@ -952,12 +953,11 @@ static void threadpool_name_exec(void *data) { struct threadpool_name_state *state = data; -#if defined(SENTRY_PLATFORM_LINUX) && !defined(SENTRY_PLATFORM_ANDROID) pthread_getname_np( pthread_self(), state->thread_name, sizeof(state->thread_name)); -#endif sentry__atomic_store(&state->captured, 1); } +#endif SENTRY_TEST(threadpool_thread_name) { From d54e36ee77bdce0ea03a2af81f91e80bce16630a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 5 Aug 2026 10:45:13 +0200 Subject: [PATCH 06/20] atomic running --- src/sentry_sync.c | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 70cc0cc288..854611b205 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -130,7 +130,7 @@ struct sentry_threadpool_s { sentry_threadpool_task_t *next_task; long pending; long index; - bool running; + long running; bool stopping; bool committing; }; @@ -258,10 +258,10 @@ sentry__threadpool_setname(sentry_threadpool_t *pool, const char *thread_name) int sentry__threadpool_start(sentry_threadpool_t *pool) { - if (!pool || pool->running) { + if (!pool || sentry__atomic_fetch(&pool->running)) { return pool ? 0 : 1; } - pool->running = true; + sentry__atomic_store(&pool->running, 1); pool->stopping = false; for (size_t i = 0; i < pool->thread_count; i++) { if (sentry__thread_spawn(&pool->threads[i], threadpool_worker, pool) @@ -275,7 +275,7 @@ sentry__threadpool_start(sentry_threadpool_t *pool) sentry__thread_free(&pool->threads[j]); } pool->started_threads = 0; - pool->running = false; + sentry__atomic_store(&pool->running, 0); return 1; } pool->started_threads++; @@ -307,7 +307,7 @@ sentry__threadpool_submit(sentry_threadpool_t *pool, task->task_data = task_data; sentry__mutex_lock(&pool->lock); - if (!pool->running || pool->stopping) { + if (!sentry__atomic_fetch(&pool->running) || pool->stopping) { sentry__mutex_unlock(&pool->lock); threadpool_task_free(task); return 1; @@ -331,7 +331,7 @@ sentry__threadpool_submit(sentry_threadpool_t *pool, void sentry__threadpool_flush(sentry_threadpool_t *pool) { - if (!pool || !pool->running) { + if (!pool || !sentry__atomic_fetch(&pool->running)) { return; } sentry__mutex_lock(&pool->lock); @@ -344,7 +344,7 @@ sentry__threadpool_flush(sentry_threadpool_t *pool) void sentry__threadpool_shutdown(sentry_threadpool_t *pool) { - if (!pool || !pool->running) { + if (!pool || !sentry__atomic_fetch(&pool->running)) { return; } sentry__mutex_lock(&pool->lock); @@ -358,7 +358,7 @@ sentry__threadpool_shutdown(sentry_threadpool_t *pool) sentry__thread_free(&pool->threads[i]); } pool->started_threads = 0; - pool->running = false; + sentry__atomic_store(&pool->running, 0); pool->index = 0; } From 8db1bc6a0c95d19d0997478d87b6960a9a4fedd4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 5 Aug 2026 11:19:01 +0200 Subject: [PATCH 07/20] docs --- src/sentry_sync.c | 4 ++-- src/sentry_sync.h | 45 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 854611b205..656e48f2a8 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -185,7 +185,7 @@ threadpool_commit_ready(sentry_threadpool_t *pool) } SENTRY_THREAD_FN -threadpool_worker(void *data) +threadpool_thread(void *data) { sentry_threadpool_t *pool = data; if (pool->thread_name) { @@ -264,7 +264,7 @@ sentry__threadpool_start(sentry_threadpool_t *pool) sentry__atomic_store(&pool->running, 1); pool->stopping = false; for (size_t i = 0; i < pool->thread_count; i++) { - if (sentry__thread_spawn(&pool->threads[i], threadpool_worker, pool) + if (sentry__thread_spawn(&pool->threads[i], threadpool_thread, pool) != 0) { sentry__mutex_lock(&pool->lock); pool->stopping = true; diff --git a/src/sentry_sync.h b/src/sentry_sync.h index cba287100c..85530003c7 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -528,22 +528,59 @@ typedef struct sentry_threadpool_s sentry_threadpool_t; typedef void (*sentry_task_exec_func_t)(void *task_data, void *state); /** - * Creates a thread pool. Tasks execute in parallel, while completion callbacks - * run in submission order. + * Creates a thread pool configured with `thread_count` threads. Tasks execute + * in parallel, while completion callbacks run in submission order. */ sentry_threadpool_t *sentry__threadpool_new(size_t thread_count); + +/** + * Sets a name for pooled threads. Each thread is named `-`, + * where `index` starts at 0. + * + * Should be executed before thread pool start. + */ void sentry__threadpool_setname( sentry_threadpool_t *pool, const char *thread_name); + +/** + * Starts the pooled threads. Calling this on a running pool succeeds without + * effect. A pool can be restarted after shutdown. + * + * Returns 0 on success, or a non-zero value if `pool` is `NULL` or the pooled + * threads cannot be started. + */ int sentry__threadpool_start(sentry_threadpool_t *pool); + /** - * Takes ownership of `task_data`, freeing it using `cleanup_func` when the - * task is completed, cancelled, or rejected. + * Submits a task for execution. `exec_func` runs on a pooled thread. The + * optional `complete_func` runs after execution, in submission order, followed + * by the optional `cleanup_func`. + * + * Takes ownership of `task_data` on every call. If the task is rejected, + * `cleanup_func` is called immediately when provided. + * + * Returns 0 if the task was accepted, or a non-zero value if the arguments are + * invalid, the pool is not running or is stopping, or allocation fails. */ int sentry__threadpool_submit(sentry_threadpool_t *pool, void (*exec_func)(void *task_data), void (*complete_func)(void *task_data), void (*cleanup_func)(void *task_data), void *task_data); + +/** + * Blocks until all accepted tasks and their completion and cleanup callbacks + * have finished. + */ void sentry__threadpool_flush(sentry_threadpool_t *pool); + +/** + * Stops accepting tasks, drains the queue, and joins all pooled threads. The + * pool can be started again after shutdown. + */ void sentry__threadpool_shutdown(sentry_threadpool_t *pool); + +/** + * Shuts down the pool if necessary and releases its resources. + */ void sentry__threadpool_free(sentry_threadpool_t *pool); /** From e8baa7dda2641e332138c26a1e69a8e174017b47 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 5 Aug 2026 11:30:33 +0200 Subject: [PATCH 08/20] prevent deadlocking flush from pooled threads --- src/sentry_sync.c | 16 ++++++++++++++++ src/sentry_sync.h | 2 +- tests/unit/test_sync.c | 32 ++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 4 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 656e48f2a8..cd1a8fddd8 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -152,6 +152,18 @@ threadpool_wake_all(sentry_threadpool_t *pool) } } +static bool +is_pooled_thread(sentry_threadpool_t *pool) +{ + const sentry_threadid_t current = sentry__current_thread(); + for (size_t i = 0; i < pool->started_threads; i++) { + if (sentry__threadid_equal(current, pool->threads[i])) { + return true; + } + } + return false; +} + static void threadpool_commit_ready(sentry_threadpool_t *pool) { @@ -334,6 +346,10 @@ sentry__threadpool_flush(sentry_threadpool_t *pool) if (!pool || !sentry__atomic_fetch(&pool->running)) { return; } + if (is_pooled_thread(pool)) { + SENTRY_WARN("cannot flush thread pool from a pooled thread"); + return; + } sentry__mutex_lock(&pool->lock); while (sentry__atomic_fetch(&pool->pending) > 0) { sentry__cond_wait(&pool->state_signal, &pool->lock); diff --git a/src/sentry_sync.h b/src/sentry_sync.h index 85530003c7..8394331442 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -568,7 +568,7 @@ int sentry__threadpool_submit(sentry_threadpool_t *pool, /** * Blocks until all accepted tasks and their completion and cleanup callbacks - * have finished. + * have finished. Does nothing when called from one of the pool's threads. */ void sentry__threadpool_flush(sentry_threadpool_t *pool); diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index 82cab9d83c..6047e54c25 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -864,6 +864,38 @@ SENTRY_TEST(threadpool_commit_reentry) sentry__mutex_free(&state.lock); } +struct callback_flush_state { + sentry_threadpool_t *pool; + volatile long calls; +}; + +static void +callback_flush(void *data) +{ + struct callback_flush_state *state = data; + sentry__threadpool_flush(state->pool); + sentry__atomic_fetch_and_add(&state->calls, 1); +} + +SENTRY_TEST(threadpool_callback_flush) +{ + struct callback_flush_state state = { 0 }; + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + state.pool = pool; + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + TEST_ASSERT(sentry__threadpool_submit(pool, callback_flush, callback_flush, + callback_flush, &state) + == 0); + + sentry__threadpool_flush(pool); + + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.calls), 3); + + sentry__threadpool_shutdown(pool); + sentry__threadpool_free(pool); +} + SENTRY_TEST(threadpool_invalid_args) { struct threadpool_count_state state = { 0 }; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 38ec6b68c1..483d939f26 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -387,6 +387,7 @@ XX(stringbuilder_reserve_overflow) XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) +XX(threadpool_callback_flush) XX(threadpool_commit_reentry) XX(threadpool_invalid_args) XX(threadpool_no_completion_callback_cleans_up) From 4473771511473ac177afbc29c9250de3b4feffc5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 5 Aug 2026 12:07:16 +0200 Subject: [PATCH 09/20] clean up spin_lock --- src/sentry_sync.h | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/sentry_sync.h b/src/sentry_sync.h index 8394331442..46b51c4584 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -3,7 +3,6 @@ #include "sentry_boot.h" #include "sentry_core.h" -#include "sentry_cpu_relax.h" #include #include @@ -453,35 +452,6 @@ sentry__atomic_compare_swap(volatile long *val, long expected, long desired) #endif } -typedef bool (*sentry_spin_wait_func_t)(int attempt, void *data); - -static inline void -sentry__spin_lock(volatile long *lock) -{ - while (!sentry__atomic_compare_swap(lock, 0, 1)) { - sentry__cpu_relax(); - } -} - -static inline bool -sentry__spin_lock_wait( - volatile long *lock, sentry_spin_wait_func_t wait_func, void *data) -{ - int attempts = 0; - while (!sentry__atomic_compare_swap(lock, 0, 1)) { - if (!wait_func || !wait_func(++attempts, data)) { - return false; - } - } - return true; -} - -static inline void -sentry__spin_unlock(volatile long *lock) -{ - sentry__atomic_store(lock, 0); -} - /** * 64-bit variants of the atomic helpers above. The `long`-based helpers are * only 32 bits wide on Windows and 32-bit POSIX targets, so callers that need From 36e0b9f4b20f33d49e0dfc200222b7e661fb9f30 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 5 Aug 2026 12:32:36 +0200 Subject: [PATCH 10/20] lock --- src/sentry_sync.c | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index cd1a8fddd8..6075a9cf5d 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -270,28 +270,42 @@ sentry__threadpool_setname(sentry_threadpool_t *pool, const char *thread_name) int sentry__threadpool_start(sentry_threadpool_t *pool) { - if (!pool || sentry__atomic_fetch(&pool->running)) { - return pool ? 0 : 1; + if (!pool) { + return 1; + } + + sentry__mutex_lock(&pool->lock); + if (sentry__atomic_fetch(&pool->running)) { + sentry__mutex_unlock(&pool->lock); + return 0; } + sentry__atomic_store(&pool->running, 1); pool->stopping = false; for (size_t i = 0; i < pool->thread_count; i++) { if (sentry__thread_spawn(&pool->threads[i], threadpool_thread, pool) != 0) { - sentry__mutex_lock(&pool->lock); pool->stopping = true; + const size_t started_threads = pool->started_threads; threadpool_wake_all(pool); sentry__mutex_unlock(&pool->lock); - for (size_t j = 0; j < pool->started_threads; j++) { + + for (size_t j = 0; j < started_threads; j++) { sentry__thread_join(pool->threads[j]); + } + + sentry__mutex_lock(&pool->lock); + for (size_t j = 0; j < started_threads; j++) { sentry__thread_free(&pool->threads[j]); } pool->started_threads = 0; sentry__atomic_store(&pool->running, 0); + sentry__mutex_unlock(&pool->lock); return 1; } pool->started_threads++; } + sentry__mutex_unlock(&pool->lock); return 0; } @@ -346,11 +360,13 @@ sentry__threadpool_flush(sentry_threadpool_t *pool) if (!pool || !sentry__atomic_fetch(&pool->running)) { return; } + + sentry__mutex_lock(&pool->lock); if (is_pooled_thread(pool)) { + sentry__mutex_unlock(&pool->lock); SENTRY_WARN("cannot flush thread pool from a pooled thread"); return; } - sentry__mutex_lock(&pool->lock); while (sentry__atomic_fetch(&pool->pending) > 0) { sentry__cond_wait(&pool->state_signal, &pool->lock); } @@ -363,19 +379,30 @@ sentry__threadpool_shutdown(sentry_threadpool_t *pool) if (!pool || !sentry__atomic_fetch(&pool->running)) { return; } + sentry__mutex_lock(&pool->lock); + if (pool->stopping) { + sentry__mutex_unlock(&pool->lock); + return; + } pool->stopping = true; + const size_t started_threads = pool->started_threads; threadpool_wake_all(pool); sentry__cond_wake(&pool->state_signal); sentry__mutex_unlock(&pool->lock); - for (size_t i = 0; i < pool->started_threads; i++) { + for (size_t i = 0; i < started_threads; i++) { sentry__thread_join(pool->threads[i]); + } + + sentry__mutex_lock(&pool->lock); + for (size_t i = 0; i < started_threads; i++) { sentry__thread_free(&pool->threads[i]); } pool->started_threads = 0; sentry__atomic_store(&pool->running, 0); pool->index = 0; + sentry__mutex_unlock(&pool->lock); } void From db8595b02ebc0f0b6f3fee9ba526a37090f4ef6d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 5 Aug 2026 12:50:57 +0200 Subject: [PATCH 11/20] stop --- src/sentry_sync.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 6075a9cf5d..18d1af0ffe 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -275,6 +275,9 @@ sentry__threadpool_start(sentry_threadpool_t *pool) } sentry__mutex_lock(&pool->lock); + while (sentry__atomic_fetch(&pool->running) && pool->stopping) { + sentry__cond_wait(&pool->state_signal, &pool->lock); + } if (sentry__atomic_fetch(&pool->running)) { sentry__mutex_unlock(&pool->lock); return 0; @@ -300,6 +303,7 @@ sentry__threadpool_start(sentry_threadpool_t *pool) } pool->started_threads = 0; sentry__atomic_store(&pool->running, 0); + sentry__cond_wake_all(&pool->state_signal); sentry__mutex_unlock(&pool->lock); return 1; } @@ -382,6 +386,9 @@ sentry__threadpool_shutdown(sentry_threadpool_t *pool) sentry__mutex_lock(&pool->lock); if (pool->stopping) { + while (sentry__atomic_fetch(&pool->running)) { + sentry__cond_wait(&pool->state_signal, &pool->lock); + } sentry__mutex_unlock(&pool->lock); return; } @@ -402,6 +409,7 @@ sentry__threadpool_shutdown(sentry_threadpool_t *pool) pool->started_threads = 0; sentry__atomic_store(&pool->running, 0); pool->index = 0; + sentry__cond_wake_all(&pool->state_signal); sentry__mutex_unlock(&pool->lock); } From b79430533fcd94dfd6bc20bd24b7c3aed60a378b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 6 Aug 2026 10:16:20 +0200 Subject: [PATCH 12/20] cleanup include --- src/sentry_sync.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 18d1af0ffe..e66c728a92 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -1,7 +1,6 @@ #include "sentry_sync.h" #include "sentry_alloc.h" #include "sentry_core.h" -#include "sentry_cpu_relax.h" #include "sentry_string.h" #include "sentry_utils.h" #include From 41bb76b9a70f947e3f148562e5700c7cc0608fd0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 6 Aug 2026 10:59:18 +0200 Subject: [PATCH 13/20] reset index --- src/sentry_sync.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index e66c728a92..6a227c6c9b 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -301,6 +301,7 @@ sentry__threadpool_start(sentry_threadpool_t *pool) sentry__thread_free(&pool->threads[j]); } pool->started_threads = 0; + pool->index = 0; sentry__atomic_store(&pool->running, 0); sentry__cond_wake_all(&pool->state_signal); sentry__mutex_unlock(&pool->lock); @@ -406,8 +407,8 @@ sentry__threadpool_shutdown(sentry_threadpool_t *pool) sentry__thread_free(&pool->threads[i]); } pool->started_threads = 0; - sentry__atomic_store(&pool->running, 0); pool->index = 0; + sentry__atomic_store(&pool->running, 0); sentry__cond_wake_all(&pool->state_signal); sentry__mutex_unlock(&pool->lock); } From 7951397f8be7e7e81b78942f1a8728f4f2bf9c24 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 6 Aug 2026 11:22:04 +0200 Subject: [PATCH 14/20] guard --- src/sentry_sync.c | 14 +++++++++++++ tests/unit/test_sync.c | 45 +++++++++++++++++++++++++++++++++++------- tests/unit/tests.inc | 2 +- 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 6a227c6c9b..4bb5d962a1 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -385,6 +385,11 @@ sentry__threadpool_shutdown(sentry_threadpool_t *pool) } sentry__mutex_lock(&pool->lock); + if (is_pooled_thread(pool)) { + sentry__mutex_unlock(&pool->lock); + SENTRY_WARN("cannot shut down thread pool from a pooled thread"); + return; + } if (pool->stopping) { while (sentry__atomic_fetch(&pool->running)) { sentry__cond_wait(&pool->state_signal, &pool->lock); @@ -419,6 +424,15 @@ sentry__threadpool_free(sentry_threadpool_t *pool) if (!pool) { return; } + if (sentry__atomic_fetch(&pool->running)) { + sentry__mutex_lock(&pool->lock); + if (is_pooled_thread(pool)) { + sentry__mutex_unlock(&pool->lock); + SENTRY_WARN("cannot free thread pool from a pooled thread"); + return; + } + sentry__mutex_unlock(&pool->lock); + } sentry__threadpool_shutdown(pool); sentry_threadpool_task_t *task = pool->first_task; while (task) { diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index 6047e54c25..5af41c66f3 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -864,22 +864,45 @@ SENTRY_TEST(threadpool_commit_reentry) sentry__mutex_free(&state.lock); } -struct callback_flush_state { +struct callback_pool_state { sentry_threadpool_t *pool; - volatile long calls; + volatile long flush_calls; + volatile long shutdown_calls; + volatile long free_calls; }; static void callback_flush(void *data) { - struct callback_flush_state *state = data; + struct callback_pool_state *state = data; sentry__threadpool_flush(state->pool); - sentry__atomic_fetch_and_add(&state->calls, 1); + sentry__atomic_fetch_and_add(&state->flush_calls, 1); } -SENTRY_TEST(threadpool_callback_flush) +static void +callback_shutdown(void *data) +{ + struct callback_pool_state *state = data; + sentry__threadpool_shutdown(state->pool); + sentry__atomic_fetch_and_add(&state->shutdown_calls, 1); +} + +static void +threadpool_noop_exec(void *UNUSED(data)) +{ +} + +static void +callback_free(void *data) +{ + struct callback_pool_state *state = data; + sentry__threadpool_free(state->pool); + sentry__atomic_fetch_and_add(&state->free_calls, 1); +} + +SENTRY_TEST(threadpool_guard) { - struct callback_flush_state state = { 0 }; + struct callback_pool_state state = { 0 }; sentry_threadpool_t *pool = sentry__threadpool_new(1); TEST_ASSERT(!!pool); state.pool = pool; @@ -887,10 +910,18 @@ SENTRY_TEST(threadpool_callback_flush) TEST_ASSERT(sentry__threadpool_submit(pool, callback_flush, callback_flush, callback_flush, &state) == 0); + TEST_ASSERT(sentry__threadpool_submit( + pool, threadpool_noop_exec, callback_shutdown, NULL, &state) + == 0); + TEST_ASSERT(sentry__threadpool_submit( + pool, threadpool_noop_exec, callback_free, NULL, &state) + == 0); sentry__threadpool_flush(pool); - TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.calls), 3); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.flush_calls), 3); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.shutdown_calls), 1); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.free_calls), 1); sentry__threadpool_shutdown(pool); sentry__threadpool_free(pool); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 483d939f26..2fdc5eeb54 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -387,8 +387,8 @@ XX(stringbuilder_reserve_overflow) XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) -XX(threadpool_callback_flush) XX(threadpool_commit_reentry) +XX(threadpool_guard) XX(threadpool_invalid_args) XX(threadpool_no_completion_callback_cleans_up) XX(threadpool_ordered_parallel) From 6ca0dceb9232794d4f4316829d6ef2b85540ad1b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 6 Aug 2026 12:09:20 +0200 Subject: [PATCH 15/20] wake_all --- src/sentry_sync.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 4bb5d962a1..b486aaafd1 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -146,9 +146,7 @@ threadpool_task_free(sentry_threadpool_task_t *task) static void threadpool_wake_all(sentry_threadpool_t *pool) { - for (size_t i = 0; i < pool->started_threads; i++) { - sentry__cond_wake(&pool->work_signal); - } + sentry__cond_wake_all(&pool->work_signal); } static bool From e7f44ddc32add0f435e5967b285bac39f2954832 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 11 Aug 2026 15:13:36 +0200 Subject: [PATCH 16/20] flush wakes all --- src/sentry_sync.c | 2 +- tests/unit/test_sync.c | 97 ++++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index b486aaafd1..8b1902e0cd 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -184,11 +184,11 @@ threadpool_commit_ready(sentry_threadpool_t *pool) sentry__mutex_lock(&pool->lock); sentry__atomic_fetch_and_add(&pool->pending, -1); - sentry__cond_wake(&pool->state_signal); } pool->committing = false; if (sentry__atomic_fetch(&pool->pending) == 0) { + sentry__cond_wake_all(&pool->state_signal); threadpool_wake_all(pool); } } diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index 5af41c66f3..dbee926cdf 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -639,6 +639,103 @@ threadpool_test_cleanup(void *data) task->state->cleanup_count++; } +struct threadpool_flush_state { + sentry_threadpool_t *pool; + sentry_mutex_t lock; + sentry_cond_t cond; + int flushing; + int flushed; +}; + +SENTRY_THREAD_FN +threadpool_flush_thread(void *data) +{ + struct threadpool_flush_state *state = data; + + sentry__mutex_lock(&state->lock); + state->flushing++; + sentry__cond_wake_all(&state->cond); + sentry__mutex_unlock(&state->lock); + + sentry__threadpool_flush(state->pool); + + sentry__mutex_lock(&state->lock); + state->flushed++; + sentry__cond_wake_all(&state->cond); + sentry__mutex_unlock(&state->lock); + + return 0; +} + +SENTRY_TEST(threadpool_flush_wakes_all) +{ + enum { FLUSH_THREADS = 2 }; + struct threadpool_test_state task_state = { 0 }; + struct threadpool_test_task task = { &task_state, 0 }; + struct threadpool_flush_state flush_state = { 0 }; + sentry_threadid_t flush_threads[FLUSH_THREADS]; + sentry_threadpool_t *pool = sentry__threadpool_new(1); + TEST_ASSERT(!!pool); + + flush_state.pool = pool; + sentry__mutex_init(&task_state.lock); + sentry__cond_init(&task_state.cond); + sentry__mutex_init(&flush_state.lock); + sentry__cond_init(&flush_state.cond); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + TEST_ASSERT( + sentry__threadpool_submit(pool, threadpool_test_exec, NULL, NULL, &task) + == 0); + + sentry__mutex_lock(&task_state.lock); + while (!sentry__atomic_fetch(&task_state.first_started)) { + sentry__cond_wait(&task_state.cond, &task_state.lock); + } + sentry__mutex_unlock(&task_state.lock); + + for (int i = 0; i < FLUSH_THREADS; i++) { + sentry__thread_init(&flush_threads[i]); + TEST_ASSERT(sentry__thread_spawn(&flush_threads[i], + threadpool_flush_thread, &flush_state) + == 0); + } + + sentry__mutex_lock(&flush_state.lock); + while (flush_state.flushing < FLUSH_THREADS) { + sentry__cond_wait(&flush_state.cond, &flush_state.lock); + } + sentry__mutex_unlock(&flush_state.lock); + + // Give both callers time to block in flush before draining the task. + sleep_ms(100); + sentry__mutex_lock(&task_state.lock); + sentry__atomic_store(&task_state.release_first, 1); + sentry__cond_wake(&task_state.cond); + sentry__mutex_unlock(&task_state.lock); + + sentry__mutex_lock(&flush_state.lock); + while (flush_state.flushed < FLUSH_THREADS + && sentry__cond_wait_timeout(&flush_state.cond, &flush_state.lock, 1000) + == 0) { } + const bool woke_all = flush_state.flushed == FLUSH_THREADS; + sentry__mutex_unlock(&flush_state.lock); + + sentry__threadpool_shutdown(pool); + for (int i = 0; i < FLUSH_THREADS; i++) { + sentry__thread_join(flush_threads[i]); + sentry__thread_free(&flush_threads[i]); + } + sentry__threadpool_free(pool); + + TEST_CHECK(woke_all); +#ifndef SENTRY_PLATFORM_WINDOWS + pthread_cond_destroy(&flush_state.cond); + pthread_cond_destroy(&task_state.cond); +#endif + sentry__mutex_free(&flush_state.lock); + sentry__mutex_free(&task_state.lock); +} + SENTRY_TEST(threadpool_ordered_parallel) { struct threadpool_test_state state = { 0 }; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 2fdc5eeb54..215bdc8083 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -388,6 +388,7 @@ XX(symbolizer) XX(task_queue) XX(thread_without_name_still_valid) XX(threadpool_commit_reentry) +XX(threadpool_flush_wakes_all) XX(threadpool_guard) XX(threadpool_invalid_args) XX(threadpool_no_completion_callback_cleans_up) From a20246dbca0d167dcb2452cf7a397fee8cdd3f0f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 11 Aug 2026 15:20:59 +0200 Subject: [PATCH 17/20] monotonic deadline --- tests/unit/test_sync.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index dbee926cdf..61bf1ac5bc 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -714,9 +714,15 @@ SENTRY_TEST(threadpool_flush_wakes_all) sentry__mutex_unlock(&task_state.lock); sentry__mutex_lock(&flush_state.lock); - while (flush_state.flushed < FLUSH_THREADS - && sentry__cond_wait_timeout(&flush_state.cond, &flush_state.lock, 1000) - == 0) { } + const uint64_t started = sentry__monotonic_time(); + while (flush_state.flushed < FLUSH_THREADS) { + const uint64_t elapsed = sentry__monotonic_time() - started; + if (elapsed >= 1000) { + break; + } + sentry__cond_wait_timeout( + &flush_state.cond, &flush_state.lock, 1000 - elapsed); + } const bool woke_all = flush_state.flushed == FLUSH_THREADS; sentry__mutex_unlock(&flush_state.lock); From 96890c2b423c8a3e0db56453f9a01faff1032650 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 14:45:41 +0200 Subject: [PATCH 18/20] prevent setname when already running --- src/sentry_sync.c | 8 ++++++-- src/sentry_sync.h | 2 +- tests/unit/test_sync.c | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 8b1902e0cd..a41825b514 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -260,8 +260,12 @@ sentry__threadpool_setname(sentry_threadpool_t *pool, const char *thread_name) if (!pool) { return; } - sentry_free(pool->thread_name); - pool->thread_name = sentry__string_clone(thread_name); + sentry__mutex_lock(&pool->lock); + if (!sentry__atomic_fetch(&pool->running)) { + sentry_free(pool->thread_name); + pool->thread_name = sentry__string_clone(thread_name); + } + sentry__mutex_unlock(&pool->lock); } int diff --git a/src/sentry_sync.h b/src/sentry_sync.h index 46b51c4584..ede77d3875 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -505,7 +505,7 @@ sentry_threadpool_t *sentry__threadpool_new(size_t thread_count); /** * Sets a name for pooled threads. Each thread is named `-`, - * where `index` starts at 0. + * where `index` starts at 0. Does nothing while the pool is running. * * Should be executed before thread pool start. */ diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index 61bf1ac5bc..ee496ff26c 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -1135,6 +1135,10 @@ SENTRY_TEST(threadpool_thread_name) TEST_ASSERT(!!pool); sentry__threadpool_setname(pool, "tp"); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + sentry__threadpool_setname(pool, "ignored"); + sentry__threadpool_shutdown(pool); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); TEST_ASSERT(sentry__threadpool_submit( pool, threadpool_name_exec, NULL, NULL, &state) From 9bcbc6c0fc6198e0c6384ce2d8e30d403dff4f44 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 15:03:01 +0200 Subject: [PATCH 19/20] max_pending allows capping the total buffer at 1000: https://develop.sentry.dev/sdk/telemetry/logs/#buffering --- src/sentry_sync.c | 11 +++- src/sentry_sync.h | 14 +++-- tests/unit/test_sync.c | 123 +++++++++++++++++++++++++++++++++++++---- tests/unit/tests.inc | 1 + 4 files changed, 130 insertions(+), 19 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index a41825b514..b292db9030 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -3,6 +3,7 @@ #include "sentry_core.h" #include "sentry_string.h" #include "sentry_utils.h" +#include #include #include @@ -127,6 +128,7 @@ struct sentry_threadpool_s { sentry_threadpool_task_t *first_task; sentry_threadpool_task_t *last_task; sentry_threadpool_task_t *next_task; + long max_pending; long pending; long index; long running; @@ -230,9 +232,10 @@ threadpool_thread(void *data) } sentry_threadpool_t * -sentry__threadpool_new(size_t thread_count) +sentry__threadpool_new(size_t thread_count, size_t max_pending) { - if (thread_count == 0) { + if (thread_count == 0 || max_pending == 0 + || max_pending > (size_t)LONG_MAX) { return NULL; } sentry_threadpool_t *pool = SENTRY_MAKE(sentry_threadpool_t); @@ -245,6 +248,7 @@ sentry__threadpool_new(size_t thread_count) return NULL; } pool->thread_count = thread_count; + pool->max_pending = (long)max_pending; sentry__mutex_init(&pool->lock); sentry__cond_init(&pool->work_signal); sentry__cond_init(&pool->state_signal); @@ -339,7 +343,8 @@ sentry__threadpool_submit(sentry_threadpool_t *pool, task->task_data = task_data; sentry__mutex_lock(&pool->lock); - if (!sentry__atomic_fetch(&pool->running) || pool->stopping) { + if (!sentry__atomic_fetch(&pool->running) || pool->stopping + || sentry__atomic_fetch(&pool->pending) >= pool->max_pending) { sentry__mutex_unlock(&pool->lock); threadpool_task_free(task); return 1; diff --git a/src/sentry_sync.h b/src/sentry_sync.h index ede77d3875..3e28a28360 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -498,10 +498,15 @@ typedef struct sentry_threadpool_s sentry_threadpool_t; typedef void (*sentry_task_exec_func_t)(void *task_data, void *state); /** - * Creates a thread pool configured with `thread_count` threads. Tasks execute - * in parallel, while completion callbacks run in submission order. + * Creates a thread pool configured with `thread_count` threads and at most + * `max_pending` outstanding tasks. Tasks execute in parallel, while completion + * callbacks run in submission order. + * + * `max_pending` includes tasks currently executing and tasks awaiting ordered + * completion, and must be greater than zero. */ -sentry_threadpool_t *sentry__threadpool_new(size_t thread_count); +sentry_threadpool_t *sentry__threadpool_new( + size_t thread_count, size_t max_pending); /** * Sets a name for pooled threads. Each thread is named `-`, @@ -530,7 +535,8 @@ int sentry__threadpool_start(sentry_threadpool_t *pool); * `cleanup_func` is called immediately when provided. * * Returns 0 if the task was accepted, or a non-zero value if the arguments are - * invalid, the pool is not running or is stopping, or allocation fails. + * invalid, the pool is not running or is stopping, its pending-task limit has + * been reached, or allocation fails. */ int sentry__threadpool_submit(sentry_threadpool_t *pool, void (*exec_func)(void *task_data), void (*complete_func)(void *task_data), diff --git a/tests/unit/test_sync.c b/tests/unit/test_sync.c index ee496ff26c..d73df51064 100644 --- a/tests/unit/test_sync.c +++ b/tests/unit/test_sync.c @@ -3,6 +3,7 @@ #include "sentry_sync.h" #include "sentry_testsupport.h" #include "sentry_utils.h" +#include struct task_state { int executed; @@ -674,7 +675,7 @@ SENTRY_TEST(threadpool_flush_wakes_all) struct threadpool_test_task task = { &task_state, 0 }; struct threadpool_flush_state flush_state = { 0 }; sentry_threadid_t flush_threads[FLUSH_THREADS]; - sentry_threadpool_t *pool = sentry__threadpool_new(1); + sentry_threadpool_t *pool = sentry__threadpool_new(1, 1); TEST_ASSERT(!!pool); flush_state.pool = pool; @@ -749,7 +750,7 @@ SENTRY_TEST(threadpool_ordered_parallel) { &state, 0 }, { &state, 1 }, }; - sentry_threadpool_t *pool = sentry__threadpool_new(2); + sentry_threadpool_t *pool = sentry__threadpool_new(2, 2); TEST_ASSERT(!!pool); sentry__mutex_init(&state.lock); sentry__cond_init(&state.cond); @@ -807,7 +808,7 @@ threadpool_restart_cleanup(void *data) SENTRY_TEST(threadpool_restart) { struct threadpool_restart_state state = { 0 }; - sentry_threadpool_t *pool = sentry__threadpool_new(1); + sentry_threadpool_t *pool = sentry__threadpool_new(1, 1); TEST_ASSERT(!!pool); for (int i = 0; i < 2; i++) { @@ -854,6 +855,100 @@ threadpool_count_cleanup(void *data) sentry__atomic_fetch_and_add(&state->cleaned_up, 1); } +struct threadpool_max_pending_state { + sentry_mutex_t lock; + sentry_cond_t cond; + bool started; + bool release; + volatile long executed; + volatile long completed; + volatile long cleaned_up; +}; + +static void +threadpool_max_pending_exec(void *data) +{ + struct threadpool_max_pending_state *state = data; + sentry__mutex_lock(&state->lock); + state->started = true; + sentry__cond_wake(&state->cond); + while (!state->release) { + sentry__cond_wait(&state->cond, &state->lock); + } + sentry__mutex_unlock(&state->lock); + sentry__atomic_fetch_and_add(&state->executed, 1); +} + +static void +threadpool_max_pending_complete(void *data) +{ + struct threadpool_max_pending_state *state = data; + sentry__atomic_fetch_and_add(&state->completed, 1); +} + +static void +threadpool_max_pending_cleanup(void *data) +{ + struct threadpool_max_pending_state *state = data; + sentry__atomic_fetch_and_add(&state->cleaned_up, 1); +} + +SENTRY_TEST(threadpool_max_pending) +{ + struct threadpool_max_pending_state state = { 0 }; + sentry_threadpool_t *pool = sentry__threadpool_new(1, 2); + TEST_ASSERT(!!pool); + sentry__mutex_init(&state.lock); + sentry__cond_init(&state.cond); + TEST_ASSERT(sentry__threadpool_start(pool) == 0); + + TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_max_pending_exec, + threadpool_max_pending_complete, + threadpool_max_pending_cleanup, &state) + == 0); + sentry__mutex_lock(&state.lock); + while (!state.started) { + sentry__cond_wait(&state.cond, &state.lock); + } + sentry__mutex_unlock(&state.lock); + + TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_max_pending_exec, + threadpool_max_pending_complete, + threadpool_max_pending_cleanup, &state) + == 0); + TEST_CHECK(sentry__threadpool_submit(pool, threadpool_max_pending_exec, + threadpool_max_pending_complete, + threadpool_max_pending_cleanup, &state) + != 0); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleaned_up), 1); + + sentry__mutex_lock(&state.lock); + state.release = true; + sentry__cond_wake(&state.cond); + sentry__mutex_unlock(&state.lock); + sentry__threadpool_flush(pool); + + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.executed), 2); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.completed), 2); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleaned_up), 3); + + TEST_ASSERT(sentry__threadpool_submit(pool, threadpool_max_pending_exec, + threadpool_max_pending_complete, + threadpool_max_pending_cleanup, &state) + == 0); + sentry__threadpool_flush(pool); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.executed), 3); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.completed), 3); + TEST_CHECK_INT_EQUAL(sentry__atomic_fetch(&state.cleaned_up), 4); + + sentry__threadpool_shutdown(pool); + sentry__threadpool_free(pool); +#ifndef SENTRY_PLATFORM_WINDOWS + pthread_cond_destroy(&state.cond); +#endif + sentry__mutex_free(&state.lock); +} + struct threadpool_reentry_state { sentry_mutex_t lock; sentry_cond_t cond; @@ -938,7 +1033,7 @@ SENTRY_TEST(threadpool_commit_reentry) { &state, 0 }, { &state, 1 }, }; - sentry_threadpool_t *pool = sentry__threadpool_new(2); + sentry_threadpool_t *pool = sentry__threadpool_new(2, 2); TEST_ASSERT(!!pool); sentry__mutex_init(&state.lock); sentry__cond_init(&state.cond); @@ -1006,7 +1101,7 @@ callback_free(void *data) SENTRY_TEST(threadpool_guard) { struct callback_pool_state state = { 0 }; - sentry_threadpool_t *pool = sentry__threadpool_new(1); + sentry_threadpool_t *pool = sentry__threadpool_new(1, 3); TEST_ASSERT(!!pool); state.pool = pool; TEST_ASSERT(sentry__threadpool_start(pool) == 0); @@ -1034,10 +1129,14 @@ SENTRY_TEST(threadpool_invalid_args) { struct threadpool_count_state state = { 0 }; - TEST_CHECK_PTR_EQUAL(sentry__threadpool_new(0), NULL); + TEST_CHECK_PTR_EQUAL(sentry__threadpool_new(0, 1), NULL); + TEST_CHECK_PTR_EQUAL(sentry__threadpool_new(1, 0), NULL); +#if SIZE_MAX > LONG_MAX + TEST_CHECK_PTR_EQUAL(sentry__threadpool_new(1, (size_t)LONG_MAX + 1), NULL); +#endif if (sizeof(sentry_threadid_t) > 1) { TEST_CHECK_PTR_EQUAL( - sentry__threadpool_new(SIZE_MAX / sizeof(sentry_threadid_t) + 1), + sentry__threadpool_new(SIZE_MAX / sizeof(sentry_threadid_t) + 1, 1), NULL); } @@ -1051,7 +1150,7 @@ SENTRY_TEST(threadpool_invalid_args) sentry__threadpool_shutdown(NULL); sentry__threadpool_free(NULL); - sentry_threadpool_t *pool = sentry__threadpool_new(1); + sentry_threadpool_t *pool = sentry__threadpool_new(1, 1); TEST_ASSERT(!!pool); TEST_ASSERT(sentry__threadpool_start(pool) == 0); TEST_CHECK_INT_EQUAL(sentry__threadpool_start(pool), 0); @@ -1070,7 +1169,7 @@ SENTRY_TEST(threadpool_invalid_args) SENTRY_TEST(threadpool_no_completion_callback_cleans_up) { struct threadpool_count_state state = { 0 }; - sentry_threadpool_t *pool = sentry__threadpool_new(1); + sentry_threadpool_t *pool = sentry__threadpool_new(1, 1); TEST_ASSERT(!!pool); TEST_ASSERT(sentry__threadpool_start(pool) == 0); @@ -1091,7 +1190,7 @@ SENTRY_TEST(threadpool_shutdown_drains) { enum { TASKS = 16 }; struct threadpool_count_state state = { 0 }; - sentry_threadpool_t *pool = sentry__threadpool_new(2); + sentry_threadpool_t *pool = sentry__threadpool_new(2, TASKS); TEST_ASSERT(!!pool); TEST_ASSERT(sentry__threadpool_start(pool) == 0); @@ -1131,7 +1230,7 @@ SENTRY_TEST(threadpool_thread_name) SKIP_TEST(); #else struct threadpool_name_state state = { 0 }; - sentry_threadpool_t *pool = sentry__threadpool_new(1); + sentry_threadpool_t *pool = sentry__threadpool_new(1, 1); TEST_ASSERT(!!pool); sentry__threadpool_setname(pool, "tp"); @@ -1157,7 +1256,7 @@ SENTRY_TEST(threadpool_rejected_submit_cleans_up) { struct threadpool_test_state state = { 0 }; struct threadpool_test_task task = { &state, 0 }; - sentry_threadpool_t *pool = sentry__threadpool_new(1); + sentry_threadpool_t *pool = sentry__threadpool_new(1, 1); TEST_ASSERT(!!pool); TEST_CHECK(sentry__threadpool_submit(pool, threadpool_test_exec, diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 215bdc8083..0f8603e536 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -391,6 +391,7 @@ XX(threadpool_commit_reentry) XX(threadpool_flush_wakes_all) XX(threadpool_guard) XX(threadpool_invalid_args) +XX(threadpool_max_pending) XX(threadpool_no_completion_callback_cleans_up) XX(threadpool_ordered_parallel) XX(threadpool_rejected_submit_cleans_up) From 7f9c7b598282fd1a419a394c34336050181b94e2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 12 Aug 2026 15:15:35 +0200 Subject: [PATCH 20/20] remove spurious wake --- src/sentry_sync.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index b292db9030..4dd53680b7 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -407,7 +407,6 @@ sentry__threadpool_shutdown(sentry_threadpool_t *pool) pool->stopping = true; const size_t started_threads = pool->started_threads; threadpool_wake_all(pool); - sentry__cond_wake(&pool->state_signal); sentry__mutex_unlock(&pool->lock); for (size_t i = 0; i < started_threads; i++) {