From 663f3bb511b3609ce6d6c015e71067bba4950c65 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 11:05:27 +0100 Subject: [PATCH 001/103] feat(transport): add HTTP retry logic with exponential backoff Co-Authored-By: Claude Opus 4.6 --- include/sentry.h | 9 + src/sentry_options.c | 12 + src/sentry_options.h | 1 + src/transports/sentry_http_transport.c | 291 +++++++++++++++++++++++-- 4 files changed, 297 insertions(+), 16 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 25b472aaa..23f202d7c 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2258,6 +2258,15 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs( SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( const sentry_options_t *opts); +/** + * Sets the maximum number of HTTP retry attempts for transient network errors. + * Set to 0 to disable retries (default). + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retries( + sentry_options_t *opts, int http_retries); +SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retries( + const sentry_options_t *opts); + /** * Enables or disables custom attributes parsing for structured logging. * diff --git a/src/sentry_options.c b/src/sentry_options.c index d9b4e05d2..d8376f0d5 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -876,6 +876,18 @@ sentry_options_set_handler_strategy( #endif // SENTRY_PLATFORM_LINUX +void +sentry_options_set_http_retries(sentry_options_t *opts, int http_retries) +{ + opts->http_retries = http_retries; +} + +int +sentry_options_get_http_retries(const sentry_options_t *opts) +{ + return opts->http_retries; +} + void sentry_options_set_propagate_traceparent( sentry_options_t *opts, int propagate_traceparent) diff --git a/src/sentry_options.h b/src/sentry_options.h index 50aeb057b..1c9a96f45 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -72,6 +72,7 @@ struct sentry_options_s { void *traces_sampler_data; size_t max_spans; bool enable_logs; + int http_retries; // takes the first varg as a `sentry_value_t` object containing attributes // if no custom attributes are to be passed, use `sentry_value_new_object()` bool logs_with_attributes; diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 24b1ba566..71bb01c2e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -3,14 +3,18 @@ #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" +#include "sentry_path.h" #include "sentry_ratelimiter.h" #include "sentry_string.h" #include "sentry_transport.h" +#include "sentry_utils.h" +#include "sentry_uuid.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" #endif +#include #include #define ENVELOPE_MIME "application/x-sentry-envelope" @@ -20,6 +24,9 @@ # define MAX_HTTP_HEADERS 3 #endif +#define RETRY_BACKOFF_BASE_MS 900000 +#define RETRY_STARTUP_DELAY_MS 100 + typedef struct { sentry_dsn_t *dsn; char *user_agent; @@ -29,6 +36,10 @@ typedef struct { int (*start_client)(void *, const sentry_options_t *); sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); + sentry_bgworker_t *bgworker; + sentry_path_t *retry_dir; + sentry_path_t *cache_dir; + int http_retries; } http_transport_state_t; #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -182,6 +193,242 @@ sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) sentry_free(req); } +static void retry_process_task(void *_check_backoff, void *_state); + +static bool +retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, + const char **uuid_out) +{ + char *end; + uint64_t ts = strtoull(filename, &end, 10); + if (*end != '-') { + return false; + } + + const char *count_str = end + 1; + long count = strtol(count_str, &end, 10); + if (*end != '-') { + return false; + } + + const char *uuid_start = end + 1; + size_t tail_len = strlen(uuid_start); + // 36 chars UUID + ".envelope" + if (tail_len != 36 + 9 || strcmp(uuid_start + 36, ".envelope") != 0) { + return false; + } + + *ts_out = ts; + *count_out = (int)count; + *uuid_out = uuid_start; + return true; +} + +static uint64_t +retry_backoff_ms(int count) +{ + int shift = count < 3 ? count : 3; + return (uint64_t)RETRY_BACKOFF_BASE_MS << shift; +} + +static int +compare_retry_paths(const void *a, const void *b) +{ + const sentry_path_t *const *pa = a; + const sentry_path_t *const *pb = b; + return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); +} + +static int +http_send_request( + http_transport_state_t *state, sentry_prepared_http_request_t *req) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + if (!state->send_func(state->client, req, &resp)) { + sentry_free(resp.retry_after); + sentry_free(resp.x_sentry_rate_limits); + return -1; + } + + if (resp.x_sentry_rate_limits) { + sentry__rate_limiter_update_from_header( + state->ratelimiter, resp.x_sentry_rate_limits); + } else if (resp.retry_after) { + sentry__rate_limiter_update_from_http_retry_after( + state->ratelimiter, resp.retry_after); + } else if (resp.status_code == 429) { + sentry__rate_limiter_update_from_429(state->ratelimiter); + } + + sentry_free(resp.retry_after); + sentry_free(resp.x_sentry_rate_limits); + return resp.status_code; +} + +static void +retry_write_envelope( + http_transport_state_t *state, const sentry_envelope_t *envelope) +{ + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + if (sentry_uuid_is_nil(&event_id)) { + return; + } + + uint64_t now = sentry__monotonic_time(); + char uuid_str[37]; + sentry__internal_uuid_as_string(&event_id, uuid_str); + + char filename[128]; + snprintf(filename, sizeof(filename), "%llu-00-%s.envelope", + (unsigned long long)now, uuid_str); + + sentry_path_t *path = sentry__path_join_str(state->retry_dir, filename); + if (path) { + int rv = sentry_envelope_write_to_path(envelope, path); + (void)rv; + sentry__path_free(path); + } + + sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, NULL, + (void *)(intptr_t)1, RETRY_BACKOFF_BASE_MS); +} + +static void +retry_process_task(void *_check_backoff, void *_state) +{ + int check_backoff = (int)(intptr_t)_check_backoff; + http_transport_state_t *state = _state; + + sentry_pathiter_t *piter = sentry__path_iter_directory(state->retry_dir); + if (!piter) { + return; + } + + size_t path_count = 0; + size_t path_cap = 16; + sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (!paths) { + sentry__pathiter_free(piter); + return; + } + + const sentry_path_t *p; + while ((p = sentry__pathiter_next(piter)) != NULL) { + const char *fname = sentry__path_filename(p); + uint64_t ts; + int count; + const char *uuid_start; + if (!retry_parse_filename(fname, &ts, &count, &uuid_start)) { + continue; + } + if (path_count == path_cap) { + path_cap *= 2; + sentry_path_t **tmp + = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (!tmp) { + break; + } + memcpy(tmp, paths, path_count * sizeof(sentry_path_t *)); + sentry_free(paths); + paths = tmp; + } + paths[path_count++] = sentry__path_clone(p); + } + sentry__pathiter_free(piter); + + if (path_count > 1) { + qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); + } + + uint64_t now = sentry__monotonic_time(); + bool files_remain = false; + + for (size_t i = 0; i < path_count; i++) { + const char *fname = sentry__path_filename(paths[i]); + uint64_t ts; + int count; + const char *uuid_start; + retry_parse_filename(fname, &ts, &count, &uuid_start); + + if (check_backoff && (now - ts) < retry_backoff_ms(count)) { + files_remain = true; + continue; + } + + sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); + if (!envelope) { + sentry__path_remove(paths[i]); + continue; + } + + sentry_prepared_http_request_t *req = sentry__prepare_http_request( + envelope, state->dsn, state->ratelimiter, state->user_agent); + int status_code; + if (!req) { + status_code = 0; + } else { + status_code = http_send_request(state, req); + sentry__prepared_http_request_free(req); + } + sentry_envelope_free(envelope); + + if (status_code < 0) { + if (count + 1 >= state->http_retries) { + if (state->cache_dir) { + sentry_path_t *dst + = sentry__path_join_str(state->cache_dir, fname); + if (dst) { + sentry__path_rename(paths[i], dst); + sentry__path_free(dst); + } else { + sentry__path_remove(paths[i]); + } + } else { + sentry__path_remove(paths[i]); + } + } else { + char new_filename[128]; + snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", + (unsigned long long)now, count + 1, uuid_start); + sentry_path_t *new_path + = sentry__path_join_str(state->retry_dir, new_filename); + if (new_path) { + sentry__path_rename(paths[i], new_path); + sentry__path_free(new_path); + } + files_remain = true; + } + } else if (status_code >= 200 && status_code < 300) { + if (state->cache_dir) { + sentry_path_t *dst + = sentry__path_join_str(state->cache_dir, fname); + if (dst) { + sentry__path_rename(paths[i], dst); + sentry__path_free(dst); + } else { + sentry__path_remove(paths[i]); + } + } else { + sentry__path_remove(paths[i]); + } + } else { + sentry__path_remove(paths[i]); + } + } + + for (size_t i = 0; i < path_count; i++) { + sentry__path_free(paths[i]); + } + sentry_free(paths); + + if (files_remain) { + sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, + NULL, (void *)(intptr_t)1, RETRY_BACKOFF_BASE_MS); + } +} + static void http_transport_state_free(void *_state) { @@ -192,6 +439,8 @@ http_transport_state_free(void *_state) sentry__dsn_decref(state->dsn); sentry_free(state->user_agent); sentry__rate_limiter_free(state->ratelimiter); + sentry__path_free(state->retry_dir); + sentry__path_free(state->cache_dir); sentry_free(state); } @@ -207,23 +456,12 @@ http_send_task(void *_envelope, void *_state) return; } - sentry_http_response_t resp; - memset(&resp, 0, sizeof(resp)); + int status_code = http_send_request(state, req); + sentry__prepared_http_request_free(req); - if (state->send_func(state->client, req, &resp)) { - if (resp.x_sentry_rate_limits) { - sentry__rate_limiter_update_from_header( - state->ratelimiter, resp.x_sentry_rate_limits); - } else if (resp.retry_after) { - sentry__rate_limiter_update_from_http_retry_after( - state->ratelimiter, resp.retry_after); - } else if (resp.status_code == 429) { - sentry__rate_limiter_update_from_429(state->ratelimiter); - } + if (status_code < 0 && state->retry_dir) { + retry_write_envelope(state, envelope); } - sentry_free(resp.retry_after); - sentry_free(resp.x_sentry_rate_limits); - sentry__prepared_http_request_free(req); } static int @@ -244,7 +482,27 @@ http_transport_start(const sentry_options_t *options, void *transport_state) } } - return sentry__bgworker_start(bgworker); + int rv = sentry__bgworker_start(bgworker); + if (rv != 0) { + return rv; + } + + if (options->http_retries > 0) { + state->http_retries = options->http_retries; + state->retry_dir + = sentry__path_join_str(options->database_path, "retry"); + if (state->retry_dir) { + sentry__path_create_dir_all(state->retry_dir); + } + if (options->cache_keep) { + state->cache_dir + = sentry__path_join_str(options->database_path, "cache"); + } + sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, + (void *)(intptr_t)0, RETRY_STARTUP_DELAY_MS); + } + + return 0; } static int @@ -316,6 +574,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) http_transport_state_free(state); return NULL; } + state->bgworker = bgworker; sentry_transport_t *transport = sentry_transport_new(http_transport_send_envelope); From 52a273e4b32c3e489dba21194e90f32721e2bf7f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:38:49 +0100 Subject: [PATCH 002/103] fix(retry): filter startup scan by timestamp, create cache dir The deferred startup retry scan (100ms delay) could pick up files written by the current session. Filter by startup_time so only previous-session files are processed. Also ensure the cache directory exists when cache_keep is enabled, since sentry__process_old_runs only creates it conditionally. Co-Authored-By: Claude Opus 4.6 --- examples/example.c | 3 + src/CMakeLists.txt | 2 + src/sentry_retry.c | 263 +++++++++++++++++++ src/sentry_retry.h | 36 +++ src/transports/sentry_http_transport.c | 231 +++-------------- tests/test_integration_http.py | 293 +++++++++++++++++++++ tests/unit/CMakeLists.txt | 1 + tests/unit/test_retry.c | 341 +++++++++++++++++++++++++ tests/unit/tests.inc | 6 + 9 files changed, 982 insertions(+), 194 deletions(-) create mode 100644 src/sentry_retry.c create mode 100644 src/sentry_retry.h create mode 100644 tests/unit/test_retry.c diff --git a/examples/example.c b/examples/example.c index 8fc7db7b3..5c2a0a85b 100644 --- a/examples/example.c +++ b/examples/example.c @@ -659,6 +659,9 @@ main(int argc, char **argv) sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); // 5 days sentry_options_set_cache_max_items(options, 5); } + if (has_arg(argc, argv, "http-retry")) { + sentry_options_set_http_retries(options, 5); + } if (has_arg(argc, argv, "enable-metrics")) { sentry_options_set_enable_metrics(options, true); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 218a9b012..326fd3ceb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,6 +34,8 @@ sentry_target_sources_cwd(sentry sentry_process.h sentry_ratelimiter.c sentry_ratelimiter.h + sentry_retry.c + sentry_retry.h sentry_ringbuffer.c sentry_ringbuffer.h sentry_sampling_context.h diff --git a/src/sentry_retry.c b/src/sentry_retry.c new file mode 100644 index 000000000..fbb8dba89 --- /dev/null +++ b/src/sentry_retry.c @@ -0,0 +1,263 @@ +#include "sentry_retry.h" +#include "sentry_alloc.h" +#include "sentry_envelope.h" +#include "sentry_utils.h" + +#include +#include + +struct sentry_retry_s { + sentry_path_t *retry_dir; + sentry_path_t *cache_dir; + int max_retries; + uint64_t startup_time; +}; + +sentry_retry_t * +sentry__retry_new( + sentry_path_t *retry_dir, sentry_path_t *cache_dir, int max_retries) +{ + sentry_retry_t *retry = SENTRY_MAKE(sentry_retry_t); + if (!retry) { + return NULL; + } + retry->retry_dir = sentry__path_clone(retry_dir); + retry->cache_dir = cache_dir ? sentry__path_clone(cache_dir) : NULL; + retry->max_retries = max_retries; + return retry; +} + +void +sentry__retry_free(sentry_retry_t *retry) +{ + if (!retry) { + return; + } + sentry__path_free(retry->retry_dir); + sentry__path_free(retry->cache_dir); + sentry_free(retry); +} + +void +sentry__retry_set_startup_time(sentry_retry_t *retry, uint64_t startup_time) +{ + retry->startup_time = startup_time; +} + +bool +sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, + int *count_out, const char **uuid_out) +{ + char *end; + uint64_t ts = strtoull(filename, &end, 10); + if (*end != '-') { + return false; + } + + const char *count_str = end + 1; + long count = strtol(count_str, &end, 10); + if (*end != '-') { + return false; + } + + const char *uuid_start = end + 1; + size_t tail_len = strlen(uuid_start); + // 36 chars UUID (with dashes) + ".envelope" + if (tail_len != 36 + 9 || strcmp(uuid_start + 36, ".envelope") != 0) { + return false; + } + + *ts_out = ts; + *count_out = (int)count; + *uuid_out = uuid_start; + return true; +} + +uint64_t +sentry__retry_backoff_ms(int count) +{ + int shift = count < 3 ? count : 3; + return (uint64_t)SENTRY_RETRY_BACKOFF_BASE_MS << shift; +} + +static int +compare_retry_paths(const void *a, const void *b) +{ + const sentry_path_t *const *pa = a; + const sentry_path_t *const *pb = b; + return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); +} + +void +sentry__retry_write_envelope( + sentry_retry_t *retry, const sentry_envelope_t *envelope) +{ + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + if (sentry_uuid_is_nil(&event_id)) { + return; + } + + uint64_t now = sentry__monotonic_time(); + char uuid_str[37]; + sentry_uuid_as_string(&event_id, uuid_str); + + char filename[128]; + snprintf(filename, sizeof(filename), "%llu-00-%s.envelope", + (unsigned long long)now, uuid_str); + + sentry_path_t *path = sentry__path_join_str(retry->retry_dir, filename); + if (path) { + (void)sentry_envelope_write_to_path(envelope, path); + sentry__path_free(path); + } +} + +sentry_path_t ** +sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) +{ + *count_out = 0; + + sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); + if (!piter) { + return NULL; + } + + size_t path_cap = 16; + sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (!paths) { + sentry__pathiter_free(piter); + return NULL; + } + + size_t path_count = 0; + uint64_t now = startup ? 0 : sentry__monotonic_time(); + + const sentry_path_t *p; + while ((p = sentry__pathiter_next(piter)) != NULL) { + const char *fname = sentry__path_filename(p); + uint64_t ts; + int count; + const char *uuid_start; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + continue; + } + if (startup) { + if (retry->startup_time > 0 && ts >= retry->startup_time) { + continue; + } + } else if ((now - ts) < sentry__retry_backoff_ms(count)) { + continue; + } + if (path_count == path_cap) { + path_cap *= 2; + sentry_path_t **tmp + = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (!tmp) { + break; + } + memcpy(tmp, paths, path_count * sizeof(sentry_path_t *)); + sentry_free(paths); + paths = tmp; + } + paths[path_count++] = sentry__path_clone(p); + } + sentry__pathiter_free(piter); + + if (path_count > 1) { + qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); + } + + *count_out = path_count; + return paths; +} + +void +sentry__retry_free_paths(sentry_path_t **paths, size_t count) +{ + if (!paths) { + return; + } + for (size_t i = 0; i < count; i++) { + sentry__path_free(paths[i]); + } + sentry_free(paths); +} + +void +sentry__retry_handle_result( + sentry_retry_t *retry, const sentry_path_t *path, int status_code) +{ + const char *fname = sentry__path_filename(path); + uint64_t ts; + int count; + const char *uuid_start; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + sentry__path_remove(path); + return; + } + + if (status_code < 0) { + if (count + 1 >= retry->max_retries) { + if (retry->cache_dir) { + sentry_path_t *dst + = sentry__path_join_str(retry->cache_dir, fname); + if (dst) { + sentry__path_rename(path, dst); + sentry__path_free(dst); + } else { + sentry__path_remove(path); + } + } else { + sentry__path_remove(path); + } + } else { + uint64_t now = sentry__monotonic_time(); + char new_filename[128]; + snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", + (unsigned long long)now, count + 1, uuid_start); + sentry_path_t *new_path + = sentry__path_join_str(retry->retry_dir, new_filename); + if (new_path) { + sentry__path_rename(path, new_path); + sentry__path_free(new_path); + } + } + } else if (status_code >= 200 && status_code < 300) { + if (retry->cache_dir) { + sentry_path_t *dst = sentry__path_join_str(retry->cache_dir, fname); + if (dst) { + sentry__path_rename(path, dst); + sentry__path_free(dst); + } else { + sentry__path_remove(path); + } + } else { + sentry__path_remove(path); + } + } else { + sentry__path_remove(path); + } +} + +bool +sentry__retry_has_files(const sentry_retry_t *retry) +{ + sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); + if (!piter) { + return false; + } + + const sentry_path_t *p; + while ((p = sentry__pathiter_next(piter)) != NULL) { + const char *fname = sentry__path_filename(p); + uint64_t ts; + int count; + const char *uuid_start; + if (sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + sentry__pathiter_free(piter); + return true; + } + } + sentry__pathiter_free(piter); + return false; +} diff --git a/src/sentry_retry.h b/src/sentry_retry.h new file mode 100644 index 000000000..07a11d93a --- /dev/null +++ b/src/sentry_retry.h @@ -0,0 +1,36 @@ +#ifndef SENTRY_RETRY_H_INCLUDED +#define SENTRY_RETRY_H_INCLUDED + +#include "sentry_boot.h" +#include "sentry_path.h" + +#define SENTRY_RETRY_BACKOFF_BASE_MS 900000 +#define SENTRY_RETRY_STARTUP_DELAY_MS 100 + +typedef struct sentry_retry_s sentry_retry_t; + +sentry_retry_t *sentry__retry_new( + sentry_path_t *retry_dir, sentry_path_t *cache_dir, int max_retries); +void sentry__retry_free(sentry_retry_t *retry); + +void sentry__retry_write_envelope( + sentry_retry_t *retry, const sentry_envelope_t *envelope); + +void sentry__retry_set_startup_time( + sentry_retry_t *retry, uint64_t startup_time); + +sentry_path_t **sentry__retry_scan( + sentry_retry_t *retry, bool startup, size_t *count_out); +void sentry__retry_free_paths(sentry_path_t **paths, size_t count); + +void sentry__retry_handle_result( + sentry_retry_t *retry, const sentry_path_t *path, int status_code); + +bool sentry__retry_has_files(const sentry_retry_t *retry); + +uint64_t sentry__retry_backoff_ms(int count); + +bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, + int *count_out, const char **uuid_out); + +#endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 71bb01c2e..3d5cee33c 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -5,10 +5,10 @@ #include "sentry_options.h" #include "sentry_path.h" #include "sentry_ratelimiter.h" +#include "sentry_retry.h" #include "sentry_string.h" #include "sentry_transport.h" #include "sentry_utils.h" -#include "sentry_uuid.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" @@ -24,9 +24,6 @@ # define MAX_HTTP_HEADERS 3 #endif -#define RETRY_BACKOFF_BASE_MS 900000 -#define RETRY_STARTUP_DELAY_MS 100 - typedef struct { sentry_dsn_t *dsn; char *user_agent; @@ -37,9 +34,7 @@ typedef struct { sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); sentry_bgworker_t *bgworker; - sentry_path_t *retry_dir; - sentry_path_t *cache_dir; - int http_retries; + sentry_retry_t *retry; } http_transport_state_t; #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -195,50 +190,6 @@ sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) static void retry_process_task(void *_check_backoff, void *_state); -static bool -retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, - const char **uuid_out) -{ - char *end; - uint64_t ts = strtoull(filename, &end, 10); - if (*end != '-') { - return false; - } - - const char *count_str = end + 1; - long count = strtol(count_str, &end, 10); - if (*end != '-') { - return false; - } - - const char *uuid_start = end + 1; - size_t tail_len = strlen(uuid_start); - // 36 chars UUID + ".envelope" - if (tail_len != 36 + 9 || strcmp(uuid_start + 36, ".envelope") != 0) { - return false; - } - - *ts_out = ts; - *count_out = (int)count; - *uuid_out = uuid_start; - return true; -} - -static uint64_t -retry_backoff_ms(int count) -{ - int shift = count < 3 ? count : 3; - return (uint64_t)RETRY_BACKOFF_BASE_MS << shift; -} - -static int -compare_retry_paths(const void *a, const void *b) -{ - const sentry_path_t *const *pa = a; - const sentry_path_t *const *pb = b; - return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); -} - static int http_send_request( http_transport_state_t *state, sentry_prepared_http_request_t *req) @@ -268,95 +219,19 @@ http_send_request( } static void -retry_write_envelope( - http_transport_state_t *state, const sentry_envelope_t *envelope) -{ - sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); - if (sentry_uuid_is_nil(&event_id)) { - return; - } - - uint64_t now = sentry__monotonic_time(); - char uuid_str[37]; - sentry__internal_uuid_as_string(&event_id, uuid_str); - - char filename[128]; - snprintf(filename, sizeof(filename), "%llu-00-%s.envelope", - (unsigned long long)now, uuid_str); - - sentry_path_t *path = sentry__path_join_str(state->retry_dir, filename); - if (path) { - int rv = sentry_envelope_write_to_path(envelope, path); - (void)rv; - sentry__path_free(path); - } - - sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, NULL, - (void *)(intptr_t)1, RETRY_BACKOFF_BASE_MS); -} - -static void -retry_process_task(void *_check_backoff, void *_state) +retry_process_task(void *_startup, void *_state) { - int check_backoff = (int)(intptr_t)_check_backoff; + int startup = (int)(intptr_t)_startup; http_transport_state_t *state = _state; - sentry_pathiter_t *piter = sentry__path_iter_directory(state->retry_dir); - if (!piter) { - return; - } - - size_t path_count = 0; - size_t path_cap = 16; - sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); - if (!paths) { - sentry__pathiter_free(piter); + if (!state->retry) { return; } - const sentry_path_t *p; - while ((p = sentry__pathiter_next(piter)) != NULL) { - const char *fname = sentry__path_filename(p); - uint64_t ts; - int count; - const char *uuid_start; - if (!retry_parse_filename(fname, &ts, &count, &uuid_start)) { - continue; - } - if (path_count == path_cap) { - path_cap *= 2; - sentry_path_t **tmp - = sentry_malloc(path_cap * sizeof(sentry_path_t *)); - if (!tmp) { - break; - } - memcpy(tmp, paths, path_count * sizeof(sentry_path_t *)); - sentry_free(paths); - paths = tmp; - } - paths[path_count++] = sentry__path_clone(p); - } - sentry__pathiter_free(piter); - - if (path_count > 1) { - qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); - } - - uint64_t now = sentry__monotonic_time(); - bool files_remain = false; - - for (size_t i = 0; i < path_count; i++) { - const char *fname = sentry__path_filename(paths[i]); - uint64_t ts; - int count; - const char *uuid_start; - retry_parse_filename(fname, &ts, &count, &uuid_start); - - if (check_backoff && (now - ts) < retry_backoff_ms(count)) { - files_remain = true; - continue; - } + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(state->retry, startup, &count); + for (size_t i = 0; i < count; i++) { sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); if (!envelope) { sentry__path_remove(paths[i]); @@ -374,58 +249,14 @@ retry_process_task(void *_check_backoff, void *_state) } sentry_envelope_free(envelope); - if (status_code < 0) { - if (count + 1 >= state->http_retries) { - if (state->cache_dir) { - sentry_path_t *dst - = sentry__path_join_str(state->cache_dir, fname); - if (dst) { - sentry__path_rename(paths[i], dst); - sentry__path_free(dst); - } else { - sentry__path_remove(paths[i]); - } - } else { - sentry__path_remove(paths[i]); - } - } else { - char new_filename[128]; - snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", - (unsigned long long)now, count + 1, uuid_start); - sentry_path_t *new_path - = sentry__path_join_str(state->retry_dir, new_filename); - if (new_path) { - sentry__path_rename(paths[i], new_path); - sentry__path_free(new_path); - } - files_remain = true; - } - } else if (status_code >= 200 && status_code < 300) { - if (state->cache_dir) { - sentry_path_t *dst - = sentry__path_join_str(state->cache_dir, fname); - if (dst) { - sentry__path_rename(paths[i], dst); - sentry__path_free(dst); - } else { - sentry__path_remove(paths[i]); - } - } else { - sentry__path_remove(paths[i]); - } - } else { - sentry__path_remove(paths[i]); - } + sentry__retry_handle_result(state->retry, paths[i], status_code); } - for (size_t i = 0; i < path_count; i++) { - sentry__path_free(paths[i]); - } - sentry_free(paths); + sentry__retry_free_paths(paths, count); - if (files_remain) { + if (sentry__retry_has_files(state->retry)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)1, RETRY_BACKOFF_BASE_MS); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_MS); } } @@ -439,8 +270,7 @@ http_transport_state_free(void *_state) sentry__dsn_decref(state->dsn); sentry_free(state->user_agent); sentry__rate_limiter_free(state->ratelimiter); - sentry__path_free(state->retry_dir); - sentry__path_free(state->cache_dir); + sentry__retry_free(state->retry); sentry_free(state); } @@ -459,8 +289,10 @@ http_send_task(void *_envelope, void *_state) int status_code = http_send_request(state, req); sentry__prepared_http_request_free(req); - if (status_code < 0 && state->retry_dir) { - retry_write_envelope(state, envelope); + if (status_code < 0 && state->retry) { + sentry__retry_write_envelope(state->retry, envelope); + sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, + NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_MS); } } @@ -488,18 +320,29 @@ http_transport_start(const sentry_options_t *options, void *transport_state) } if (options->http_retries > 0) { - state->http_retries = options->http_retries; - state->retry_dir + sentry_path_t *retry_dir = sentry__path_join_str(options->database_path, "retry"); - if (state->retry_dir) { - sentry__path_create_dir_all(state->retry_dir); + if (retry_dir) { + sentry__path_create_dir_all(retry_dir); + sentry_path_t *cache_dir = NULL; + if (options->cache_keep) { + cache_dir + = sentry__path_join_str(options->database_path, "cache"); + if (cache_dir) { + sentry__path_create_dir_all(cache_dir); + } + } + state->retry = sentry__retry_new( + retry_dir, cache_dir, options->http_retries); + sentry__path_free(cache_dir); + sentry__path_free(retry_dir); } - if (options->cache_keep) { - state->cache_dir - = sentry__path_join_str(options->database_path, "cache"); + if (state->retry) { + sentry__retry_set_startup_time( + state->retry, sentry__monotonic_time()); + sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, + (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); } - sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, - (void *)(intptr_t)0, RETRY_STARTUP_DELAY_MS); } return 0; diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index ec008315a..f14c1645a 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -838,3 +838,296 @@ def test_native_crash_http(cmake, httpserver): assert_minidump(envelope) assert_breadcrumb(envelope) assert_attachment(envelope) + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_on_network_error(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + # unreachable port triggers CURLE_COULDNT_CONNECT + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "capture-event"], + env=env_unreachable, + ) + + assert retry_dir.exists() + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 1 + assert "-00-" in str(retry_files[0].name) + + # retry on next run with working server + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "no-setup"], + env=env_reachable, + ) + assert waiting.result + + assert len(httpserver.log) == 1 + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + assert_meta(envelope, integration="inproc") + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_multiple_attempts(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run(tmp_path, "sentry_example", ["log", "http-retry", "capture-event"], env=env) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 1 + assert "-00-" in str(retry_files[0].name) + + run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 1 + assert "-01-" in str(retry_files[0].name) + + run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 1 + assert "-02-" in str(retry_files[0].name) + + # exhaust remaining retries (max 5) + for i in range(3): + run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + + # discarded after max retries (cache_keep not enabled) + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 0 + + cache_dir = tmp_path.joinpath(".sentry-native/cache") + cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + assert len(cache_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_with_cache_keep(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "cache-keep", "capture-event"], + env=env_unreachable, + ) + + assert retry_dir.exists() + assert len(list(retry_dir.glob("*.envelope"))) == 1 + + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "cache-keep", "no-setup"], + env=env_reachable, + ) + assert waiting.result + + assert len(list(retry_dir.glob("*.envelope"))) == 0 + assert len(list(cache_dir.glob("*.envelope"))) == 1 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_cache_keep_max_attempts(cmake): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "cache-keep", "capture-event"], + env=env, + ) + + assert retry_dir.exists() + assert len(list(retry_dir.glob("*.envelope"))) == 1 + + for _ in range(5): + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "cache-keep", "no-setup"], + env=env, + ) + + assert len(list(retry_dir.glob("*.envelope"))) == 0 + assert cache_dir.exists() + assert len(list(cache_dir.glob("*.envelope"))) == 1 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_http_error_discards_envelope(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( + "Internal Server Error", status=500 + ) + + with httpserver.wait(timeout=10) as waiting: + run(tmp_path, "sentry_example", ["log", "capture-event"], env=env) + assert waiting.result + + # HTTP errors discard, not retry + retry_files = list(retry_dir.glob("*.envelope")) if retry_dir.exists() else [] + assert len(retry_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_rate_limit_discards_envelope(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( + "Rate Limited", status=429, headers={"retry-after": "60"} + ) + + with httpserver.wait(timeout=10) as waiting: + run(tmp_path, "sentry_example", ["log", "capture-event"], env=env) + assert waiting.result + + # 429 discards, not retry + retry_files = list(retry_dir.glob("*.envelope")) if retry_dir.exists() else [] + assert len(retry_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_multiple_success(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "capture-multiple"], + env=env_unreachable, + ) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 10 + + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + for _ in range(10): + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( + "OK" + ) + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "no-setup"], + env=env_reachable, + ) + assert waiting.result + + assert len(httpserver.log) == 10 + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_multiple_network_error(cmake): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "capture-multiple"], + env=env, + ) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 10 + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "no-setup"], + env=env, + ) + + # all envelopes retried, all bumped to retry 1 + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 10 + retry_1 = [f for f in retry_files if "-01-" in f.name] + assert len(retry_1) == 10 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_multiple_rate_limit(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "capture-multiple"], + env=env_unreachable, + ) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 10 + + # rate limit response followed by discards for the rest (rate limiter + # kicks in after the first 429) + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_request("/api/123456/envelope/").respond_with_data( + "Rate Limited", status=429, headers={"retry-after": "60"} + ) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "no-setup"], + env=env_reachable, + ) + + # first envelope gets 429, rest are discarded by rate limiter + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 0 diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index fd16affca..3b3036259 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -43,6 +43,7 @@ add_executable(sentry_test_unit test_path.c test_process.c test_ratelimiter.c + test_retry.c test_ringbuffer.c test_sampling.c test_scope.c diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c new file mode 100644 index 000000000..74d5a9481 --- /dev/null +++ b/tests/unit/test_retry.c @@ -0,0 +1,341 @@ +#include "sentry_envelope.h" +#include "sentry_path.h" +#include "sentry_retry.h" +#include "sentry_session.h" +#include "sentry_testsupport.h" +#include "sentry_utils.h" +#include "sentry_uuid.h" + +#include +#include + +static int +count_envelope_files(const sentry_path_t *dir) +{ + int count = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(dir); + const sentry_path_t *file; + while (iter && (file = sentry__pathiter_next(iter)) != NULL) { + if (sentry__path_ends_with(file, ".envelope")) { + count++; + } + } + sentry__pathiter_free(iter); + return count; +} + +static int +find_envelope_attempt(const sentry_path_t *dir) +{ + sentry_pathiter_t *iter = sentry__path_iter_directory(dir); + const sentry_path_t *file; + while (iter && (file = sentry__pathiter_next(iter)) != NULL) { + if (!sentry__path_ends_with(file, ".envelope")) { + continue; + } + const char *name = sentry__path_filename(file); + uint64_t ts; + int attempt; + const char *uuid; + if (sentry__retry_parse_filename(name, &ts, &attempt, &uuid)) { + sentry__pathiter_free(iter); + return attempt; + } + } + sentry__pathiter_free(iter); + return -1; +} + +static void +write_retry_file(const sentry_path_t *retry_path, uint64_t timestamp, + int retry_count, const sentry_uuid_t *event_id) +{ + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry_value_t event = sentry__value_new_event_with_id(event_id); + sentry__envelope_add_event(envelope, event); + + char uuid_str[37]; + sentry_uuid_as_string(event_id, uuid_str); + char filename[80]; + snprintf(filename, sizeof(filename), "%llu-%02d-%s.envelope", + (unsigned long long)timestamp, retry_count, uuid_str); + + sentry_path_t *path = sentry__path_join_str(retry_path, filename); + (void)sentry_envelope_write_to_path(envelope, path); + sentry__path_free(path); + sentry_envelope_free(envelope); +} + +static sentry_envelope_t * +make_test_envelope(sentry_uuid_t *event_id) +{ + *event_id = sentry_uuid_new_v4(); + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry_value_t event = sentry__value_new_event_with_id(event_id); + sentry__envelope_add_event(envelope, event); + return envelope; +} + +SENTRY_TEST(retry_throttle) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-throttle"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); + TEST_ASSERT(!!retry); + + sentry_uuid_t ids[4]; + for (int i = 0; i < 4; i++) { + sentry_envelope_t *envelope = make_test_envelope(&ids[i]); + sentry__retry_write_envelope(retry, envelope); + sentry_envelope_free(envelope); + } + + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); + + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 4); + + for (size_t i = 0; i < count; i++) { + sentry__retry_handle_result(retry, paths[i], 200); + } + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); +} + +SENTRY_TEST(retry_result) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-result"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 2); + TEST_ASSERT(!!retry); + + sentry_uuid_t event_id; + sentry_envelope_t *envelope = make_test_envelope(&event_id); + + // 1. Write envelope (simulates network error → save for retry) + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); + + // 2. Success (200) → removes from retry dir + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], 200); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + // 3. Write again + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + + // 4. Rate limited (429) → removes + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], 429); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + // 5. Write again, then discard (0) → removes + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], 0); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + // 6. Network error twice → bumps count + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); + + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], -1); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); + + // 7. Network error again → exceeds max_retries=2, removed + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], -1); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + sentry_envelope_free(envelope); + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); +} + +SENTRY_TEST(retry_session) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_release(options, "test@1.0.0"); + sentry_init(options); + + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-session"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 2); + TEST_ASSERT(!!retry); + + sentry_session_t *session = sentry__session_new(); + TEST_ASSERT(!!session); + sentry_envelope_t *envelope = sentry__envelope_new(); + TEST_ASSERT(!!envelope); + sentry__envelope_add_session(envelope, session); + + // Session-only envelopes have no event_id → should not be written + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + sentry_envelope_free(envelope); + sentry__session_free(session); + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); + sentry_close(); +} + +SENTRY_TEST(retry_cache) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache/retry"); + sentry_path_t *cache_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache/cache"); + sentry__path_remove_all(retry_path); + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(retry_path); + sentry__path_create_dir_all(cache_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, cache_path, 5); + TEST_ASSERT(!!retry); + + // Create a retry file at the max retry count (4, with max_retries=5) + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(retry_path, sentry__monotonic_time(), 4, &event_id); + + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); + + // Network error on a file at count=4 with max_retries=5 → moves to cache + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], -1); + sentry__retry_free_paths(paths, count); + + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_remove_all(cache_path); + sentry__path_free(retry_path); + sentry__path_free(cache_path); +} + +SENTRY_TEST(retry_backoff) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-backoff"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); + TEST_ASSERT(!!retry); + + uint64_t now = sentry__monotonic_time(); + uint64_t base = SENTRY_RETRY_BACKOFF_BASE_MS; + + // retry 0 with old timestamp: eligible (base backoff expired) + sentry_uuid_t id1 = sentry_uuid_new_v4(); + write_retry_file(retry_path, now - base, 0, &id1); + + // retry 1 with recent timestamp: not yet eligible (needs 2*base) + sentry_uuid_t id2 = sentry_uuid_new_v4(); + write_retry_file(retry_path, now, 1, &id2); + + // retry 1 with old timestamp: eligible (2*base backoff expired) + sentry_uuid_t id3 = sentry_uuid_new_v4(); + write_retry_file(retry_path, now - 2 * base, 1, &id3); + + // retry 2 with old-ish timestamp: needs 4*base but only 2*base old + sentry_uuid_t id4 = sentry_uuid_new_v4(); + write_retry_file(retry_path, now - 2 * base, 2, &id4); + + // Startup scan (no backoff check): all 4 files returned + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 4); + sentry__retry_free_paths(paths, count); + + // With backoff check: only eligible ones (id1 and id3) + paths = sentry__retry_scan(retry, false, &count); + TEST_CHECK_INT_EQUAL(count, 2); + sentry__retry_free_paths(paths, count); + + // Verify backoff_ms calculation + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(0), base); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(1), base * 2); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(2), base * 4); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(3), base * 8); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(4), base * 8); + + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); +} + +SENTRY_TEST(retry_no_duplicate_rescan) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 3); + TEST_ASSERT(!!retry); + + sentry_uuid_t event_id; + sentry_envelope_t *envelope = make_test_envelope(&event_id); + sentry__retry_write_envelope(retry, envelope); + + // First scan returns the file + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + + // Handle as success → removes from retry dir + sentry__retry_handle_result(retry, paths[0], 200); + sentry__retry_free_paths(paths, count); + + // Second scan returns nothing + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 0); + sentry__retry_free_paths(paths, count); + + TEST_CHECK(!sentry__retry_has_files(retry)); + + sentry_envelope_free(envelope); + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 9c5abab0f..0b8ceefc8 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -191,6 +191,12 @@ XX(read_envelope_from_file) XX(read_write_envelope_to_file_null) XX(read_write_envelope_to_invalid_path) XX(recursive_paths) +XX(retry_backoff) +XX(retry_cache) +XX(retry_no_duplicate_rescan) +XX(retry_result) +XX(retry_session) +XX(retry_throttle) XX(ringbuffer_append) XX(ringbuffer_append_invalid_decref_value) XX(ringbuffer_append_null_decref_value) From e230f8bc55f950f59581ee422052fd6054f21a74 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:51:01 +0100 Subject: [PATCH 003/103] fix(retry): use wall clock time instead of monotonic time Monotonic time is process-relative and doesn't work across restarts. Retry envelope timestamps need to persist across sessions, so use time() (seconds since epoch) for file timestamps, startup_time, and backoff comparison. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 9 +++++---- src/transports/sentry_http_transport.c | 4 ++-- tests/unit/test_retry.c | 21 +++++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index fbb8dba89..5b5307a86 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -5,6 +5,7 @@ #include #include +#include struct sentry_retry_s { sentry_path_t *retry_dir; @@ -97,7 +98,7 @@ sentry__retry_write_envelope( return; } - uint64_t now = sentry__monotonic_time(); + uint64_t now = (uint64_t)time(NULL); char uuid_str[37]; sentry_uuid_as_string(&event_id, uuid_str); @@ -130,7 +131,7 @@ sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) } size_t path_count = 0; - uint64_t now = startup ? 0 : sentry__monotonic_time(); + uint64_t now = startup ? 0 : (uint64_t)time(NULL); const sentry_path_t *p; while ((p = sentry__pathiter_next(piter)) != NULL) { @@ -145,7 +146,7 @@ sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) if (retry->startup_time > 0 && ts >= retry->startup_time) { continue; } - } else if ((now - ts) < sentry__retry_backoff_ms(count)) { + } else if ((now - ts) < sentry__retry_backoff_ms(count) / 1000) { continue; } if (path_count == path_cap) { @@ -211,7 +212,7 @@ sentry__retry_handle_result( sentry__path_remove(path); } } else { - uint64_t now = sentry__monotonic_time(); + uint64_t now = (uint64_t)time(NULL); char new_filename[128]; snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", (unsigned long long)now, count + 1, uuid_start); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 3d5cee33c..28d5cc4ca 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -16,6 +16,7 @@ #include #include +#include #define ENVELOPE_MIME "application/x-sentry-envelope" #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -338,8 +339,7 @@ http_transport_start(const sentry_options_t *options, void *transport_state) sentry__path_free(retry_dir); } if (state->retry) { - sentry__retry_set_startup_time( - state->retry, sentry__monotonic_time()); + sentry__retry_set_startup_time(state->retry, (uint64_t)time(NULL)); sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 74d5a9481..dd2718901 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -230,7 +230,7 @@ SENTRY_TEST(retry_cache) // Create a retry file at the max retry count (4, with max_retries=5) sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry_path, sentry__monotonic_time(), 4, &event_id); + write_retry_file(retry_path, (uint64_t)time(NULL), 4, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); @@ -262,8 +262,8 @@ SENTRY_TEST(retry_backoff) sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); TEST_ASSERT(!!retry); - uint64_t now = sentry__monotonic_time(); - uint64_t base = SENTRY_RETRY_BACKOFF_BASE_MS; + uint64_t now = (uint64_t)time(NULL); + uint64_t base = SENTRY_RETRY_BACKOFF_BASE_MS / 1000; // retry 0 with old timestamp: eligible (base backoff expired) sentry_uuid_t id1 = sentry_uuid_new_v4(); @@ -293,11 +293,16 @@ SENTRY_TEST(retry_backoff) sentry__retry_free_paths(paths, count); // Verify backoff_ms calculation - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(0), base); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(1), base * 2); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(2), base * 4); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(3), base * 8); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(4), base * 8); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(0), SENTRY_RETRY_BACKOFF_BASE_MS); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(1), SENTRY_RETRY_BACKOFF_BASE_MS * 2); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(2), SENTRY_RETRY_BACKOFF_BASE_MS * 4); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(3), SENTRY_RETRY_BACKOFF_BASE_MS * 8); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(4), SENTRY_RETRY_BACKOFF_BASE_MS * 8); sentry__retry_free(retry); sentry__path_remove_all(retry_path); From 3810f2b925e439f4d5523ed0f52f988e05311537 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:54:09 +0100 Subject: [PATCH 004/103] ref(retry): define backoff base in seconds Rename SENTRY_RETRY_BACKOFF_BASE_MS to SENTRY_RETRY_BACKOFF_BASE_S and sentry__retry_backoff_ms to sentry__retry_backoff, since file timestamps are now in seconds. The bgworker delay sites multiply by 1000 to convert to the milliseconds it expects. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 6 +++--- src/sentry_retry.h | 4 ++-- src/transports/sentry_http_transport.c | 4 ++-- tests/unit/test_retry.c | 17 ++++++----------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 5b5307a86..0e4058a22 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -75,10 +75,10 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, } uint64_t -sentry__retry_backoff_ms(int count) +sentry__retry_backoff(int count) { int shift = count < 3 ? count : 3; - return (uint64_t)SENTRY_RETRY_BACKOFF_BASE_MS << shift; + return (uint64_t)SENTRY_RETRY_BACKOFF_BASE_S << shift; } static int @@ -146,7 +146,7 @@ sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) if (retry->startup_time > 0 && ts >= retry->startup_time) { continue; } - } else if ((now - ts) < sentry__retry_backoff_ms(count) / 1000) { + } else if ((now - ts) < sentry__retry_backoff(count)) { continue; } if (path_count == path_cap) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 07a11d93a..4aa360d0c 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -4,7 +4,7 @@ #include "sentry_boot.h" #include "sentry_path.h" -#define SENTRY_RETRY_BACKOFF_BASE_MS 900000 +#define SENTRY_RETRY_BACKOFF_BASE_S 900 #define SENTRY_RETRY_STARTUP_DELAY_MS 100 typedef struct sentry_retry_s sentry_retry_t; @@ -28,7 +28,7 @@ void sentry__retry_handle_result( bool sentry__retry_has_files(const sentry_retry_t *retry); -uint64_t sentry__retry_backoff_ms(int count); +uint64_t sentry__retry_backoff(int count); bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 28d5cc4ca..3f41f2984 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -257,7 +257,7 @@ retry_process_task(void *_startup, void *_state) if (sentry__retry_has_files(state->retry)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_MS); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); } } @@ -293,7 +293,7 @@ http_send_task(void *_envelope, void *_state) if (status_code < 0 && state->retry) { sentry__retry_write_envelope(state->retry, envelope); sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_MS); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); } } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index dd2718901..7ec7c4046 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -263,7 +263,7 @@ SENTRY_TEST(retry_backoff) TEST_ASSERT(!!retry); uint64_t now = (uint64_t)time(NULL); - uint64_t base = SENTRY_RETRY_BACKOFF_BASE_MS / 1000; + uint64_t base = SENTRY_RETRY_BACKOFF_BASE_S; // retry 0 with old timestamp: eligible (base backoff expired) sentry_uuid_t id1 = sentry_uuid_new_v4(); @@ -293,16 +293,11 @@ SENTRY_TEST(retry_backoff) sentry__retry_free_paths(paths, count); // Verify backoff_ms calculation - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(0), SENTRY_RETRY_BACKOFF_BASE_MS); - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(1), SENTRY_RETRY_BACKOFF_BASE_MS * 2); - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(2), SENTRY_RETRY_BACKOFF_BASE_MS * 4); - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(3), SENTRY_RETRY_BACKOFF_BASE_MS * 8); - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(4), SENTRY_RETRY_BACKOFF_BASE_MS * 8); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(1), base * 2); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(2), base * 4); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(3), base * 8); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 8); sentry__retry_free(retry); sentry__path_remove_all(retry_path); From a910f324dcd4bff080e02f210105418edfd401c9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:03:50 +0100 Subject: [PATCH 005/103] ref(retry): replace scan+free_paths with foreach callback API Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 27 +++---- src/sentry_retry.h | 5 +- src/transports/sentry_http_transport.c | 54 ++++++------- tests/unit/test_retry.c | 101 +++++++++++++------------ 4 files changed, 93 insertions(+), 94 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 0e4058a22..88bb9714e 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -113,21 +113,20 @@ sentry__retry_write_envelope( } } -sentry_path_t ** -sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) +void +sentry__retry_foreach(sentry_retry_t *retry, bool startup, + bool (*callback)(const sentry_path_t *path, void *data), void *data) { - *count_out = 0; - sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); if (!piter) { - return NULL; + return; } size_t path_cap = 16; sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); if (!paths) { sentry__pathiter_free(piter); - return NULL; + return; } size_t path_count = 0; @@ -168,17 +167,13 @@ sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); } - *count_out = path_count; - return paths; -} - -void -sentry__retry_free_paths(sentry_path_t **paths, size_t count) -{ - if (!paths) { - return; + for (size_t i = 0; i < path_count; i++) { + if (!callback(paths[i], data)) { + break; + } } - for (size_t i = 0; i < count; i++) { + + for (size_t i = 0; i < path_count; i++) { sentry__path_free(paths[i]); } sentry_free(paths); diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 4aa360d0c..15adaf8b3 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -19,9 +19,8 @@ void sentry__retry_write_envelope( void sentry__retry_set_startup_time( sentry_retry_t *retry, uint64_t startup_time); -sentry_path_t **sentry__retry_scan( - sentry_retry_t *retry, bool startup, size_t *count_out); -void sentry__retry_free_paths(sentry_path_t **paths, size_t count); +void sentry__retry_foreach(sentry_retry_t *retry, bool startup, + bool (*callback)(const sentry_path_t *path, void *data), void *data); void sentry__retry_handle_result( sentry_retry_t *retry, const sentry_path_t *path, int status_code); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 3f41f2984..c01c25744 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -189,8 +189,6 @@ sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) sentry_free(req); } -static void retry_process_task(void *_check_backoff, void *_state); - static int http_send_request( http_transport_state_t *state, sentry_prepared_http_request_t *req) @@ -219,41 +217,43 @@ http_send_request( return resp.status_code; } -static void -retry_process_task(void *_startup, void *_state) +static bool +retry_send_cb(const sentry_path_t *path, void *_state) { - int startup = (int)(intptr_t)_startup; http_transport_state_t *state = _state; - if (!state->retry) { - return; + sentry_envelope_t *envelope = sentry__envelope_from_path(path); + if (!envelope) { + sentry__path_remove(path); + return true; } - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(state->retry, startup, &count); + sentry_prepared_http_request_t *req = sentry__prepare_http_request( + envelope, state->dsn, state->ratelimiter, state->user_agent); + int status_code; + if (!req) { + status_code = 0; + } else { + status_code = http_send_request(state, req); + sentry__prepared_http_request_free(req); + } + sentry_envelope_free(envelope); - for (size_t i = 0; i < count; i++) { - sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); - if (!envelope) { - sentry__path_remove(paths[i]); - continue; - } + sentry__retry_handle_result(state->retry, path, status_code); + return true; +} - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - envelope, state->dsn, state->ratelimiter, state->user_agent); - int status_code; - if (!req) { - status_code = 0; - } else { - status_code = http_send_request(state, req); - sentry__prepared_http_request_free(req); - } - sentry_envelope_free(envelope); +static void +retry_process_task(void *_startup, void *_state) +{ + int startup = (int)(intptr_t)_startup; + http_transport_state_t *state = _state; - sentry__retry_handle_result(state->retry, paths[i], status_code); + if (!state->retry) { + return; } - sentry__retry_free_paths(paths, count); + sentry__retry_foreach(state->retry, startup, retry_send_cb, state); if (sentry__retry_has_files(state->retry)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 7ec7c4046..5e88d9731 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -76,6 +76,29 @@ make_test_envelope(sentry_uuid_t *event_id) return envelope; } +typedef struct { + sentry_retry_t *retry; + int status_code; + size_t count; +} retry_test_ctx_t; + +static bool +handle_result_cb(const sentry_path_t *path, void *_ctx) +{ + retry_test_ctx_t *ctx = _ctx; + ctx->count++; + sentry__retry_handle_result(ctx->retry, path, ctx->status_code); + return true; +} + +static bool +count_cb(const sentry_path_t *path, void *_count) +{ + (void)path; + (*(size_t *)_count)++; + return true; +} + SENTRY_TEST(retry_throttle) { sentry_path_t *retry_path @@ -95,14 +118,9 @@ SENTRY_TEST(retry_throttle) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 4); - - for (size_t i = 0; i < count; i++) { - sentry__retry_handle_result(retry, paths[i], 200); - } - sentry__retry_free_paths(paths, count); + retry_test_ctx_t ctx = { retry, 200, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 4); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); sentry__retry_free(retry); @@ -129,11 +147,9 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); // 2. Success (200) → removes from retry dir - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], 200); - sentry__retry_free_paths(paths, count); + retry_test_ctx_t ctx = { retry, 200, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 3. Write again @@ -141,19 +157,17 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); // 4. Rate limited (429) → removes - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], 429); - sentry__retry_free_paths(paths, count); + ctx = (retry_test_ctx_t) { retry, 429, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 5. Write again, then discard (0) → removes sentry__retry_write_envelope(retry, envelope); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], 0); - sentry__retry_free_paths(paths, count); + ctx = (retry_test_ctx_t) { retry, 0, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 6. Network error twice → bumps count @@ -161,18 +175,16 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], -1); - sentry__retry_free_paths(paths, count); + ctx = (retry_test_ctx_t) { retry, -1, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); // 7. Network error again → exceeds max_retries=2, removed - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], -1); - sentry__retry_free_paths(paths, count); + ctx = (retry_test_ctx_t) { retry, -1, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); sentry_envelope_free(envelope); @@ -236,11 +248,9 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // Network error on a file at count=4 with max_retries=5 → moves to cache - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], -1); - sentry__retry_free_paths(paths, count); + retry_test_ctx_t ctx = { retry, -1, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); @@ -283,14 +293,13 @@ SENTRY_TEST(retry_backoff) // Startup scan (no backoff check): all 4 files returned size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + sentry__retry_foreach(retry, true, count_cb, &count); TEST_CHECK_INT_EQUAL(count, 4); - sentry__retry_free_paths(paths, count); // With backoff check: only eligible ones (id1 and id3) - paths = sentry__retry_scan(retry, false, &count); + count = 0; + sentry__retry_foreach(retry, false, count_cb, &count); TEST_CHECK_INT_EQUAL(count, 2); - sentry__retry_free_paths(paths, count); // Verify backoff_ms calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); @@ -319,18 +328,14 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry__retry_write_envelope(retry, envelope); // First scan returns the file - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - - // Handle as success → removes from retry dir - sentry__retry_handle_result(retry, paths[0], 200); - sentry__retry_free_paths(paths, count); + retry_test_ctx_t ctx = { retry, 200, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); // Second scan returns nothing - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 0); - sentry__retry_free_paths(paths, count); + ctx.count = 0; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 0); TEST_CHECK(!sentry__retry_has_files(retry)); From 8b8921bc2d7b866fd827bb535f91415e6a36bc3f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:14:51 +0100 Subject: [PATCH 006/103] feat(transport): set 15s request timeout for curl and winhttp Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_curl.c | 1 + src/transports/sentry_http_transport_winhttp.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index b0c967f5c..417c24cfe 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -189,6 +189,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index 0997d3562..e3f003a18 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -134,6 +134,8 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 1; } + WinHttpSetTimeouts(client->session, 15000, 15000, 15000, 15000); + return 0; } From 46d2a9e25988e02b2f887a60365ab85b3c4c2e88 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:21:26 +0100 Subject: [PATCH 007/103] fix(retry): avoid duplicate delayed retry tasks on startup Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 30 ++++---------------------- src/sentry_retry.h | 4 +--- src/transports/sentry_http_transport.c | 4 +--- tests/unit/test_retry.c | 2 -- 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 88bb9714e..86a77175e 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -113,20 +113,20 @@ sentry__retry_write_envelope( } } -void +size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, bool (*callback)(const sentry_path_t *path, void *data), void *data) { sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); if (!piter) { - return; + return 0; } size_t path_cap = 16; sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); if (!paths) { sentry__pathiter_free(piter); - return; + return 0; } size_t path_count = 0; @@ -177,6 +177,7 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, sentry__path_free(paths[i]); } sentry_free(paths); + return path_count; } void @@ -234,26 +235,3 @@ sentry__retry_handle_result( sentry__path_remove(path); } } - -bool -sentry__retry_has_files(const sentry_retry_t *retry) -{ - sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); - if (!piter) { - return false; - } - - const sentry_path_t *p; - while ((p = sentry__pathiter_next(piter)) != NULL) { - const char *fname = sentry__path_filename(p); - uint64_t ts; - int count; - const char *uuid_start; - if (sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { - sentry__pathiter_free(piter); - return true; - } - } - sentry__pathiter_free(piter); - return false; -} diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 15adaf8b3..5ee2c6dc7 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -19,14 +19,12 @@ void sentry__retry_write_envelope( void sentry__retry_set_startup_time( sentry_retry_t *retry, uint64_t startup_time); -void sentry__retry_foreach(sentry_retry_t *retry, bool startup, +size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, bool (*callback)(const sentry_path_t *path, void *data), void *data); void sentry__retry_handle_result( sentry_retry_t *retry, const sentry_path_t *path, int status_code); -bool sentry__retry_has_files(const sentry_retry_t *retry); - uint64_t sentry__retry_backoff(int count); bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index c01c25744..d94f68349 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -253,9 +253,7 @@ retry_process_task(void *_startup, void *_state) return; } - sentry__retry_foreach(state->retry, startup, retry_send_cb, state); - - if (sentry__retry_has_files(state->retry)) { + if (sentry__retry_foreach(state->retry, startup, retry_send_cb, state)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 5e88d9731..922dae1c1 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -337,8 +337,6 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry__retry_foreach(retry, true, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 0); - TEST_CHECK(!sentry__retry_has_files(retry)); - sentry_envelope_free(envelope); sentry__retry_free(retry); sentry__path_remove_all(retry_path); From 4f3d62533207741188bc28da59573934c6e2c1cb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:38:57 +0100 Subject: [PATCH 008/103] ref(retry): take options in sentry__retry_new, own path construction Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 29 +++++- src/sentry_retry.h | 3 +- src/transports/sentry_http_transport.c | 28 +----- tests/unit/test_retry.c | 122 ++++++++++++++++--------- 4 files changed, 109 insertions(+), 73 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 86a77175e..87f2d2c46 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -1,6 +1,7 @@ #include "sentry_retry.h" #include "sentry_alloc.h" #include "sentry_envelope.h" +#include "sentry_options.h" #include "sentry_utils.h" #include @@ -15,16 +16,34 @@ struct sentry_retry_s { }; sentry_retry_t * -sentry__retry_new( - sentry_path_t *retry_dir, sentry_path_t *cache_dir, int max_retries) +sentry__retry_new(const sentry_options_t *options) { + if (options->http_retries <= 0 || !options->database_path) { + return NULL; + } + sentry_path_t *retry_dir + = sentry__path_join_str(options->database_path, "retry"); + if (!retry_dir) { + return NULL; + } + sentry_path_t *cache_dir = NULL; + if (options->cache_keep) { + cache_dir = sentry__path_join_str(options->database_path, "cache"); + } + sentry_retry_t *retry = SENTRY_MAKE(sentry_retry_t); if (!retry) { + sentry__path_free(cache_dir); + sentry__path_free(retry_dir); return NULL; } - retry->retry_dir = sentry__path_clone(retry_dir); - retry->cache_dir = cache_dir ? sentry__path_clone(cache_dir) : NULL; - retry->max_retries = max_retries; + retry->retry_dir = retry_dir; + retry->cache_dir = cache_dir; + retry->max_retries = options->http_retries; + sentry__path_create_dir_all(retry->retry_dir); + if (retry->cache_dir) { + sentry__path_create_dir_all(retry->cache_dir); + } return retry; } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 5ee2c6dc7..10bf7e1c4 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -9,8 +9,7 @@ typedef struct sentry_retry_s sentry_retry_t; -sentry_retry_t *sentry__retry_new( - sentry_path_t *retry_dir, sentry_path_t *cache_dir, int max_retries); +sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); void sentry__retry_write_envelope( diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index d94f68349..6848dcb7e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -318,29 +318,11 @@ http_transport_start(const sentry_options_t *options, void *transport_state) return rv; } - if (options->http_retries > 0) { - sentry_path_t *retry_dir - = sentry__path_join_str(options->database_path, "retry"); - if (retry_dir) { - sentry__path_create_dir_all(retry_dir); - sentry_path_t *cache_dir = NULL; - if (options->cache_keep) { - cache_dir - = sentry__path_join_str(options->database_path, "cache"); - if (cache_dir) { - sentry__path_create_dir_all(cache_dir); - } - } - state->retry = sentry__retry_new( - retry_dir, cache_dir, options->http_retries); - sentry__path_free(cache_dir); - sentry__path_free(retry_dir); - } - if (state->retry) { - sentry__retry_set_startup_time(state->retry, (uint64_t)time(NULL)); - sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, - (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); - } + state->retry = sentry__retry_new(options); + if (state->retry) { + sentry__retry_set_startup_time(state->retry, (uint64_t)time(NULL)); + sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, + (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); } return 0; diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 922dae1c1..101c1d9e2 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -101,14 +101,20 @@ count_cb(const sentry_path_t *path, void *_count) SENTRY_TEST(retry_throttle) { - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-throttle"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-throttle"); + sentry_options_set_http_retries(options, 5); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { sentry_envelope_t *envelope = make_test_envelope(&ids[i]); @@ -124,20 +130,27 @@ SENTRY_TEST(retry_throttle) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } SENTRY_TEST(retry_result) { - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-result"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 2); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-result"); + sentry_options_set_http_retries(options, 2); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry_uuid_t event_id; sentry_envelope_t *envelope = make_test_envelope(&event_id); @@ -189,25 +202,32 @@ SENTRY_TEST(retry_result) sentry_envelope_free(envelope); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } SENTRY_TEST(retry_session) { - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_release(options, "test@1.0.0"); - sentry_init(options); + SENTRY_TEST_OPTIONS_NEW(init_options); + sentry_options_set_dsn(init_options, "https://foo@sentry.invalid/42"); + sentry_options_set_release(init_options, "test@1.0.0"); + sentry_init(init_options); - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-session"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 2); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-session"); + sentry_options_set_http_retries(options, 2); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry_session_t *session = sentry__session_new(); TEST_ASSERT(!!session); sentry_envelope_t *envelope = sentry__envelope_new(); @@ -221,25 +241,30 @@ SENTRY_TEST(retry_session) sentry_envelope_free(envelope); sentry__session_free(session); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); sentry_close(); } SENTRY_TEST(retry_cache) { - sentry_path_t *retry_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache/retry"); - sentry_path_t *cache_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache/cache"); - sentry__path_remove_all(retry_path); - sentry__path_remove_all(cache_path); - sentry__path_create_dir_all(retry_path); - sentry__path_create_dir_all(cache_path); - - sentry_retry_t *retry = sentry__retry_new(retry_path, cache_path, 5); + sentry_path_t *db_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache"); + sentry__path_remove_all(db_path); + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-cache"); + sentry_options_set_http_retries(options, 5); + sentry_options_set_cache_keep(options, 1); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); + // Create a retry file at the max retry count (4, with max_retries=5) sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, (uint64_t)time(NULL), 4, &event_id); @@ -256,22 +281,28 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); - sentry__path_remove_all(cache_path); sentry__path_free(retry_path); sentry__path_free(cache_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } SENTRY_TEST(retry_backoff) { - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-backoff"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-backoff"); + sentry_options_set_http_retries(options, 5); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + uint64_t now = (uint64_t)time(NULL); uint64_t base = SENTRY_RETRY_BACKOFF_BASE_S; @@ -301,7 +332,7 @@ SENTRY_TEST(retry_backoff) sentry__retry_foreach(retry, false, count_cb, &count); TEST_CHECK_INT_EQUAL(count, 2); - // Verify backoff_ms calculation + // Verify backoff calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(1), base * 2); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(2), base * 4); @@ -309,18 +340,23 @@ SENTRY_TEST(retry_backoff) TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 8); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } SENTRY_TEST(retry_no_duplicate_rescan) { - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 3); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); + sentry_options_set_http_retries(options, 3); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); sentry_uuid_t event_id; @@ -339,6 +375,6 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry_envelope_free(envelope); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); - sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } From 199bd3047fb40b164578af93dc21de99b716b2f5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 14:19:41 +0100 Subject: [PATCH 009/103] ref(retry): set startup_time at creation, remove setter Move startup_time initialization into sentry__retry_new and remove the unnecessary sentry__retry_set_startup_time indirection. Tests now use write_retry_file with timestamps well in the past to match production behavior where retry files are from previous sessions. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 7 +- src/sentry_retry.h | 3 - src/transports/sentry_http_transport.c | 1 - tests/unit/test_retry.c | 98 +++++++++++--------------- 4 files changed, 44 insertions(+), 65 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 87f2d2c46..d7280ac7c 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -40,6 +40,7 @@ sentry__retry_new(const sentry_options_t *options) retry->retry_dir = retry_dir; retry->cache_dir = cache_dir; retry->max_retries = options->http_retries; + retry->startup_time = (uint64_t)time(NULL); sentry__path_create_dir_all(retry->retry_dir); if (retry->cache_dir) { sentry__path_create_dir_all(retry->cache_dir); @@ -58,12 +59,6 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } -void -sentry__retry_set_startup_time(sentry_retry_t *retry, uint64_t startup_time) -{ - retry->startup_time = startup_time; -} - bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out) diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 10bf7e1c4..56bf99966 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -15,9 +15,6 @@ void sentry__retry_free(sentry_retry_t *retry); void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); -void sentry__retry_set_startup_time( - sentry_retry_t *retry, uint64_t startup_time); - size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, bool (*callback)(const sentry_path_t *path, void *data), void *data); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 6848dcb7e..caeff28aa 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -320,7 +320,6 @@ http_transport_start(const sentry_options_t *options, void *transport_state) state->retry = sentry__retry_new(options); if (state->retry) { - sentry__retry_set_startup_time(state->retry, (uint64_t)time(NULL)); sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 101c1d9e2..9f829daa0 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -66,16 +66,6 @@ write_retry_file(const sentry_path_t *retry_path, uint64_t timestamp, sentry_envelope_free(envelope); } -static sentry_envelope_t * -make_test_envelope(sentry_uuid_t *event_id) -{ - *event_id = sentry_uuid_new_v4(); - sentry_envelope_t *envelope = sentry__envelope_new(); - sentry_value_t event = sentry__value_new_event_with_id(event_id); - sentry__envelope_add_event(envelope, event); - return envelope; -} - typedef struct { sentry_retry_t *retry; int status_code; @@ -115,17 +105,17 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { - sentry_envelope_t *envelope = make_test_envelope(&ids[i]); - sentry__retry_write_envelope(retry, envelope); - sentry_envelope_free(envelope); + ids[i] = sentry_uuid_new_v4(); + write_retry_file(retry_path, old_ts, 0, &ids[i]); } TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 4); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -151,56 +141,52 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - sentry_uuid_t event_id; - sentry_envelope_t *envelope = make_test_envelope(&event_id); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + sentry_uuid_t event_id = sentry_uuid_new_v4(); - // 1. Write envelope (simulates network error → save for retry) - sentry__retry_write_envelope(retry, envelope); + // 1. Success (200) → removes from retry dir + write_retry_file(retry_path, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); - // 2. Success (200) → removes from retry dir retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); - // 3. Write again - sentry__retry_write_envelope(retry, envelope); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - - // 4. Rate limited (429) → removes + // 2. Rate limited (429) → removes + write_retry_file(retry_path, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 429, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); - // 5. Write again, then discard (0) → removes - sentry__retry_write_envelope(retry, envelope); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + // 3. Discard (0) → removes + write_retry_file(retry_path, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 0, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); - // 6. Network error twice → bumps count - sentry__retry_write_envelope(retry, envelope); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + // 4. Network error → bumps count + write_retry_file(retry_path, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); - // 7. Network error again → exceeds max_retries=2, removed + // 5. Network error at max count → exceeds max_retries=2, removed + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + write_retry_file(retry_path, old_ts, 1, &event_id); ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); - sentry_envelope_free(envelope); sentry__retry_free(retry); sentry__path_free(retry_path); sentry__path_remove_all(db_path); @@ -265,16 +251,16 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - // Create a retry file at the max retry count (4, with max_retries=5) + uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry_path, (uint64_t)time(NULL), 4, &event_id); + write_retry_file(retry_path, old_ts, 4, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // Network error on a file at count=4 with max_retries=5 → moves to cache retry_test_ctx_t ctx = { retry, -1, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -303,24 +289,24 @@ SENTRY_TEST(retry_backoff) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t now = (uint64_t)time(NULL); uint64_t base = SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t ref = (uint64_t)time(NULL) - 10 * base; - // retry 0 with old timestamp: eligible (base backoff expired) + // retry 0: 10*base old, eligible (backoff=base) sentry_uuid_t id1 = sentry_uuid_new_v4(); - write_retry_file(retry_path, now - base, 0, &id1); + write_retry_file(retry_path, ref, 0, &id1); - // retry 1 with recent timestamp: not yet eligible (needs 2*base) + // retry 1: 1*base old, not yet eligible (backoff=2*base) sentry_uuid_t id2 = sentry_uuid_new_v4(); - write_retry_file(retry_path, now, 1, &id2); + write_retry_file(retry_path, ref + 9 * base, 1, &id2); - // retry 1 with old timestamp: eligible (2*base backoff expired) + // retry 1: 10*base old, eligible (backoff=2*base) sentry_uuid_t id3 = sentry_uuid_new_v4(); - write_retry_file(retry_path, now - 2 * base, 1, &id3); + write_retry_file(retry_path, ref, 1, &id3); - // retry 2 with old-ish timestamp: needs 4*base but only 2*base old + // retry 2: 2*base old, not eligible (backoff=4*base) sentry_uuid_t id4 = sentry_uuid_new_v4(); - write_retry_file(retry_path, now - 2 * base, 2, &id4); + write_retry_file(retry_path, ref + 8 * base, 2, &id4); // Startup scan (no backoff check): all 4 files returned size_t count = 0; @@ -359,21 +345,23 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_uuid_t event_id; - sentry_envelope_t *envelope = make_test_envelope(&event_id); - sentry__retry_write_envelope(retry, envelope); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + + uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(retry_path, old_ts, 0, &event_id); // First scan returns the file retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); // Second scan returns nothing ctx.count = 0; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 0); - sentry_envelope_free(envelope); + sentry__path_free(retry_path); sentry__retry_free(retry); sentry__path_remove_all(db_path); sentry__path_free(db_path); From 94a7ca7f8c7e09d1c5de92a568fe885ae50f2a4d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 14:49:31 +0100 Subject: [PATCH 010/103] fix(retry): return total file count so polling continues during backoff When files exist but aren't eligible yet (backoff not elapsed), foreach was returning 0 causing the retry polling task to stop. Return total valid retry files found instead of just the eligible count so the caller keeps rescheduling. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d7280ac7c..d3400fc2d 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -143,7 +143,8 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, return 0; } - size_t path_count = 0; + size_t total = 0; + size_t eligible = 0; uint64_t now = startup ? 0 : (uint64_t)time(NULL); const sentry_path_t *p; @@ -159,39 +160,41 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, if (retry->startup_time > 0 && ts >= retry->startup_time) { continue; } - } else if ((now - ts) < sentry__retry_backoff(count)) { + } + total++; + if (!startup && (now - ts) < sentry__retry_backoff(count)) { continue; } - if (path_count == path_cap) { + if (eligible == path_cap) { path_cap *= 2; sentry_path_t **tmp = sentry_malloc(path_cap * sizeof(sentry_path_t *)); if (!tmp) { break; } - memcpy(tmp, paths, path_count * sizeof(sentry_path_t *)); + memcpy(tmp, paths, eligible * sizeof(sentry_path_t *)); sentry_free(paths); paths = tmp; } - paths[path_count++] = sentry__path_clone(p); + paths[eligible++] = sentry__path_clone(p); } sentry__pathiter_free(piter); - if (path_count > 1) { - qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); + if (eligible > 1) { + qsort(paths, eligible, sizeof(sentry_path_t *), compare_retry_paths); } - for (size_t i = 0; i < path_count; i++) { + for (size_t i = 0; i < eligible; i++) { if (!callback(paths[i], data)) { break; } } - for (size_t i = 0; i < path_count; i++) { + for (size_t i = 0; i < eligible; i++) { sentry__path_free(paths[i]); } sentry_free(paths); - return path_count; + return total; } void From 011fbb5adfc853285cd9d480d1aace2d333ebf73 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 15:08:40 +0100 Subject: [PATCH 011/103] fix(retry): use callback return value to track remaining retry files Make handle_result return bool (true = file rescheduled for retry, false = file consumed) and use it in foreach to decrement the total count. This avoids one extra no-op poll cycle after the last retry. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 9 ++++++--- src/sentry_retry.h | 4 ++-- src/transports/sentry_http_transport.c | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d3400fc2d..da99ffdae 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -186,7 +186,7 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, for (size_t i = 0; i < eligible; i++) { if (!callback(paths[i], data)) { - break; + total--; } } @@ -197,7 +197,7 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, return total; } -void +bool sentry__retry_handle_result( sentry_retry_t *retry, const sentry_path_t *path, int status_code) { @@ -207,7 +207,7 @@ sentry__retry_handle_result( const char *uuid_start; if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { sentry__path_remove(path); - return; + return false; } if (status_code < 0) { @@ -224,6 +224,7 @@ sentry__retry_handle_result( } else { sentry__path_remove(path); } + return false; } else { uint64_t now = (uint64_t)time(NULL); char new_filename[128]; @@ -235,6 +236,7 @@ sentry__retry_handle_result( sentry__path_rename(path, new_path); sentry__path_free(new_path); } + return true; } } else if (status_code >= 200 && status_code < 300) { if (retry->cache_dir) { @@ -251,4 +253,5 @@ sentry__retry_handle_result( } else { sentry__path_remove(path); } + return false; } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 56bf99966..0e46ff62a 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -4,7 +4,7 @@ #include "sentry_boot.h" #include "sentry_path.h" -#define SENTRY_RETRY_BACKOFF_BASE_S 900 +#define SENTRY_RETRY_BACKOFF_BASE_S 15 // 900 #define SENTRY_RETRY_STARTUP_DELAY_MS 100 typedef struct sentry_retry_s sentry_retry_t; @@ -18,7 +18,7 @@ void sentry__retry_write_envelope( size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, bool (*callback)(const sentry_path_t *path, void *data), void *data); -void sentry__retry_handle_result( +bool sentry__retry_handle_result( sentry_retry_t *retry, const sentry_path_t *path, int status_code); uint64_t sentry__retry_backoff(int count); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index caeff28aa..e41cc71fd 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -239,8 +239,7 @@ retry_send_cb(const sentry_path_t *path, void *_state) } sentry_envelope_free(envelope); - sentry__retry_handle_result(state->retry, path, status_code); - return true; + return sentry__retry_handle_result(state->retry, path, status_code); } static void From f6731932c2d682c952b3d9c604688d0eabd5de4e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 15:18:00 +0100 Subject: [PATCH 012/103] ref(retry): rename constants to SENTRY_RETRY_INTERVAL and SENTRY_RETRY_THROTTLE Replace SENTRY_RETRY_BACKOFF_BASE_S and SENTRY_RETRY_STARTUP_DELAY_MS with ms-based constants so the transport uses them directly without leaking unit conversion details. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- src/sentry_retry.h | 4 ++-- src/transports/sentry_http_transport.c | 6 +++--- tests/unit/test_retry.c | 14 +++++++++----- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index da99ffdae..96f8cf409 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -92,7 +92,7 @@ uint64_t sentry__retry_backoff(int count) { int shift = count < 3 ? count : 3; - return (uint64_t)SENTRY_RETRY_BACKOFF_BASE_S << shift; + return (uint64_t)(SENTRY_RETRY_INTERVAL / 1000) << shift; } static int diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 0e46ff62a..ac84afac6 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -4,8 +4,8 @@ #include "sentry_boot.h" #include "sentry_path.h" -#define SENTRY_RETRY_BACKOFF_BASE_S 15 // 900 -#define SENTRY_RETRY_STARTUP_DELAY_MS 100 +#define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) +#define SENTRY_RETRY_THROTTLE 100 typedef struct sentry_retry_s sentry_retry_t; diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index e41cc71fd..79406591e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -254,7 +254,7 @@ retry_process_task(void *_startup, void *_state) if (sentry__retry_foreach(state->retry, startup, retry_send_cb, state)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_INTERVAL); } } @@ -290,7 +290,7 @@ http_send_task(void *_envelope, void *_state) if (status_code < 0 && state->retry) { sentry__retry_write_envelope(state->retry, envelope); sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_INTERVAL); } } @@ -320,7 +320,7 @@ http_transport_start(const sentry_options_t *options, void *transport_state) state->retry = sentry__retry_new(options); if (state->retry) { sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, - (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); + (void *)(intptr_t)1, SENTRY_RETRY_THROTTLE); } return 0; diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 9f829daa0..375a55f51 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -105,7 +105,8 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t old_ts + = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); @@ -141,7 +142,8 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t old_ts + = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir @@ -251,7 +253,8 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t old_ts + = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, old_ts, 4, &event_id); @@ -289,7 +292,7 @@ SENTRY_TEST(retry_backoff) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t base = SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t base = (SENTRY_RETRY_INTERVAL / 1000); uint64_t ref = (uint64_t)time(NULL) - 10 * base; // retry 0: 10*base old, eligible (backoff=base) @@ -347,7 +350,8 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t old_ts + = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, old_ts, 0, &event_id); From caef6c18892666d24ac59e77d90c4cc1d72239ec Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:38:31 +0100 Subject: [PATCH 013/103] ref(retry): encapsulate retry scheduling into the retry module Give the retry module a bgworker ref and send callback so it owns all scheduling. Transport just calls _start and _enqueue. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 55 ++++++++++++++++++++++++++ src/sentry_retry.h | 12 ++++-- src/transports/sentry_http_transport.c | 30 ++------------ tests/unit/test_retry.c | 14 +++---- 4 files changed, 72 insertions(+), 39 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 96f8cf409..20767d49a 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -8,11 +8,17 @@ #include #include +#define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) +#define SENTRY_RETRY_THROTTLE 100 + struct sentry_retry_s { sentry_path_t *retry_dir; sentry_path_t *cache_dir; int max_retries; uint64_t startup_time; + sentry_bgworker_t *bgworker; + sentry_retry_send_func_t send_cb; + void *send_data; }; sentry_retry_t * @@ -59,6 +65,55 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } +static void retry_poll_task(void *_retry, void *_state); + +static void +retry_startup_task(void *_retry, void *_state) +{ + (void)_state; + sentry_retry_t *retry = _retry; + if (sentry__retry_foreach(retry, true, retry->send_cb, retry->send_data)) { + sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, + retry, SENTRY_RETRY_INTERVAL); + } +} + +static void +retry_poll_task(void *_retry, void *_state) +{ + (void)_state; + sentry_retry_t *retry = _retry; + if (sentry__retry_foreach(retry, false, retry->send_cb, retry->send_data)) { + sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, + retry, SENTRY_RETRY_INTERVAL); + } +} + +void +sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, + sentry_retry_send_func_t send_cb, void *send_data) +{ + if (!retry) { + return; + } + retry->bgworker = bgworker; + retry->send_cb = send_cb; + retry->send_data = send_data; + sentry__bgworker_submit_delayed( + bgworker, retry_startup_task, NULL, retry, SENTRY_RETRY_THROTTLE); +} + +void +sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) +{ + if (!retry) { + return; + } + sentry__retry_write_envelope(retry, envelope); + sentry__bgworker_submit_delayed( + retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); +} + bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out) diff --git a/src/sentry_retry.h b/src/sentry_retry.h index ac84afac6..9151888b1 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -3,15 +3,21 @@ #include "sentry_boot.h" #include "sentry_path.h" - -#define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) -#define SENTRY_RETRY_THROTTLE 100 +#include "sentry_sync.h" typedef struct sentry_retry_s sentry_retry_t; +typedef bool (*sentry_retry_send_func_t)(const sentry_path_t *path, void *data); + sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); +void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, + sentry_retry_send_func_t send_cb, void *send_data); + +void sentry__retry_enqueue( + sentry_retry_t *retry, const sentry_envelope_t *envelope); + void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 79406591e..90e7ff9d5 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -34,7 +34,6 @@ typedef struct { int (*start_client)(void *, const sentry_options_t *); sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); - sentry_bgworker_t *bgworker; sentry_retry_t *retry; } http_transport_state_t; @@ -242,22 +241,6 @@ retry_send_cb(const sentry_path_t *path, void *_state) return sentry__retry_handle_result(state->retry, path, status_code); } -static void -retry_process_task(void *_startup, void *_state) -{ - int startup = (int)(intptr_t)_startup; - http_transport_state_t *state = _state; - - if (!state->retry) { - return; - } - - if (sentry__retry_foreach(state->retry, startup, retry_send_cb, state)) { - sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_INTERVAL); - } -} - static void http_transport_state_free(void *_state) { @@ -287,10 +270,8 @@ http_send_task(void *_envelope, void *_state) int status_code = http_send_request(state, req); sentry__prepared_http_request_free(req); - if (status_code < 0 && state->retry) { - sentry__retry_write_envelope(state->retry, envelope); - sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_INTERVAL); + if (status_code < 0) { + sentry__retry_enqueue(state->retry, envelope); } } @@ -318,10 +299,7 @@ http_transport_start(const sentry_options_t *options, void *transport_state) } state->retry = sentry__retry_new(options); - if (state->retry) { - sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, - (void *)(intptr_t)1, SENTRY_RETRY_THROTTLE); - } + sentry__retry_start(state->retry, bgworker, retry_send_cb, state); return 0; } @@ -395,8 +373,6 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) http_transport_state_free(state); return NULL; } - state->bgworker = bgworker; - sentry_transport_t *transport = sentry_transport_new(http_transport_send_envelope); if (!transport) { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 375a55f51..515c8bd9a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -105,8 +105,7 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts - = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); @@ -142,8 +141,7 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts - = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir @@ -253,8 +251,7 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts - = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, old_ts, 4, &event_id); @@ -292,7 +289,7 @@ SENTRY_TEST(retry_backoff) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t base = (SENTRY_RETRY_INTERVAL / 1000); + uint64_t base = sentry__retry_backoff(0); uint64_t ref = (uint64_t)time(NULL) - 10 * base; // retry 0: 10*base old, eligible (backoff=base) @@ -350,8 +347,7 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts - = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, old_ts, 0, &event_id); From 9c7c99dd507e5fd5adb99823b5c24e029f2193b5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:41:57 +0100 Subject: [PATCH 014/103] ref(transport): remove unnecessary includes, restore blank line Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 90e7ff9d5..9c00deb20 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -8,15 +8,12 @@ #include "sentry_retry.h" #include "sentry_string.h" #include "sentry_transport.h" -#include "sentry_utils.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" #endif -#include #include -#include #define ENVELOPE_MIME "application/x-sentry-envelope" #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -373,6 +370,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) http_transport_state_free(state); return NULL; } + sentry_transport_t *transport = sentry_transport_new(http_transport_send_envelope); if (!transport) { From 3813feb1f00e856277db747cda836a9f4803dadb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:48:12 +0100 Subject: [PATCH 015/103] ref(retry): move precondition checks to callers sentry__retry_new only returns NULL on failure, not based on options. sentry__retry_start and _enqueue require non-NULL retry. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 9 --------- src/transports/sentry_http_transport.c | 10 +++++++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 20767d49a..6766d88ee 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -24,9 +24,6 @@ struct sentry_retry_s { sentry_retry_t * sentry__retry_new(const sentry_options_t *options) { - if (options->http_retries <= 0 || !options->database_path) { - return NULL; - } sentry_path_t *retry_dir = sentry__path_join_str(options->database_path, "retry"); if (!retry_dir) { @@ -93,9 +90,6 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data) { - if (!retry) { - return; - } retry->bgworker = bgworker; retry->send_cb = send_cb; retry->send_data = send_data; @@ -106,9 +100,6 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { - if (!retry) { - return; - } sentry__retry_write_envelope(retry, envelope); sentry__bgworker_submit_delayed( retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 9c00deb20..fe0102e37 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -267,7 +267,7 @@ http_send_task(void *_envelope, void *_state) int status_code = http_send_request(state, req); sentry__prepared_http_request_free(req); - if (status_code < 0) { + if (status_code < 0 && state->retry) { sentry__retry_enqueue(state->retry, envelope); } } @@ -295,8 +295,12 @@ http_transport_start(const sentry_options_t *options, void *transport_state) return rv; } - state->retry = sentry__retry_new(options); - sentry__retry_start(state->retry, bgworker, retry_send_cb, state); + if (options->http_retries > 0) { + state->retry = sentry__retry_new(options); + if (state->retry) { + sentry__retry_start(state->retry, bgworker, retry_send_cb, state); + } + } return 0; } From ea5233fa93577171e7178711e8bc0d10836dd108 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:55:04 +0100 Subject: [PATCH 016/103] ref(transport): extract http_send_envelope helper Deduplicate prepare/send/free sequence shared by retry_send_cb and http_send_task. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 34 ++++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index fe0102e37..4b688177e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -213,6 +213,19 @@ http_send_request( return resp.status_code; } +static int +http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope) +{ + sentry_prepared_http_request_t *req = sentry__prepare_http_request( + envelope, state->dsn, state->ratelimiter, state->user_agent); + if (!req) { + return 0; + } + int status_code = http_send_request(state, req); + sentry__prepared_http_request_free(req); + return status_code; +} + static bool retry_send_cb(const sentry_path_t *path, void *_state) { @@ -224,17 +237,8 @@ retry_send_cb(const sentry_path_t *path, void *_state) return true; } - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - envelope, state->dsn, state->ratelimiter, state->user_agent); - int status_code; - if (!req) { - status_code = 0; - } else { - status_code = http_send_request(state, req); - sentry__prepared_http_request_free(req); - } + int status_code = http_send_envelope(state, envelope); sentry_envelope_free(envelope); - return sentry__retry_handle_result(state->retry, path, status_code); } @@ -258,15 +262,7 @@ http_send_task(void *_envelope, void *_state) sentry_envelope_t *envelope = _envelope; http_transport_state_t *state = _state; - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - envelope, state->dsn, state->ratelimiter, state->user_agent); - if (!req) { - return; - } - - int status_code = http_send_request(state, req); - sentry__prepared_http_request_free(req); - + int status_code = http_send_envelope(state, envelope); if (status_code < 0 && state->retry) { sentry__retry_enqueue(state->retry, envelope); } From f17f8f93aac697510ef352ee06f2b205e1ee1ec8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:59:33 +0100 Subject: [PATCH 017/103] test(retry): remove redundant retry_no_duplicate_rescan test Already covered by retry_throttle and retry_result. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 36 ------------------------------------ tests/unit/tests.inc | 1 - 2 files changed, 37 deletions(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 515c8bd9a..a6b84a562 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -330,39 +330,3 @@ SENTRY_TEST(retry_backoff) sentry__path_remove_all(db_path); sentry__path_free(db_path); } - -SENTRY_TEST(retry_no_duplicate_rescan) -{ - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); - sentry__path_remove_all(db_path); - - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); - sentry_options_set_http_retries(options, 3); - sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); - TEST_ASSERT(!!retry); - - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - - uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); - sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry_path, old_ts, 0, &event_id); - - // First scan returns the file - retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); - TEST_CHECK_INT_EQUAL(ctx.count, 1); - - // Second scan returns nothing - ctx.count = 0; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); - TEST_CHECK_INT_EQUAL(ctx.count, 0); - - sentry__path_free(retry_path); - sentry__retry_free(retry); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); -} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 0b8ceefc8..537510365 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -193,7 +193,6 @@ XX(read_write_envelope_to_invalid_path) XX(recursive_paths) XX(retry_backoff) XX(retry_cache) -XX(retry_no_duplicate_rescan) XX(retry_result) XX(retry_session) XX(retry_throttle) From e7886a4e48335da4750e1c9ae8f484b70d33b72c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:02:22 +0100 Subject: [PATCH 018/103] ref(curl): use CURLOPT_TIMEOUT_MS for consistency with winhttp and crashpad Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_curl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 417c24cfe..969f86925 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -189,7 +189,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 15000L); char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; From ce4ac80c107addef7e2065101cd23cbf16a1c2f9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:20:24 +0100 Subject: [PATCH 019/103] ref(retry): unify startup and poll into a single task Pass startup_time directly to _foreach as a `before` filter instead of a bool. Clear it after the first run so subsequent polls use backoff. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 32 ++++++++++---------------------- src/sentry_retry.h | 2 +- tests/unit/test_retry.c | 18 +++++++++--------- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6766d88ee..f388c9da6 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -62,28 +62,18 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } -static void retry_poll_task(void *_retry, void *_state); - -static void -retry_startup_task(void *_retry, void *_state) -{ - (void)_state; - sentry_retry_t *retry = _retry; - if (sentry__retry_foreach(retry, true, retry->send_cb, retry->send_data)) { - sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, - retry, SENTRY_RETRY_INTERVAL); - } -} - static void retry_poll_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (sentry__retry_foreach(retry, false, retry->send_cb, retry->send_data)) { + if (sentry__retry_foreach( + retry, retry->startup_time, retry->send_cb, retry->send_data)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } + // subsequent polls use backoff instead of the startup time filter + retry->startup_time = 0; } void @@ -94,7 +84,7 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, retry->send_cb = send_cb; retry->send_data = send_data; sentry__bgworker_submit_delayed( - bgworker, retry_startup_task, NULL, retry, SENTRY_RETRY_THROTTLE); + bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); } void @@ -174,7 +164,7 @@ sentry__retry_write_envelope( } size_t -sentry__retry_foreach(sentry_retry_t *retry, bool startup, +sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, bool (*callback)(const sentry_path_t *path, void *data), void *data) { sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); @@ -191,7 +181,7 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, size_t total = 0; size_t eligible = 0; - uint64_t now = startup ? 0 : (uint64_t)time(NULL); + uint64_t now = before ? 0 : (uint64_t)time(NULL); const sentry_path_t *p; while ((p = sentry__pathiter_next(piter)) != NULL) { @@ -202,13 +192,11 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { continue; } - if (startup) { - if (retry->startup_time > 0 && ts >= retry->startup_time) { - continue; - } + if (before && ts >= before) { + continue; } total++; - if (!startup && (now - ts) < sentry__retry_backoff(count)) { + if (!before && (now - ts) < sentry__retry_backoff(count)) { continue; } if (eligible == path_cap) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 9151888b1..898ce3a13 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -21,7 +21,7 @@ void sentry__retry_enqueue( void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); -size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, +size_t sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, bool (*callback)(const sentry_path_t *path, void *data), void *data); bool sentry__retry_handle_result( diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index a6b84a562..fc90e3a7f 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -115,7 +115,7 @@ SENTRY_TEST(retry_throttle) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 4); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -150,21 +150,21 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 2. Rate limited (429) → removes write_retry_file(retry_path, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 429, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 3. Discard (0) → removes write_retry_file(retry_path, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 0, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -173,7 +173,7 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); @@ -183,7 +183,7 @@ SENTRY_TEST(retry_result) sentry__path_create_dir_all(retry_path); write_retry_file(retry_path, old_ts, 1, &event_id); ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -260,7 +260,7 @@ SENTRY_TEST(retry_cache) // Network error on a file at count=4 with max_retries=5 → moves to cache retry_test_ctx_t ctx = { retry, -1, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -310,12 +310,12 @@ SENTRY_TEST(retry_backoff) // Startup scan (no backoff check): all 4 files returned size_t count = 0; - sentry__retry_foreach(retry, true, count_cb, &count); + sentry__retry_foreach(retry, (uint64_t)time(NULL), count_cb, &count); TEST_CHECK_INT_EQUAL(count, 4); // With backoff check: only eligible ones (id1 and id3) count = 0; - sentry__retry_foreach(retry, false, count_cb, &count); + sentry__retry_foreach(retry, 0, count_cb, &count); TEST_CHECK_INT_EQUAL(count, 2); // Verify backoff calculation From 34d5131f31c4cae69bc848dc16ddee289d8675ba Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:47:36 +0100 Subject: [PATCH 020/103] ref(retry): extract sentry__retry_make_path helper Deduplicate filename construction across write_envelope, handle_result, and tests. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 46 +++++++++++++++++++++-------------------- src/sentry_retry.h | 9 ++++++++ tests/unit/test_retry.c | 36 +++++++++++++++----------------- 3 files changed, 50 insertions(+), 41 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index f388c9da6..99fe1f0c9 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -111,16 +111,16 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, return false; } - const char *uuid_start = end + 1; - size_t tail_len = strlen(uuid_start); + const char *uuid = end + 1; + size_t tail_len = strlen(uuid); // 36 chars UUID (with dashes) + ".envelope" - if (tail_len != 36 + 9 || strcmp(uuid_start + 36, ".envelope") != 0) { + if (tail_len != 36 + 9 || strcmp(uuid + 36, ".envelope") != 0) { return false; } *ts_out = ts; *count_out = (int)count; - *uuid_out = uuid_start; + *uuid_out = uuid; return true; } @@ -139,6 +139,16 @@ compare_retry_paths(const void *a, const void *b) return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); } +sentry_path_t * +sentry__retry_make_path( + sentry_retry_t *retry, uint64_t ts, int count, const char *uuid) +{ + char filename[128]; + snprintf(filename, sizeof(filename), "%llu-%02d-%.36s.envelope", + (unsigned long long)ts, count, uuid); + return sentry__path_join_str(retry->retry_dir, filename); +} + void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope) @@ -148,15 +158,11 @@ sentry__retry_write_envelope( return; } - uint64_t now = (uint64_t)time(NULL); - char uuid_str[37]; - sentry_uuid_as_string(&event_id, uuid_str); - - char filename[128]; - snprintf(filename, sizeof(filename), "%llu-00-%s.envelope", - (unsigned long long)now, uuid_str); + char uuid[37]; + sentry_uuid_as_string(&event_id, uuid); - sentry_path_t *path = sentry__path_join_str(retry->retry_dir, filename); + sentry_path_t *path + = sentry__retry_make_path(retry, (uint64_t)time(NULL), 0, uuid); if (path) { (void)sentry_envelope_write_to_path(envelope, path); sentry__path_free(path); @@ -188,8 +194,8 @@ sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, const char *fname = sentry__path_filename(p); uint64_t ts; int count; - const char *uuid_start; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + const char *uuid; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { continue; } if (before && ts >= before) { @@ -238,8 +244,8 @@ sentry__retry_handle_result( const char *fname = sentry__path_filename(path); uint64_t ts; int count; - const char *uuid_start; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + const char *uuid; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { sentry__path_remove(path); return false; } @@ -260,12 +266,8 @@ sentry__retry_handle_result( } return false; } else { - uint64_t now = (uint64_t)time(NULL); - char new_filename[128]; - snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", - (unsigned long long)now, count + 1, uuid_start); - sentry_path_t *new_path - = sentry__path_join_str(retry->retry_dir, new_filename); + sentry_path_t *new_path = sentry__retry_make_path( + retry, (uint64_t)time(NULL), count + 1, uuid); if (new_path) { sentry__path_rename(path, new_path); sentry__path_free(new_path); diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 898ce3a13..665fa7564 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -29,6 +29,15 @@ bool sentry__retry_handle_result( uint64_t sentry__retry_backoff(int count); +/** + * /retry/--.envelope + */ +sentry_path_t *sentry__retry_make_path( + sentry_retry_t *retry, uint64_t ts, int count, const char *uuid); + +/** + * --.envelope + */ bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out); diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index fc90e3a7f..8fb8c9042 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -47,20 +47,18 @@ find_envelope_attempt(const sentry_path_t *dir) } static void -write_retry_file(const sentry_path_t *retry_path, uint64_t timestamp, - int retry_count, const sentry_uuid_t *event_id) +write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, + const sentry_uuid_t *event_id) { sentry_envelope_t *envelope = sentry__envelope_new(); sentry_value_t event = sentry__value_new_event_with_id(event_id); sentry__envelope_add_event(envelope, event); - char uuid_str[37]; - sentry_uuid_as_string(event_id, uuid_str); - char filename[80]; - snprintf(filename, sizeof(filename), "%llu-%02d-%s.envelope", - (unsigned long long)timestamp, retry_count, uuid_str); + char uuid[37]; + sentry_uuid_as_string(event_id, uuid); - sentry_path_t *path = sentry__path_join_str(retry_path, filename); + sentry_path_t *path + = sentry__retry_make_path(retry, timestamp, retry_count, uuid); (void)sentry_envelope_write_to_path(envelope, path); sentry__path_free(path); sentry_envelope_free(envelope); @@ -109,7 +107,7 @@ SENTRY_TEST(retry_throttle) sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); - write_retry_file(retry_path, old_ts, 0, &ids[i]); + write_retry_file(retry, old_ts, 0, &ids[i]); } TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); @@ -145,7 +143,7 @@ SENTRY_TEST(retry_result) sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir - write_retry_file(retry_path, old_ts, 0, &event_id); + write_retry_file(retry, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); @@ -155,21 +153,21 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 2. Rate limited (429) → removes - write_retry_file(retry_path, old_ts, 0, &event_id); + write_retry_file(retry, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 429, 0 }; sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 3. Discard (0) → removes - write_retry_file(retry_path, old_ts, 0, &event_id); + write_retry_file(retry, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 0, 0 }; sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 4. Network error → bumps count - write_retry_file(retry_path, old_ts, 0, &event_id); + write_retry_file(retry, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); ctx = (retry_test_ctx_t) { retry, -1, 0 }; @@ -181,7 +179,7 @@ SENTRY_TEST(retry_result) // 5. Network error at max count → exceeds max_retries=2, removed sentry__path_remove_all(retry_path); sentry__path_create_dir_all(retry_path); - write_retry_file(retry_path, old_ts, 1, &event_id); + write_retry_file(retry, old_ts, 1, &event_id); ctx = (retry_test_ctx_t) { retry, -1, 0 }; sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); @@ -253,7 +251,7 @@ SENTRY_TEST(retry_cache) uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry_path, old_ts, 4, &event_id); + write_retry_file(retry, old_ts, 4, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); @@ -294,19 +292,19 @@ SENTRY_TEST(retry_backoff) // retry 0: 10*base old, eligible (backoff=base) sentry_uuid_t id1 = sentry_uuid_new_v4(); - write_retry_file(retry_path, ref, 0, &id1); + write_retry_file(retry, ref, 0, &id1); // retry 1: 1*base old, not yet eligible (backoff=2*base) sentry_uuid_t id2 = sentry_uuid_new_v4(); - write_retry_file(retry_path, ref + 9 * base, 1, &id2); + write_retry_file(retry, ref + 9 * base, 1, &id2); // retry 1: 10*base old, eligible (backoff=2*base) sentry_uuid_t id3 = sentry_uuid_new_v4(); - write_retry_file(retry_path, ref, 1, &id3); + write_retry_file(retry, ref, 1, &id3); // retry 2: 2*base old, not eligible (backoff=4*base) sentry_uuid_t id4 = sentry_uuid_new_v4(); - write_retry_file(retry_path, ref + 8 * base, 2, &id4); + write_retry_file(retry, ref + 8 * base, 2, &id4); // Startup scan (no backoff check): all 4 files returned size_t count = 0; From 8015d2cc2c1a834f3f5eca80d495ffdf389d3089 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 20:03:19 +0100 Subject: [PATCH 021/103] fix(retry): prevent envelope duplication between retry and cache When the transport supports retry and http_retries > 0, sentry__process_old_runs now skips caching .envelope files from old runs. The retry system handles persistence, so duplicating into cache/ is unnecessary. Also simplifies sentry__retry_handle_result: only cache on max retries exhausted, not on successful send. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 5 ++- src/sentry_retry.c | 49 +++++++++----------------- src/sentry_transport.c | 13 +++++++ src/sentry_transport.h | 4 +++ src/transports/sentry_http_transport.c | 1 + tests/test_integration_http.py | 3 +- 6 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 45f8b8eb1..7976ffc6b 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -4,6 +4,7 @@ #include "sentry_json.h" #include "sentry_options.h" #include "sentry_session.h" +#include "sentry_transport.h" #include "sentry_uuid.h" #include #include @@ -292,7 +293,9 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - if (cache_dir) { + bool can_retry = sentry__transport_can_retry(options->transport) + && options->http_retries > 0; + if (cache_dir && !can_retry) { sentry_path_t *cached_file = sentry__path_join_str( cache_dir, sentry__path_filename(file)); if (!cached_file diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 99fe1f0c9..0f145cbc2 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -250,39 +250,24 @@ sentry__retry_handle_result( return false; } - if (status_code < 0) { - if (count + 1 >= retry->max_retries) { - if (retry->cache_dir) { - sentry_path_t *dst - = sentry__path_join_str(retry->cache_dir, fname); - if (dst) { - sentry__path_rename(path, dst); - sentry__path_free(dst); - } else { - sentry__path_remove(path); - } - } else { - sentry__path_remove(path); - } - return false; - } else { - sentry_path_t *new_path = sentry__retry_make_path( - retry, (uint64_t)time(NULL), count + 1, uuid); - if (new_path) { - sentry__path_rename(path, new_path); - sentry__path_free(new_path); - } - return true; + if (status_code < 0 && count + 1 < retry->max_retries) { + sentry_path_t *new_path = sentry__retry_make_path( + retry, (uint64_t)time(NULL), count + 1, uuid); + if (new_path) { + sentry__path_rename(path, new_path); + sentry__path_free(new_path); } - } else if (status_code >= 200 && status_code < 300) { - if (retry->cache_dir) { - sentry_path_t *dst = sentry__path_join_str(retry->cache_dir, fname); - if (dst) { - sentry__path_rename(path, dst); - sentry__path_free(dst); - } else { - sentry__path_remove(path); - } + return true; + } + + if (count + 1 >= retry->max_retries && retry->cache_dir) { + char cache_name[46]; + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid); + sentry_path_t *dst + = sentry__path_join_str(retry->cache_dir, cache_name); + if (dst) { + sentry__path_rename(path, dst); + sentry__path_free(dst); } else { sentry__path_remove(path); } diff --git a/src/sentry_transport.c b/src/sentry_transport.c index 6b63c5783..744570928 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -12,6 +12,7 @@ struct sentry_transport_s { size_t (*dump_func)(sentry_run_t *run, void *state); void *state; bool running; + bool can_retry; }; sentry_transport_t * @@ -147,3 +148,15 @@ sentry__transport_get_state(sentry_transport_t *transport) { return transport ? transport->state : NULL; } + +void +sentry__transport_set_can_retry(sentry_transport_t *transport, bool can_retry) +{ + transport->can_retry = can_retry; +} + +bool +sentry__transport_can_retry(sentry_transport_t *transport) +{ + return transport && transport->can_retry; +} diff --git a/src/sentry_transport.h b/src/sentry_transport.h index 036233284..ebb901ac6 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -57,4 +57,8 @@ size_t sentry__transport_dump_queue( void *sentry__transport_get_state(sentry_transport_t *transport); +void sentry__transport_set_can_retry( + sentry_transport_t *transport, bool can_retry); +bool sentry__transport_can_retry(sentry_transport_t *transport); + #endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 4b688177e..5ee48b7a7 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -385,6 +385,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) sentry_transport_set_flush_func(transport, http_transport_flush); sentry_transport_set_shutdown_func(transport, http_transport_shutdown); sentry__transport_set_dump_func(transport, http_dump_queue); + sentry__transport_set_can_retry(transport, true); return transport; } diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index f14c1645a..dc451ae92 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -953,7 +953,8 @@ def test_http_retry_with_cache_keep(cmake, httpserver): assert waiting.result assert len(list(retry_dir.glob("*.envelope"))) == 0 - assert len(list(cache_dir.glob("*.envelope"))) == 1 + cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") From 8fcdb4f7c6cc246d2448425c2e9fd9e915a9d532 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 21:19:40 +0100 Subject: [PATCH 022/103] ref(database): derive can_cache flag to skip cache dir creation early Move the retry-aware check before cache_dir creation so we avoid mkdir when the retry system handles persistence. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 7976ffc6b..0315c1484 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -240,8 +240,12 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) continue; } + bool can_cache = options->cache_keep + && (options->http_retries == 0 + || !sentry__transport_can_retry(options->transport)); + sentry_path_t *cache_dir = NULL; - if (options->cache_keep) { + if (can_cache) { cache_dir = sentry__path_join_str(options->database_path, "cache"); if (cache_dir) { sentry__path_create_dir_all(cache_dir); @@ -293,9 +297,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - bool can_retry = sentry__transport_can_retry(options->transport) - && options->http_retries > 0; - if (cache_dir && !can_retry) { + if (cache_dir) { sentry_path_t *cached_file = sentry__path_join_str( cache_dir, sentry__path_filename(file)); if (!cached_file From 551ad7464fd889263517aeb3ec748217048a2908 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 21:36:59 +0100 Subject: [PATCH 023/103] ref(retry): change send callback to envelope-based API The retry callback now receives a sentry_envelope_t and returns a status code. The retry system handles deserialization and file lifecycle internally, keeping path concerns out of the transport. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 148 +++++++++++++------------ src/sentry_retry.h | 10 +- src/transports/sentry_http_transport.c | 16 +-- tests/unit/test_retry.c | 83 ++++++++------ 4 files changed, 133 insertions(+), 124 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 0f145cbc2..6881880cb 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -62,39 +62,6 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } -static void -retry_poll_task(void *_retry, void *_state) -{ - (void)_state; - sentry_retry_t *retry = _retry; - if (sentry__retry_foreach( - retry, retry->startup_time, retry->send_cb, retry->send_data)) { - sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, - retry, SENTRY_RETRY_INTERVAL); - } - // subsequent polls use backoff instead of the startup time filter - retry->startup_time = 0; -} - -void -sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, - sentry_retry_send_func_t send_cb, void *send_data) -{ - retry->bgworker = bgworker; - retry->send_cb = send_cb; - retry->send_data = send_data; - sentry__bgworker_submit_delayed( - bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); -} - -void -sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) -{ - sentry__retry_write_envelope(retry, envelope); - sentry__bgworker_submit_delayed( - retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); -} - bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out) @@ -169,9 +136,48 @@ sentry__retry_write_envelope( } } +static bool +handle_result(sentry_retry_t *retry, const sentry_path_t *path, int status_code) +{ + const char *fname = sentry__path_filename(path); + uint64_t ts; + int count; + const char *uuid; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { + sentry__path_remove(path); + return false; + } + + if (status_code < 0 && count + 1 < retry->max_retries) { + sentry_path_t *new_path = sentry__retry_make_path( + retry, (uint64_t)time(NULL), count + 1, uuid); + if (new_path) { + sentry__path_rename(path, new_path); + sentry__path_free(new_path); + } + return true; + } + + if (count + 1 >= retry->max_retries && retry->cache_dir) { + char cache_name[46]; + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid); + sentry_path_t *dst + = sentry__path_join_str(retry->cache_dir, cache_name); + if (dst) { + sentry__path_rename(path, dst); + sentry__path_free(dst); + } else { + sentry__path_remove(path); + } + } else { + sentry__path_remove(path); + } + return false; +} + size_t -sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, - bool (*callback)(const sentry_path_t *path, void *data), void *data) +sentry__retry_send(sentry_retry_t *retry, uint64_t before, + sentry_retry_send_func_t send_cb, void *data) { sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); if (!piter) { @@ -225,8 +231,15 @@ sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, } for (size_t i = 0; i < eligible; i++) { - if (!callback(paths[i], data)) { - total--; + sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); + if (!envelope) { + sentry__path_remove(paths[i]); + } else { + int status_code = send_cb(envelope, data); + sentry_envelope_free(envelope); + if (!handle_result(retry, paths[i], status_code)) { + total--; + } } } @@ -237,42 +250,35 @@ sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, return total; } -bool -sentry__retry_handle_result( - sentry_retry_t *retry, const sentry_path_t *path, int status_code) +static void +retry_poll_task(void *_retry, void *_state) { - const char *fname = sentry__path_filename(path); - uint64_t ts; - int count; - const char *uuid; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { - sentry__path_remove(path); - return false; + (void)_state; + sentry_retry_t *retry = _retry; + if (sentry__retry_send( + retry, retry->startup_time, retry->send_cb, retry->send_data)) { + sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, + retry, SENTRY_RETRY_INTERVAL); } + // subsequent polls use backoff instead of the startup time filter + retry->startup_time = 0; +} - if (status_code < 0 && count + 1 < retry->max_retries) { - sentry_path_t *new_path = sentry__retry_make_path( - retry, (uint64_t)time(NULL), count + 1, uuid); - if (new_path) { - sentry__path_rename(path, new_path); - sentry__path_free(new_path); - } - return true; - } +void +sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, + sentry_retry_send_func_t send_cb, void *send_data) +{ + retry->bgworker = bgworker; + retry->send_cb = send_cb; + retry->send_data = send_data; + sentry__bgworker_submit_delayed( + bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); +} - if (count + 1 >= retry->max_retries && retry->cache_dir) { - char cache_name[46]; - snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid); - sentry_path_t *dst - = sentry__path_join_str(retry->cache_dir, cache_name); - if (dst) { - sentry__path_rename(path, dst); - sentry__path_free(dst); - } else { - sentry__path_remove(path); - } - } else { - sentry__path_remove(path); - } - return false; +void +sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) +{ + sentry__retry_write_envelope(retry, envelope); + sentry__bgworker_submit_delayed( + retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 665fa7564..a75045e67 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -7,7 +7,8 @@ typedef struct sentry_retry_s sentry_retry_t; -typedef bool (*sentry_retry_send_func_t)(const sentry_path_t *path, void *data); +typedef int (*sentry_retry_send_func_t)( + sentry_envelope_t *envelope, void *data); sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); @@ -21,11 +22,8 @@ void sentry__retry_enqueue( void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); -size_t sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, - bool (*callback)(const sentry_path_t *path, void *data), void *data); - -bool sentry__retry_handle_result( - sentry_retry_t *retry, const sentry_path_t *path, int status_code); +size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, + sentry_retry_send_func_t send_cb, void *data); uint64_t sentry__retry_backoff(int count); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 5ee48b7a7..1cb3ac42d 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -3,7 +3,6 @@ #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" -#include "sentry_path.h" #include "sentry_ratelimiter.h" #include "sentry_retry.h" #include "sentry_string.h" @@ -226,20 +225,11 @@ http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope) return status_code; } -static bool -retry_send_cb(const sentry_path_t *path, void *_state) +static int +retry_send_cb(sentry_envelope_t *envelope, void *_state) { http_transport_state_t *state = _state; - - sentry_envelope_t *envelope = sentry__envelope_from_path(path); - if (!envelope) { - sentry__path_remove(path); - return true; - } - - int status_code = http_send_envelope(state, envelope); - sentry_envelope_free(envelope); - return sentry__retry_handle_result(state->retry, path, status_code); + return http_send_envelope(state, envelope); } static void diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 8fb8c9042..eec3f67f7 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -46,6 +46,33 @@ find_envelope_attempt(const sentry_path_t *dir) return -1; } +static int +count_eligible_files(const sentry_path_t *dir, uint64_t before) +{ + int eligible = 0; + uint64_t now = before ? 0 : (uint64_t)time(NULL); + sentry_pathiter_t *iter = sentry__path_iter_directory(dir); + const sentry_path_t *file; + while (iter && (file = sentry__pathiter_next(iter)) != NULL) { + const char *name = sentry__path_filename(file); + uint64_t ts; + int count; + const char *uuid; + if (!sentry__retry_parse_filename(name, &ts, &count, &uuid)) { + continue; + } + if (before && ts >= before) { + continue; + } + if (!before && (now - ts) < sentry__retry_backoff(count)) { + continue; + } + eligible++; + } + sentry__pathiter_free(iter); + return eligible; +} + static void write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, const sentry_uuid_t *event_id) @@ -65,26 +92,17 @@ write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, } typedef struct { - sentry_retry_t *retry; int status_code; size_t count; } retry_test_ctx_t; -static bool -handle_result_cb(const sentry_path_t *path, void *_ctx) +static int +test_send_cb(sentry_envelope_t *envelope, void *_ctx) { + (void)envelope; retry_test_ctx_t *ctx = _ctx; ctx->count++; - sentry__retry_handle_result(ctx->retry, path, ctx->status_code); - return true; -} - -static bool -count_cb(const sentry_path_t *path, void *_count) -{ - (void)path; - (*(size_t *)_count)++; - return true; + return ctx->status_code; } SENTRY_TEST(retry_throttle) @@ -112,8 +130,8 @@ SENTRY_TEST(retry_throttle) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); - retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 4); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -147,22 +165,22 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); - retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 2. Rate limited (429) → removes write_retry_file(retry, old_ts, 0, &event_id); - ctx = (retry_test_ctx_t) { retry, 429, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + ctx = (retry_test_ctx_t) { 429, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 3. Discard (0) → removes write_retry_file(retry, old_ts, 0, &event_id); - ctx = (retry_test_ctx_t) { retry, 0, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + ctx = (retry_test_ctx_t) { 0, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -170,8 +188,8 @@ SENTRY_TEST(retry_result) write_retry_file(retry, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); - ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); @@ -180,8 +198,8 @@ SENTRY_TEST(retry_result) sentry__path_remove_all(retry_path); sentry__path_create_dir_all(retry_path); write_retry_file(retry, old_ts, 1, &event_id); - ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -257,8 +275,8 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // Network error on a file at count=4 with max_retries=5 → moves to cache - retry_test_ctx_t ctx = { retry, -1, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + retry_test_ctx_t ctx = { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -306,15 +324,12 @@ SENTRY_TEST(retry_backoff) sentry_uuid_t id4 = sentry_uuid_new_v4(); write_retry_file(retry, ref + 8 * base, 2, &id4); - // Startup scan (no backoff check): all 4 files returned - size_t count = 0; - sentry__retry_foreach(retry, (uint64_t)time(NULL), count_cb, &count); - TEST_CHECK_INT_EQUAL(count, 4); + // Startup scan (no backoff check): all 4 files + TEST_CHECK_INT_EQUAL( + count_eligible_files(retry_path, (uint64_t)time(NULL)), 4); // With backoff check: only eligible ones (id1 and id3) - count = 0; - sentry__retry_foreach(retry, 0, count_cb, &count); - TEST_CHECK_INT_EQUAL(count, 2); + TEST_CHECK_INT_EQUAL(count_eligible_files(retry_path, 0), 2); // Verify backoff calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); From d2f1a071779bec58baa2e7b53cdf15a198d9cb49 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:08:46 +0100 Subject: [PATCH 024/103] test(retry): verify cache_keep preserves envelopes on successful send Add test case for successful send at max retry count with cache_keep enabled, confirming envelopes are cached regardless of send outcome. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index eec3f67f7..603e93652 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -282,6 +282,20 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + // Success on a file at count=4 → also moves to cache (cache_keep + // preserves all envelopes regardless of send outcome) + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); + write_retry_file(retry, old_ts, 4, &event_id); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + + ctx = (retry_test_ctx_t) { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); + + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + sentry__retry_free(retry); sentry__path_free(retry_path); sentry__path_free(cache_path); From 3750856bf57eb1e2f4c482e3d28af7585c9c1dd7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:10:39 +0100 Subject: [PATCH 025/103] fix(retry): use PRIu64 format specifier for uint64_t Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6881880cb..eb2e25071 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -111,8 +111,8 @@ sentry__retry_make_path( sentry_retry_t *retry, uint64_t ts, int count, const char *uuid) { char filename[128]; - snprintf(filename, sizeof(filename), "%llu-%02d-%.36s.envelope", - (unsigned long long)ts, count, uuid); + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, + count, uuid); return sentry__path_join_str(retry->retry_dir, filename); } From 0cbf407aadf01ab31d6d4269e1357169bae1ea75 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:11:30 +0100 Subject: [PATCH 026/103] fix(retry): guard against unsigned underflow in backoff check Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index eb2e25071..77959f2a1 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -208,7 +208,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, continue; } total++; - if (!before && (now - ts) < sentry__retry_backoff(count)) { + if (!before && now >= ts && (now - ts) < sentry__retry_backoff(count)) { continue; } if (eligible == path_cap) { From 3b2a5d6b89dcdbf337da83c4e0c80285882fef3d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 13 Feb 2026 17:14:04 +0100 Subject: [PATCH 027/103] fix(retry): prevent startup poll from re-processing same-session envelopes The startup poll used `ts >= startup_time` to skip envelopes written after startup. With second-precision timestamps, this also skipped cross-session envelopes written in the same second as a fast restart. Reset `startup_time` in `sentry__retry_enqueue` so the startup poll falls through to the backoff path for same-session envelopes. The bgworker processes the send task (immediate) before the startup poll (delayed), so by the time the poll fires, `startup_time` is already 0. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 6 ++++-- tests/unit/test_retry.c | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 77959f2a1..393705913 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -193,7 +193,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, size_t total = 0; size_t eligible = 0; - uint64_t now = before ? 0 : (uint64_t)time(NULL); + uint64_t now = before > 0 ? 0 : (uint64_t)time(NULL); const sentry_path_t *p; while ((p = sentry__pathiter_next(piter)) != NULL) { @@ -204,7 +204,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { continue; } - if (before && ts >= before) { + if (before > 0 && ts >= before) { continue; } total++; @@ -279,6 +279,8 @@ void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { sentry__retry_write_envelope(retry, envelope); + // prevent the startup poll from re-processing this session's envelope + retry->startup_time = 0; sentry__bgworker_submit_delayed( retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 603e93652..7d29e6ba1 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -50,7 +50,7 @@ static int count_eligible_files(const sentry_path_t *dir, uint64_t before) { int eligible = 0; - uint64_t now = before ? 0 : (uint64_t)time(NULL); + uint64_t now = before > 0 ? 0 : (uint64_t)time(NULL); sentry_pathiter_t *iter = sentry__path_iter_directory(dir); const sentry_path_t *file; while (iter && (file = sentry__pathiter_next(iter)) != NULL) { @@ -61,7 +61,7 @@ count_eligible_files(const sentry_path_t *dir, uint64_t before) if (!sentry__retry_parse_filename(name, &ts, &count, &uuid)) { continue; } - if (before && ts >= before) { + if (before > 0 && ts >= before) { continue; } if (!before && (now - ts) < sentry__retry_backoff(count)) { From 6608084a4e1f5ff48db8781bc36ec93a3ce0ebfb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 10:40:22 +0100 Subject: [PATCH 028/103] fix(retry): flush pending retries on shutdown Submit a one-shot retry send task before bgworker shutdown to ensure pre-existing retry files are sent even if the startup poll hasn't fired yet. The flush checks startup_time on the worker thread to avoid re-sending files already handled by enqueue. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 19 +++++++++++++++++++ src/sentry_retry.h | 2 ++ src/transports/sentry_http_transport.c | 2 ++ 3 files changed, 23 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 393705913..f5b4bbf14 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -275,6 +275,25 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); } +static void +retry_flush_task(void *_retry, void *_state) +{ + (void)_state; + sentry_retry_t *retry = _retry; + if (retry->startup_time > 0) { + sentry__retry_send(retry, UINT64_MAX, retry->send_cb, retry->send_data); + retry->startup_time = 0; + } +} + +void +sentry__retry_flush(sentry_retry_t *retry) +{ + if (retry) { + sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); + } +} + void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index a75045e67..9518388bb 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -16,6 +16,8 @@ void sentry__retry_free(sentry_retry_t *retry); void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data); +void sentry__retry_flush(sentry_retry_t *retry); + void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 1cb3ac42d..b43885bf9 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -304,6 +304,8 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); + sentry__retry_flush(state->retry); + int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0 && state->shutdown_client) { state->shutdown_client(state->client); From 961ec95973791040444b23a3f42515210ff2e426 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 15:37:21 +0100 Subject: [PATCH 029/103] ref(retry): use millisecond timestamps for retry filenames Replace `time(NULL)` (1-second granularity) with `sentry__usec_time() / 1000` (millisecond granularity) to avoid timestamp collisions that caused flaky `>=` vs `>` comparison behavior in CI. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 11 +++++------ tests/unit/test_retry.c | 13 ++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index f5b4bbf14..d7c0bc477 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -6,7 +6,6 @@ #include #include -#include #define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) #define SENTRY_RETRY_THROTTLE 100 @@ -43,7 +42,7 @@ sentry__retry_new(const sentry_options_t *options) retry->retry_dir = retry_dir; retry->cache_dir = cache_dir; retry->max_retries = options->http_retries; - retry->startup_time = (uint64_t)time(NULL); + retry->startup_time = sentry__usec_time() / 1000; sentry__path_create_dir_all(retry->retry_dir); if (retry->cache_dir) { sentry__path_create_dir_all(retry->cache_dir); @@ -95,7 +94,7 @@ uint64_t sentry__retry_backoff(int count) { int shift = count < 3 ? count : 3; - return (uint64_t)(SENTRY_RETRY_INTERVAL / 1000) << shift; + return (uint64_t)SENTRY_RETRY_INTERVAL << shift; } static int @@ -129,7 +128,7 @@ sentry__retry_write_envelope( sentry_uuid_as_string(&event_id, uuid); sentry_path_t *path - = sentry__retry_make_path(retry, (uint64_t)time(NULL), 0, uuid); + = sentry__retry_make_path(retry, sentry__usec_time() / 1000, 0, uuid); if (path) { (void)sentry_envelope_write_to_path(envelope, path); sentry__path_free(path); @@ -150,7 +149,7 @@ handle_result(sentry_retry_t *retry, const sentry_path_t *path, int status_code) if (status_code < 0 && count + 1 < retry->max_retries) { sentry_path_t *new_path = sentry__retry_make_path( - retry, (uint64_t)time(NULL), count + 1, uuid); + retry, sentry__usec_time() / 1000, count + 1, uuid); if (new_path) { sentry__path_rename(path, new_path); sentry__path_free(new_path); @@ -193,7 +192,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, size_t total = 0; size_t eligible = 0; - uint64_t now = before > 0 ? 0 : (uint64_t)time(NULL); + uint64_t now = before > 0 ? 0 : sentry__usec_time() / 1000; const sentry_path_t *p; while ((p = sentry__pathiter_next(piter)) != NULL) { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 7d29e6ba1..ee3f89547 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -7,7 +7,6 @@ #include "sentry_uuid.h" #include -#include static int count_envelope_files(const sentry_path_t *dir) @@ -50,7 +49,7 @@ static int count_eligible_files(const sentry_path_t *dir, uint64_t before) { int eligible = 0; - uint64_t now = before > 0 ? 0 : (uint64_t)time(NULL); + uint64_t now = before > 0 ? 0 : sentry__usec_time() / 1000; sentry_pathiter_t *iter = sentry__path_iter_directory(dir); const sentry_path_t *file; while (iter && (file = sentry__pathiter_next(iter)) != NULL) { @@ -121,7 +120,7 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); + uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); @@ -157,7 +156,7 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); + uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir @@ -267,7 +266,7 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); + uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry, old_ts, 4, &event_id); @@ -320,7 +319,7 @@ SENTRY_TEST(retry_backoff) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); uint64_t base = sentry__retry_backoff(0); - uint64_t ref = (uint64_t)time(NULL) - 10 * base; + uint64_t ref = sentry__usec_time() / 1000 - 10 * base; // retry 0: 10*base old, eligible (backoff=base) sentry_uuid_t id1 = sentry_uuid_new_v4(); @@ -340,7 +339,7 @@ SENTRY_TEST(retry_backoff) // Startup scan (no backoff check): all 4 files TEST_CHECK_INT_EQUAL( - count_eligible_files(retry_path, (uint64_t)time(NULL)), 4); + count_eligible_files(retry_path, sentry__usec_time() / 1000), 4); // With backoff check: only eligible ones (id1 and id3) TEST_CHECK_INT_EQUAL(count_eligible_files(retry_path, 0), 2); From 558c32e85700fa8cbdd3a336b62cee61d9b0d69e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:16:03 +0100 Subject: [PATCH 030/103] fix(retry): flush pending retries synchronously before shutdown Make sentry__retry_flush block until the flush task completes by adding a bgworker_flush call, and subtract the elapsed time from the shutdown timeout. This ensures retries are actually sent before the worker stops. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 ++- src/sentry_retry.h | 2 +- src/transports/sentry_http_transport.c | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d7c0bc477..222294bc1 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -286,10 +286,11 @@ retry_flush_task(void *_retry, void *_state) } void -sentry__retry_flush(sentry_retry_t *retry) +sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) { if (retry) { sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); + sentry__bgworker_flush(retry->bgworker, timeout); } } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 9518388bb..c84e1760a 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -16,7 +16,7 @@ void sentry__retry_free(sentry_retry_t *retry); void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data); -void sentry__retry_flush(sentry_retry_t *retry); +void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index b43885bf9..d9cfae9ca 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -7,6 +7,7 @@ #include "sentry_retry.h" #include "sentry_string.h" #include "sentry_transport.h" +#include "sentry_utils.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" @@ -304,9 +305,12 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - sentry__retry_flush(state->retry); + uint64_t started = sentry__monotonic_time(); + sentry__retry_flush(state->retry, timeout); + uint64_t elapsed = sentry__monotonic_time() - started; + uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; - int rv = sentry__bgworker_shutdown(bgworker, timeout); + int rv = sentry__bgworker_shutdown(bgworker, remaining); if (rv != 0 && state->shutdown_client) { state->shutdown_client(state->client); } From be1d7cf2363dc666c578fba388c3cbc1eff7effd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:16:08 +0100 Subject: [PATCH 031/103] fix(retry): stop retrying on network failure Break out of the send loop on the first network error to avoid wasting time on a dead connection. Remaining envelopes stay untouched for the next retry poll. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 222294bc1..9b7730a7f 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -239,6 +239,11 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, if (!handle_result(retry, paths[i], status_code)) { total--; } + // stop on network failure to avoid wasting time on a dead + // connection; remaining envelopes stay untouched for later + if (status_code < 0) { + break; + } } } From fccc565934c3bb6cbe2cd96f39f52e4236ff49f7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:28:13 +0100 Subject: [PATCH 032/103] fix(retry): dump unsent envelopes to retry dir on shutdown timeout When bgworker shutdown times out, persist any remaining queued envelopes to the retry directory so they are not lost. The retry module provides sentry__retry_dump_queue to keep retry internals out of the transport. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 18 ++++++++++++++++++ src/sentry_retry.h | 3 +++ src/transports/sentry_http_transport.c | 7 +++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 9b7730a7f..dbc6ded05 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -299,6 +299,24 @@ sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) } } +static bool +retry_dump_cb(void *_envelope, void *_retry) +{ + sentry__retry_write_envelope( + (sentry_retry_t *)_retry, (sentry_envelope_t *)_envelope); + return true; +} + +void +sentry__retry_dump_queue( + sentry_retry_t *retry, sentry_task_exec_func_t task_func) +{ + if (retry) { + sentry__bgworker_foreach_matching( + retry->bgworker, task_func, retry_dump_cb, retry); + } +} + void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index c84e1760a..bd3b00ce4 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -18,6 +18,9 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); +void sentry__retry_dump_queue( + sentry_retry_t *retry, sentry_task_exec_func_t task_func); + void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index d9cfae9ca..bc1e7a782 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -311,8 +311,11 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; int rv = sentry__bgworker_shutdown(bgworker, remaining); - if (rv != 0 && state->shutdown_client) { - state->shutdown_client(state->client); + if (rv != 0) { + sentry__retry_dump_queue(state->retry, http_send_task); + if (state->shutdown_client) { + state->shutdown_client(state->client); + } } return rv; } From d8319877d054e43522b223221badef8fb71b3909 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:43:59 +0100 Subject: [PATCH 033/103] test(retry): update expectations for stop-on-failure behavior Co-Authored-By: Claude Opus 4.6 --- tests/test_integration_http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index dc451ae92..d5af9b69b 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1090,11 +1090,11 @@ def test_http_retry_multiple_network_error(cmake): env=env, ) - # all envelopes retried, all bumped to retry 1 + # first envelope retried and bumped, rest untouched (stop on failure) retry_files = list(retry_dir.glob("*.envelope")) assert len(retry_files) == 10 - retry_1 = [f for f in retry_files if "-01-" in f.name] - assert len(retry_1) == 10 + assert len([f for f in retry_files if "-00-" in f.name]) == 9 + assert len([f for f in retry_files if "-01-" in f.name]) == 1 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") From 69f4f687b2c797c5aa7f73eac3e3bc3f20bc2d3a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:52:03 +0100 Subject: [PATCH 034/103] style(retry): fix line length in unit tests Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index ee3f89547..ebaea5161 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -120,7 +120,8 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + uint64_t old_ts + = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); @@ -156,7 +157,8 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + uint64_t old_ts + = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir @@ -266,7 +268,8 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + uint64_t old_ts + = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry, old_ts, 4, &event_id); From 5562feb24839266202fc65c7d09db1138a687afe Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:58:35 +0100 Subject: [PATCH 035/103] fix(retry): prevent duplicate envelope writes from detached worker After shutdown timeout, the bgworker thread is detached but may still be executing an http_send_task. Since dump_queue already saves that task's envelope to the retry dir, the worker's subsequent call to retry_enqueue would create a duplicate file. Seal the retry module after dumping so that any late enqueue calls are silently skipped. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index dbc6ded05..a74968d6e 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -15,6 +15,7 @@ struct sentry_retry_s { sentry_path_t *cache_dir; int max_retries; uint64_t startup_time; + volatile long sealed; sentry_bgworker_t *bgworker; sentry_retry_send_func_t send_cb; void *send_data; @@ -43,6 +44,7 @@ sentry__retry_new(const sentry_options_t *options) retry->cache_dir = cache_dir; retry->max_retries = options->http_retries; retry->startup_time = sentry__usec_time() / 1000; + retry->sealed = 0; sentry__path_create_dir_all(retry->retry_dir); if (retry->cache_dir) { sentry__path_create_dir_all(retry->cache_dir); @@ -314,12 +316,17 @@ sentry__retry_dump_queue( if (retry) { sentry__bgworker_foreach_matching( retry->bgworker, task_func, retry_dump_cb, retry); + // prevent duplicate writes from a still-running detached worker + sentry__atomic_store(&retry->sealed, 1); } } void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { + if (sentry__atomic_fetch(&retry->sealed)) { + return; + } sentry__retry_write_envelope(retry, envelope); // prevent the startup poll from re-processing this session's envelope retry->startup_time = 0; From 0664fb3cada8def9f7a289f5cbcd473dc478452f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 17:45:36 +0100 Subject: [PATCH 036/103] docs: add changelog entry for HTTP retry feature Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 +++++- include/sentry.h | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad01ff29..98a94eb86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -## Unreleased: +## Unreleased + +**Features**: + +- Add HTTP retry with exponential backoff: `sentry_options_set_http_retries()`. ([#1520](https://github.com/getsentry/sentry-native/pull/1520)) **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index 23f202d7c..aede8a588 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2259,7 +2259,7 @@ SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( const sentry_options_t *opts); /** - * Sets the maximum number of HTTP retry attempts for transient network errors. + * Sets the maximum number of HTTP retry attempts for network failures. * Set to 0 to disable retries (default). */ SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retries( From c241adf549b7d5e5e454272c7cf9fa1d4a39d365 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 19:04:46 +0100 Subject: [PATCH 037/103] test(retry): use sentry__retry_send instead of duplicated eligibility logic Remove count_eligible_files helper that duplicated filtering logic from sentry__retry_send. The retry_backoff test now exercises the actual send path for both backoff and startup modes. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 42 ++++++++++------------------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index ebaea5161..a3cff011d 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -45,33 +45,6 @@ find_envelope_attempt(const sentry_path_t *dir) return -1; } -static int -count_eligible_files(const sentry_path_t *dir, uint64_t before) -{ - int eligible = 0; - uint64_t now = before > 0 ? 0 : sentry__usec_time() / 1000; - sentry_pathiter_t *iter = sentry__path_iter_directory(dir); - const sentry_path_t *file; - while (iter && (file = sentry__pathiter_next(iter)) != NULL) { - const char *name = sentry__path_filename(file); - uint64_t ts; - int count; - const char *uuid; - if (!sentry__retry_parse_filename(name, &ts, &count, &uuid)) { - continue; - } - if (before > 0 && ts >= before) { - continue; - } - if (!before && (now - ts) < sentry__retry_backoff(count)) { - continue; - } - eligible++; - } - sentry__pathiter_free(iter); - return eligible; -} - static void write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, const sentry_uuid_t *event_id) @@ -340,12 +313,17 @@ SENTRY_TEST(retry_backoff) sentry_uuid_t id4 = sentry_uuid_new_v4(); write_retry_file(retry, ref + 8 * base, 2, &id4); - // Startup scan (no backoff check): all 4 files - TEST_CHECK_INT_EQUAL( - count_eligible_files(retry_path, sentry__usec_time() / 1000), 4); + // With backoff: only eligible ones (id1 and id3) are sent + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 2); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 2); - // With backoff check: only eligible ones (id1 and id3) - TEST_CHECK_INT_EQUAL(count_eligible_files(retry_path, 0), 2); + // Startup scan (no backoff check): remaining 2 files are sent + ctx = (retry_test_ctx_t) { 200, 0 }; + sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 2); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // Verify backoff calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); From a053ccb46803acb8215ccb604a1c124bd5201b16 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 19:28:49 +0100 Subject: [PATCH 038/103] fix(retry): raise backoff cap from 2h to 8h to match crashpad Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 6 ++++-- tests/unit/test_retry.c | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index a74968d6e..8b92e9244 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -92,11 +92,13 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, return true; } +/** + * Exponential backoff: 15m, 30m, 1h, 2h, 4h, 8h, 8h, ... (capped at 8 hours) + */ uint64_t sentry__retry_backoff(int count) { - int shift = count < 3 ? count : 3; - return (uint64_t)SENTRY_RETRY_INTERVAL << shift; + return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(count, 5); } static int diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index a3cff011d..fe6721c8a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -241,8 +241,7 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts - = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry, old_ts, 4, &event_id); @@ -330,7 +329,9 @@ SENTRY_TEST(retry_backoff) TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(1), base * 2); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(2), base * 4); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(3), base * 8); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 8); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 16); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(5), base * 32); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(6), base * 32); sentry__retry_free(retry); sentry__path_free(retry_path); From 71ee17a1611641d51b3a7e0dff70679138890796 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 09:55:45 +0100 Subject: [PATCH 039/103] refactor(retry): introduce retry_item_t to avoid re-parsing filenames Store parsed fields (ts, count, uuid) alongside the path during the filter phase so handle_result and future debug logging can use them without re-parsing. Also improves sort performance by comparing numeric fields before falling back to string comparison. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 86 +++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 8b92e9244..df6d78612 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -101,12 +101,25 @@ sentry__retry_backoff(int count) return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(count, 5); } +typedef struct { + sentry_path_t *path; + uint64_t ts; + int count; + char uuid[37]; +} retry_item_t; + static int -compare_retry_paths(const void *a, const void *b) +compare_retry_items(const void *a, const void *b) { - const sentry_path_t *const *pa = a; - const sentry_path_t *const *pb = b; - return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); + const retry_item_t *ia = a; + const retry_item_t *ib = b; + if (ia->ts != ib->ts) { + return ia->ts < ib->ts ? -1 : 1; + } + if (ia->count != ib->count) { + return ia->count - ib->count; + } + return strcmp(ia->uuid, ib->uuid); } sentry_path_t * @@ -140,40 +153,31 @@ sentry__retry_write_envelope( } static bool -handle_result(sentry_retry_t *retry, const sentry_path_t *path, int status_code) +handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { - const char *fname = sentry__path_filename(path); - uint64_t ts; - int count; - const char *uuid; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { - sentry__path_remove(path); - return false; - } - - if (status_code < 0 && count + 1 < retry->max_retries) { + if (status_code < 0 && item->count + 1 < retry->max_retries) { sentry_path_t *new_path = sentry__retry_make_path( - retry, sentry__usec_time() / 1000, count + 1, uuid); + retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { - sentry__path_rename(path, new_path); + sentry__path_rename(item->path, new_path); sentry__path_free(new_path); } return true; } - if (count + 1 >= retry->max_retries && retry->cache_dir) { + if (item->count + 1 >= retry->max_retries && retry->cache_dir) { char cache_name[46]; - snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid); + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); sentry_path_t *dst = sentry__path_join_str(retry->cache_dir, cache_name); if (dst) { - sentry__path_rename(path, dst); + sentry__path_rename(item->path, dst); sentry__path_free(dst); } else { - sentry__path_remove(path); + sentry__path_remove(item->path); } } else { - sentry__path_remove(path); + sentry__path_remove(item->path); } return false; } @@ -187,9 +191,9 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, return 0; } - size_t path_cap = 16; - sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); - if (!paths) { + size_t item_cap = 16; + retry_item_t *items = sentry_malloc(item_cap * sizeof(retry_item_t)); + if (!items) { sentry__pathiter_free(piter); return 0; } @@ -214,33 +218,37 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, if (!before && now >= ts && (now - ts) < sentry__retry_backoff(count)) { continue; } - if (eligible == path_cap) { - path_cap *= 2; - sentry_path_t **tmp - = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (eligible == item_cap) { + item_cap *= 2; + retry_item_t *tmp = sentry_malloc(item_cap * sizeof(retry_item_t)); if (!tmp) { break; } - memcpy(tmp, paths, eligible * sizeof(sentry_path_t *)); - sentry_free(paths); - paths = tmp; + memcpy(tmp, items, eligible * sizeof(retry_item_t)); + sentry_free(items); + items = tmp; } - paths[eligible++] = sentry__path_clone(p); + retry_item_t *item = &items[eligible++]; + item->path = sentry__path_clone(p); + item->ts = ts; + item->count = count; + memcpy(item->uuid, uuid, 36); + item->uuid[36] = '\0'; } sentry__pathiter_free(piter); if (eligible > 1) { - qsort(paths, eligible, sizeof(sentry_path_t *), compare_retry_paths); + qsort(items, eligible, sizeof(retry_item_t), compare_retry_items); } for (size_t i = 0; i < eligible; i++) { - sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); + sentry_envelope_t *envelope = sentry__envelope_from_path(items[i].path); if (!envelope) { - sentry__path_remove(paths[i]); + sentry__path_remove(items[i].path); } else { int status_code = send_cb(envelope, data); sentry_envelope_free(envelope); - if (!handle_result(retry, paths[i], status_code)) { + if (!handle_result(retry, &items[i], status_code)) { total--; } // stop on network failure to avoid wasting time on a dead @@ -252,9 +260,9 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, } for (size_t i = 0; i < eligible; i++) { - sentry__path_free(paths[i]); + sentry__path_free(items[i].path); } - sentry_free(paths); + sentry_free(items); return total; } From dfe7c1f3986086114cc79d7a9c624e262f251bb0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 10:04:33 +0100 Subject: [PATCH 040/103] feat(retry): add debug and warning output for HTTP retries Log retry attempts at DEBUG level and max-retries-reached at WARN level to make retry behavior observable. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index df6d78612..82ccf0380 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -1,6 +1,7 @@ #include "sentry_retry.h" #include "sentry_alloc.h" #include "sentry_envelope.h" +#include "sentry_logger.h" #include "sentry_options.h" #include "sentry_utils.h" @@ -147,7 +148,10 @@ sentry__retry_write_envelope( sentry_path_t *path = sentry__retry_make_path(retry, sentry__usec_time() / 1000, 0, uuid); if (path) { - (void)sentry_envelope_write_to_path(envelope, path); + if (sentry_envelope_write_to_path(envelope, path) != 0) { + SENTRY_WARNF( + "failed to write retry envelope to \"%s\"", path->path); + } sentry__path_free(path); } } @@ -155,7 +159,9 @@ sentry__retry_write_envelope( static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { - if (status_code < 0 && item->count + 1 < retry->max_retries) { + bool exhausted = item->count + 1 >= retry->max_retries; + + if (status_code < 0 && !exhausted) { sentry_path_t *new_path = sentry__retry_make_path( retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { @@ -165,7 +171,9 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) return true; } - if (item->count + 1 >= retry->max_retries && retry->cache_dir) { + if (exhausted && retry->cache_dir) { + SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", + retry->max_retries); char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); sentry_path_t *dst @@ -177,6 +185,10 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) sentry__path_remove(item->path); } } else { + if (exhausted) { + SENTRY_WARNF("max retries (%d) reached, discarding envelope", + retry->max_retries); + } sentry__path_remove(item->path); } return false; @@ -246,6 +258,8 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, if (!envelope) { sentry__path_remove(items[i].path); } else { + SENTRY_DEBUGF("retrying envelope (%d/%d)", items[i].count + 1, + retry->max_retries); int status_code = send_cb(envelope, data); sentry_envelope_free(envelope); if (!handle_result(retry, &items[i], status_code)) { From 5d80da6bc89c55195259b2392b0e64640455186f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 11:08:31 +0100 Subject: [PATCH 041/103] refactor(cache): add cache_path to sentry_run_t and centralize cache writes Three places independently constructed /cache and wrote envelopes there. Add cache_path to sentry_run_t and introduce sentry__run_write_cache() and sentry__run_move_cache() to centralize the cache directory creation and file operations. Co-Authored-By: Claude Opus 4.6 --- src/backends/sentry_backend_crashpad.cpp | 8 +-- src/sentry_database.c | 87 ++++++++++++++++-------- src/sentry_database.h | 16 +++++ src/sentry_retry.c | 25 ++----- tests/unit/test_retry.c | 9 ++- 5 files changed, 92 insertions(+), 53 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 9815bdb42..ba7420734 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -565,11 +565,9 @@ process_completed_reports( SENTRY_DEBUGF("caching %zu completed reports", reports.size()); - sentry_path_t *cache_dir - = sentry__path_join_str(options->database_path, "cache"); - if (!cache_dir || sentry__path_create_dir_all(cache_dir) != 0) { + sentry_path_t *cache_dir = options->run->cache_path; + if (sentry__path_create_dir_all(cache_dir) != 0) { SENTRY_WARN("failed to create cache dir"); - sentry__path_free(cache_dir); return; } @@ -593,8 +591,6 @@ process_completed_reports( sentry__path_free(out_path); sentry_envelope_free(envelope); } - - sentry__path_free(cache_dir); } static int diff --git a/src/sentry_database.c b/src/sentry_database.c index 0315c1484..1d55278bd 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -51,12 +51,23 @@ sentry__run_new(const sentry_path_t *database_path) return NULL; } + // `/cache` + sentry_path_t *cache_path = sentry__path_join_str(database_path, "cache"); + if (!cache_path) { + sentry__path_free(run_path); + sentry__path_free(lock_path); + sentry__path_free(session_path); + sentry__path_free(external_path); + return NULL; + } + sentry_run_t *run = SENTRY_MAKE(sentry_run_t); if (!run) { sentry__path_free(run_path); sentry__path_free(session_path); sentry__path_free(lock_path); sentry__path_free(external_path); + sentry__path_free(cache_path); return NULL; } @@ -64,6 +75,7 @@ sentry__run_new(const sentry_path_t *database_path) run->run_path = run_path; run->session_path = session_path; run->external_path = external_path; + run->cache_path = cache_path; run->lock = sentry__filelock_new(lock_path); if (!run->lock) { goto error; @@ -97,12 +109,13 @@ sentry__run_free(sentry_run_t *run) sentry__path_free(run->run_path); sentry__path_free(run->session_path); sentry__path_free(run->external_path); + sentry__path_free(run->cache_path); sentry__filelock_free(run->lock); sentry_free(run); } static bool -write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) +write_envelope(const sentry_path_t *dir, const sentry_envelope_t *envelope) { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); @@ -112,24 +125,23 @@ write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) event_id = sentry_uuid_new_v4(); } - char *envelope_filename = sentry__uuid_as_filename(&event_id, ".envelope"); - if (!envelope_filename) { + char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); + if (!filename) { return false; } - sentry_path_t *output_path = sentry__path_join_str(path, envelope_filename); - sentry_free(envelope_filename); - if (!output_path) { + sentry_path_t *path = sentry__path_join_str(dir, filename); + sentry_free(filename); + if (!path) { return false; } - int rv = sentry_envelope_write_to_path(envelope, output_path); - sentry__path_free(output_path); + int rv = sentry_envelope_write_to_path(envelope, path); + sentry__path_free(path); if (rv) { SENTRY_WARN("writing envelope to file failed"); return false; } - return true; } @@ -148,10 +160,45 @@ sentry__run_write_external( SENTRY_ERRORF("mkdir failed: \"%s\"", run->external_path->path); return false; } - return write_envelope(run->external_path, envelope); } +bool +sentry__run_write_cache( + const sentry_run_t *run, const sentry_envelope_t *envelope) +{ + if (sentry__path_create_dir_all(run->cache_path) != 0) { + SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); + return false; + } + return write_envelope(run->cache_path, envelope); +} + +bool +sentry__run_move_cache( + const sentry_run_t *run, const sentry_path_t *src, const char *dst) +{ + if (sentry__path_create_dir_all(run->cache_path) != 0) { + SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); + return false; + } + + const char *filename = dst ? dst : sentry__path_filename(src); + sentry_path_t *dst_path = sentry__path_join_str(run->cache_path, filename); + if (!dst_path) { + return false; + } + + int rv = sentry__path_rename(src, dst_path); + sentry__path_free(dst_path); + if (rv != 0) { + SENTRY_WARNF( + "failed to cache envelope \"%s\"", sentry__path_filename(src)); + return false; + } + return true; +} + bool sentry__run_write_session( const sentry_run_t *run, const sentry_session_t *session) @@ -244,14 +291,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) && (options->http_retries == 0 || !sentry__transport_can_retry(options->transport)); - sentry_path_t *cache_dir = NULL; - if (can_cache) { - cache_dir = sentry__path_join_str(options->database_path, "cache"); - if (cache_dir) { - sentry__path_create_dir_all(cache_dir); - } - } - sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) { @@ -297,15 +336,8 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - if (cache_dir) { - sentry_path_t *cached_file = sentry__path_join_str( - cache_dir, sentry__path_filename(file)); - if (!cached_file - || sentry__path_rename(file, cached_file) != 0) { - SENTRY_WARNF("failed to cache envelope \"%s\"", - sentry__path_filename(file)); - } - sentry__path_free(cached_file); + if (can_cache + && sentry__run_move_cache(options->run, file, NULL)) { continue; } } @@ -314,7 +346,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } sentry__pathiter_free(run_iter); - sentry__path_free(cache_dir); sentry__path_remove_all(run_dir); sentry__filelock_free(lock); } diff --git a/src/sentry_database.h b/src/sentry_database.h index c3fee8bc0..f92fff8f8 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -11,6 +11,7 @@ typedef struct sentry_run_s { sentry_path_t *run_path; sentry_path_t *session_path; sentry_path_t *external_path; + sentry_path_t *cache_path; sentry_filelock_t *lock; } sentry_run_t; @@ -63,6 +64,21 @@ bool sentry__run_write_session( */ bool sentry__run_clear_session(const sentry_run_t *run); +/** + * This will serialize and write the given envelope to disk into a file named + * like so: + * `/cache/.envelope` + */ +bool sentry__run_write_cache( + const sentry_run_t *run, const sentry_envelope_t *envelope); + +/** + * Moves `src` to `/cache/`. If `dst` is NULL, the filename of + * `src` is used. + */ +bool sentry__run_move_cache( + const sentry_run_t *run, const sentry_path_t *src, const char *dst); + /** * This function is essential to send crash reports from previous runs of the * program. diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 82ccf0380..b04a128ec 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -1,5 +1,6 @@ #include "sentry_retry.h" #include "sentry_alloc.h" +#include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_logger.h" #include "sentry_options.h" @@ -13,8 +14,9 @@ struct sentry_retry_s { sentry_path_t *retry_dir; - sentry_path_t *cache_dir; + const sentry_run_t *run; int max_retries; + bool cache_keep; uint64_t startup_time; volatile long sealed; sentry_bgworker_t *bgworker; @@ -30,26 +32,19 @@ sentry__retry_new(const sentry_options_t *options) if (!retry_dir) { return NULL; } - sentry_path_t *cache_dir = NULL; - if (options->cache_keep) { - cache_dir = sentry__path_join_str(options->database_path, "cache"); - } sentry_retry_t *retry = SENTRY_MAKE(sentry_retry_t); if (!retry) { - sentry__path_free(cache_dir); sentry__path_free(retry_dir); return NULL; } retry->retry_dir = retry_dir; - retry->cache_dir = cache_dir; + retry->run = options->run; retry->max_retries = options->http_retries; + retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; retry->sealed = 0; sentry__path_create_dir_all(retry->retry_dir); - if (retry->cache_dir) { - sentry__path_create_dir_all(retry->cache_dir); - } return retry; } @@ -60,7 +55,6 @@ sentry__retry_free(sentry_retry_t *retry) return; } sentry__path_free(retry->retry_dir); - sentry__path_free(retry->cache_dir); sentry_free(retry); } @@ -171,17 +165,12 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) return true; } - if (exhausted && retry->cache_dir) { + if (exhausted && retry->cache_keep && retry->run) { SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", retry->max_retries); char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); - sentry_path_t *dst - = sentry__path_join_str(retry->cache_dir, cache_name); - if (dst) { - sentry__path_rename(item->path, dst); - sentry__path_free(dst); - } else { + if (!sentry__run_move_cache(retry->run, item->path, cache_name)) { sentry__path_remove(item->path); } } else { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index fe6721c8a..6abcfa729 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -1,4 +1,6 @@ +#include "sentry_database.h" #include "sentry_envelope.h" +#include "sentry_options.h" #include "sentry_path.h" #include "sentry_retry.h" #include "sentry_session.h" @@ -228,14 +230,18 @@ SENTRY_TEST(retry_cache) sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache"); sentry__path_remove_all(db_path); + sentry__path_create_dir_all(db_path); + + sentry_run_t *run = sentry__run_new(db_path); + TEST_ASSERT(!!run); SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_database_path( options, SENTRY_TEST_PATH_PREFIX ".retry-cache"); sentry_options_set_http_retries(options, 5); sentry_options_set_cache_keep(options, 1); + options->run = run; sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); @@ -273,6 +279,7 @@ SENTRY_TEST(retry_cache) sentry__retry_free(retry); sentry__path_free(retry_path); sentry__path_free(cache_path); + sentry_options_free(options); sentry__path_remove_all(db_path); sentry__path_free(db_path); } From 06cca6691e03f0ee4047649dc0d1e5384b5721bd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:00:36 +0100 Subject: [PATCH 042/103] fix(transport): use connect-only timeouts for curl and winhttp CURLOPT_TIMEOUT_MS is a total transfer timeout that could cut off large envelopes. Use CURLOPT_CONNECTTIMEOUT_MS instead so only connection establishment is bounded. For winhttp, limit resolve and connect to 15s but leave send/receive at their defaults. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_curl.c | 2 +- src/transports/sentry_http_transport_winhttp.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 969f86925..eec48f8e4 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -189,7 +189,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 15000L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 15000L); char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index e3f003a18..fa18ac8c3 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -134,7 +134,8 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 1; } - WinHttpSetTimeouts(client->session, 15000, 15000, 15000, 15000); + // 15s resolve, 15s connect, default send/receive + WinHttpSetTimeouts(client->session, 15000, 15000, 0, 0); return 0; } From fd6c5779d2c30f9e3f91fd01fa15f2a39211cbd6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:03:26 +0100 Subject: [PATCH 043/103] fix(retry): decrement total count when removing corrupt envelope files Without this, sentry__retry_send overcounts remaining files, causing an unnecessary extra poll cycle. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index b04a128ec..f8615e27f 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -246,6 +246,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_envelope_t *envelope = sentry__envelope_from_path(items[i].path); if (!envelope) { sentry__path_remove(items[i].path); + total--; } else { SENTRY_DEBUGF("retrying envelope (%d/%d)", items[i].count + 1, retry->max_retries); From 173de803f2d97b23e66c886dffb5dc5efee79450 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:25:39 +0100 Subject: [PATCH 044/103] fix(retry): only warn about exhausted retries on network failure Restructure handle_result so "max retries reached" warnings only fire on actual network failures, not on successful delivery at the last attempt. Separate the warning logic from the cache/discard actions and put the re-enqueue branch first for clarity. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index f8615e27f..07b54e8dd 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -153,9 +153,8 @@ sentry__retry_write_envelope( static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { - bool exhausted = item->count + 1 >= retry->max_retries; - - if (status_code < 0 && !exhausted) { + // network failure with retries remaining: bump count & re-enqueue + if (item->count + 1 < retry->max_retries && status_code < 0) { sentry_path_t *new_path = sentry__retry_make_path( retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { @@ -165,21 +164,30 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) return true; } - if (exhausted && retry->cache_keep && retry->run) { - SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", - retry->max_retries); + bool exhausted = item->count + 1 >= retry->max_retries; + + // network failure with retries exhausted + if (exhausted && status_code < 0) { + if (retry->cache_keep) { + SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", + retry->max_retries); + } else { + SENTRY_WARNF("max retries (%d) reached, discarding envelope", + retry->max_retries); + } + } + + // cache on last attempt + if (exhausted && retry->cache_keep) { char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); if (!sentry__run_move_cache(retry->run, item->path, cache_name)) { sentry__path_remove(item->path); } - } else { - if (exhausted) { - SENTRY_WARNF("max retries (%d) reached, discarding envelope", - retry->max_retries); - } - sentry__path_remove(item->path); + return false; } + + sentry__path_remove(item->path); return false; } From 0482bf40f5074ecff82ee266da6c21803ae922dd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:53:01 +0100 Subject: [PATCH 045/103] docs(retry): add doc comments to sentry_retry.h declarations Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 --- src/sentry_retry.h | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 07b54e8dd..4d8e696ca 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -87,9 +87,6 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, return true; } -/** - * Exponential backoff: 15m, 30m, 1h, 2h, 4h, 8h, 8h, ... (capped at 8 hours) - */ uint64_t sentry__retry_backoff(int count) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index bd3b00ce4..12191e09e 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -13,23 +13,46 @@ typedef int (*sentry_retry_send_func_t)( sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); +/** + * Schedules retry polling on `bgworker` using `send_cb`. + */ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data); +/** + * Flushes unprocessed previous-session retries. No-op if already polled. + */ void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); +/** + * Dumps queued envelopes to the retry dir and seals against further writes. + */ void sentry__retry_dump_queue( sentry_retry_t *retry, sentry_task_exec_func_t task_func); +/** + * Writes a failed envelope to the retry dir and schedules a delayed poll. + */ void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); +/** + * Writes an event envelope to the retry dir. Non-event envelopes are skipped. + */ void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); +/** + * Sends eligible retry files via `send_cb`. `before > 0`: send files with + * ts < before (startup). `before == 0`: use backoff. Returns remaining file + * count for controlling polling. + */ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_retry_send_func_t send_cb, void *data); +/** + * Exponential backoff: 15m, 30m, 1h, 2h, 4h, 8h, 8h, ... (capped at 8h). + */ uint64_t sentry__retry_backoff(int count); /** From 39f36ec50ce7518d5d65cdc5a2636bf124fdf1b4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 13:54:06 +0100 Subject: [PATCH 046/103] feat(transport): add sentry_transport_retry() Replace the `can_retry` bool on the transport with a `retry_func` callback, and expose `sentry_transport_retry()` as an experimental public API for explicitly retrying all pending envelopes, e.g. when coming back online. Co-Authored-By: Claude Opus 4.6 --- include/sentry.h | 8 ++++++ src/sentry_retry.c | 17 ++++++++++++ src/sentry_retry.h | 5 ++++ src/sentry_transport.c | 17 +++++++++--- src/sentry_transport.h | 4 +-- src/transports/sentry_http_transport.c | 12 +++++++- tests/unit/test_retry.c | 38 ++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 8 files changed, 95 insertions(+), 7 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index aede8a588..5e7e8427b 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -946,6 +946,14 @@ SENTRY_API void sentry_transport_set_shutdown_func( sentry_transport_t *transport, int (*shutdown_func)(uint64_t timeout, void *state)); +/** + * Retries sending all pending envelopes in the transport's retry queue, + * e.g. when coming back online. Only applicable for HTTP transports with + * retries enabled via `sentry_options_set_http_retries`. + */ +SENTRY_EXPERIMENTAL_API void sentry_transport_retry( + sentry_transport_t *transport); + /** * Generic way to free transport. */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 4d8e696ca..e66c47d40 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -340,6 +340,23 @@ sentry__retry_dump_queue( } } +static void +retry_trigger_task(void *_retry, void *_state) +{ + (void)_state; + sentry_retry_t *retry = _retry; + if (sentry__retry_send( + retry, UINT64_MAX, retry->send_cb, retry->send_data)) { + sentry__retry_trigger(retry); + } +} + +void +sentry__retry_trigger(sentry_retry_t *retry) +{ + sentry__bgworker_submit(retry->bgworker, retry_trigger_task, NULL, retry); +} + void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 12191e09e..609e6dd0e 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -67,4 +67,9 @@ sentry_path_t *sentry__retry_make_path( bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out); +/** + * Submits a delayed retry poll task on the background worker. + */ +void sentry__retry_trigger(sentry_retry_t *retry); + #endif diff --git a/src/sentry_transport.c b/src/sentry_transport.c index 744570928..1b81cb652 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -10,9 +10,9 @@ struct sentry_transport_s { int (*flush_func)(uint64_t timeout, void *state); void (*free_func)(void *state); size_t (*dump_func)(sentry_run_t *run, void *state); + void (*retry_func)(void *state); void *state; bool running; - bool can_retry; }; sentry_transport_t * @@ -150,13 +150,22 @@ sentry__transport_get_state(sentry_transport_t *transport) } void -sentry__transport_set_can_retry(sentry_transport_t *transport, bool can_retry) +sentry_transport_retry(sentry_transport_t *transport) { - transport->can_retry = can_retry; + if (transport && transport->retry_func) { + transport->retry_func(transport->state); + } +} + +void +sentry__transport_set_retry_func( + sentry_transport_t *transport, void (*retry_func)(void *state)) +{ + transport->retry_func = retry_func; } bool sentry__transport_can_retry(sentry_transport_t *transport) { - return transport && transport->can_retry; + return transport && transport->retry_func; } diff --git a/src/sentry_transport.h b/src/sentry_transport.h index ebb901ac6..5ed1e7b81 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -57,8 +57,8 @@ size_t sentry__transport_dump_queue( void *sentry__transport_get_state(sentry_transport_t *transport); -void sentry__transport_set_can_retry( - sentry_transport_t *transport, bool can_retry); +void sentry__transport_set_retry_func( + sentry_transport_t *transport, void (*retry_func)(void *state)); bool sentry__transport_can_retry(sentry_transport_t *transport); #endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index bc1e7a782..04249a109 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -351,6 +351,16 @@ http_transport_get_state(sentry_transport_t *transport) return sentry__bgworker_get_state(bgworker); } +static void +http_transport_retry(void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + http_transport_state_t *state = sentry__bgworker_get_state(bgworker); + if (state->retry) { + sentry__retry_trigger(state->retry); + } +} + sentry_transport_t * sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) { @@ -384,7 +394,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) sentry_transport_set_flush_func(transport, http_transport_flush); sentry_transport_set_shutdown_func(transport, http_transport_shutdown); sentry__transport_set_dump_func(transport, http_dump_queue); - sentry__transport_set_can_retry(transport, true); + sentry__transport_set_retry_func(transport, http_transport_retry); return transport; } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 6abcfa729..738fa994d 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -5,6 +5,7 @@ #include "sentry_retry.h" #include "sentry_session.h" #include "sentry_testsupport.h" +#include "sentry_transport.h" #include "sentry_utils.h" #include "sentry_uuid.h" @@ -284,6 +285,43 @@ SENTRY_TEST(retry_cache) sentry__path_free(db_path); } +static int retry_func_calls = 0; + +static void +mock_retry_func(void *state) +{ + (void)state; + retry_func_calls++; +} + +static void +noop_send(sentry_envelope_t *envelope, void *state) +{ + (void)state; + sentry_envelope_free(envelope); +} + +SENTRY_TEST(transport_retry) +{ + // no retry_func → no-op + sentry_transport_t *transport = sentry_transport_new(noop_send); + TEST_CHECK(!sentry__transport_can_retry(transport)); + sentry_transport_retry(transport); + + // with retry_func → calls it + retry_func_calls = 0; + sentry__transport_set_retry_func(transport, mock_retry_func); + TEST_CHECK(sentry__transport_can_retry(transport)); + sentry_transport_retry(transport); + TEST_CHECK_INT_EQUAL(retry_func_calls, 1); + + // NULL transport → no-op + sentry_transport_retry(NULL); + TEST_CHECK_INT_EQUAL(retry_func_calls, 1); + + sentry_transport_free(transport); +} + SENTRY_TEST(retry_backoff) { sentry_path_t *db_path diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 537510365..9619585da 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -245,6 +245,7 @@ XX(traceparent_header_disabled_by_default) XX(traceparent_header_generation) XX(transaction_name_backfill_on_finish) XX(transactions_skip_before_send) +XX(transport_retry) XX(transport_sampling_transactions) XX(transport_sampling_transactions_set_trace) XX(txn_data) From 7b306885ff04fea9f5e2a0e41da9e39f1afe0289 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 17:37:37 +0100 Subject: [PATCH 047/103] refactor(retry): store retry envelopes in cache/ directory Move retry envelopes from a separate retry/ directory into cache/ so that sentry__cleanup_cache() enforces disk limits for both file formats out of the box. The two formats are distinguishable by length: retry files use --.envelope (49+ chars) while cache files use .envelope (45 chars). Default http_retries to 0 (opt-in). Co-Authored-By: Claude Opus 4.6 --- src/sentry_core.c | 3 +- src/sentry_options.h | 2 +- src/sentry_retry.c | 23 ++--- src/sentry_retry.h | 2 +- tests/test_integration_cache.py | 54 +++++++++++ tests/test_integration_http.py | 101 +++++++++----------- tests/unit/test_cache.c | 64 +++++++++++++ tests/unit/test_retry.c | 160 +++++++++++++------------------- tests/unit/tests.inc | 1 + 9 files changed, 245 insertions(+), 165 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index 3b4b38bfb..3522f935a 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -292,7 +292,8 @@ sentry_init(sentry_options_t *options) backend->prune_database_func(backend); } - if (options->cache_keep) { + if (options->cache_keep + || sentry__transport_can_retry(options->transport)) { sentry__cleanup_cache(options); } diff --git a/src/sentry_options.h b/src/sentry_options.h index 1c9a96f45..77209314b 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -72,13 +72,13 @@ struct sentry_options_s { void *traces_sampler_data; size_t max_spans; bool enable_logs; - int http_retries; // takes the first varg as a `sentry_value_t` object containing attributes // if no custom attributes are to be passed, use `sentry_value_new_object()` bool logs_with_attributes; bool enable_metrics; sentry_before_send_metric_function_t before_send_metric_func; void *before_send_metric_data; + int http_retries; /* everything from here on down are options which are stored here but not exposed through the options API */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index e66c47d40..6eda36524 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -13,7 +13,6 @@ #define SENTRY_RETRY_THROTTLE 100 struct sentry_retry_s { - sentry_path_t *retry_dir; const sentry_run_t *run; int max_retries; bool cache_keep; @@ -27,24 +26,16 @@ struct sentry_retry_s { sentry_retry_t * sentry__retry_new(const sentry_options_t *options) { - sentry_path_t *retry_dir - = sentry__path_join_str(options->database_path, "retry"); - if (!retry_dir) { - return NULL; - } - sentry_retry_t *retry = SENTRY_MAKE(sentry_retry_t); if (!retry) { - sentry__path_free(retry_dir); return NULL; } - retry->retry_dir = retry_dir; retry->run = options->run; retry->max_retries = options->http_retries; retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; retry->sealed = 0; - sentry__path_create_dir_all(retry->retry_dir); + sentry__path_create_dir_all(options->run->cache_path); return retry; } @@ -54,7 +45,6 @@ sentry__retry_free(sentry_retry_t *retry) if (!retry) { return; } - sentry__path_free(retry->retry_dir); sentry_free(retry); } @@ -62,6 +52,12 @@ bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out) { + // Minimum retry filename: --.envelope (49+ chars). + // Cache filenames are exactly 45 chars (.envelope). + if (strlen(filename) <= 45) { + return false; + } + char *end; uint64_t ts = strtoull(filename, &end, 10); if (*end != '-') { @@ -121,7 +117,7 @@ sentry__retry_make_path( char filename[128]; snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, count, uuid); - return sentry__path_join_str(retry->retry_dir, filename); + return sentry__path_join_str(retry->run->cache_path, filename); } void @@ -192,7 +188,8 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_retry_send_func_t send_cb, void *data) { - sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); + sentry_pathiter_t *piter + = sentry__path_iter_directory(retry->run->cache_path); if (!piter) { return 0; } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 609e6dd0e..12008259e 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -56,7 +56,7 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, uint64_t sentry__retry_backoff(int count); /** - * /retry/--.envelope + * /cache/--.envelope */ sentry_path_t *sentry__retry_make_path( sentry_retry_t *retry, uint64_t ts, int count, const char *uuid); diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index aff10fa9f..354d12c11 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -179,3 +179,57 @@ def test_cache_max_items(cmake, backend): assert cache_dir.exists() cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 5 + + +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + ], +) +def test_cache_max_items_with_retry(cmake, backend): + tmp_path = cmake( + ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} + ) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + # Create cache files via crash+restart cycles + for i in range(4): + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "crash"], + expect_failure=True, + ) + + # Move envelopes into cache + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "no-setup"], + ) + + # Pre-populate cache/ with retry-format envelope files + cache_dir.mkdir(parents=True, exist_ok=True) + for i in range(4): + ts = int(time.time() * 1000) + f = cache_dir / f"{ts}-00-00000000-0000-0000-0000-{i:012x}.envelope" + f.write_text("dummy envelope content") + + # Trigger sentry_init which runs cleanup + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "http-retry", "no-setup"], + ) + + # max 5 items total in cache/ + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) <= 5 diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index d5af9b69b..509964593 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -843,7 +843,7 @@ def test_native_crash_http(cmake, httpserver): @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_on_network_error(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") # unreachable port triggers CURLE_COULDNT_CONNECT unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" @@ -856,10 +856,10 @@ def test_http_retry_on_network_error(cmake, httpserver): env=env_unreachable, ) - assert retry_dir.exists() - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 1 - assert "-00-" in str(retry_files[0].name) + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-00-" in str(cache_files[0].name) # retry on next run with working server env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) @@ -878,53 +878,48 @@ def test_http_retry_on_network_error(cmake, httpserver): envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) assert_meta(envelope, integration="inproc") - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_multiple_attempts(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) run(tmp_path, "sentry_example", ["log", "http-retry", "capture-event"], env=env) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 1 - assert "-00-" in str(retry_files[0].name) + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-00-" in str(cache_files[0].name) run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 1 - assert "-01-" in str(retry_files[0].name) + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-01-" in str(cache_files[0].name) run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 1 - assert "-02-" in str(retry_files[0].name) + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-02-" in str(cache_files[0].name) # exhaust remaining retries (max 5) for i in range(3): run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) # discarded after max retries (cache_keep not enabled) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 0 - - cache_dir = tmp_path.joinpath(".sentry-native/cache") - cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_with_cache_keep(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" @@ -937,8 +932,8 @@ def test_http_retry_with_cache_keep(cmake, httpserver): env=env_unreachable, ) - assert retry_dir.exists() - assert len(list(retry_dir.glob("*.envelope"))) == 1 + assert cache_dir.exists() + assert len(list(cache_dir.glob("*.envelope"))) == 1 env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") @@ -952,15 +947,12 @@ def test_http_retry_with_cache_keep(cmake, httpserver): ) assert waiting.result - assert len(list(retry_dir.glob("*.envelope"))) == 0 - cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] - assert len(cache_files) == 0 + assert len(list(cache_dir.glob("*.envelope"))) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_cache_keep_max_attempts(cmake): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" @@ -973,8 +965,8 @@ def test_http_retry_cache_keep_max_attempts(cmake): env=env, ) - assert retry_dir.exists() - assert len(list(retry_dir.glob("*.envelope"))) == 1 + assert cache_dir.exists() + assert len(list(cache_dir.glob("*.envelope"))) == 1 for _ in range(5): run( @@ -984,7 +976,6 @@ def test_http_retry_cache_keep_max_attempts(cmake): env=env, ) - assert len(list(retry_dir.glob("*.envelope"))) == 0 assert cache_dir.exists() assert len(list(cache_dir.glob("*.envelope"))) == 1 @@ -992,7 +983,7 @@ def test_http_retry_cache_keep_max_attempts(cmake): @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_http_error_discards_envelope(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( @@ -1004,14 +995,14 @@ def test_http_retry_http_error_discards_envelope(cmake, httpserver): assert waiting.result # HTTP errors discard, not retry - retry_files = list(retry_dir.glob("*.envelope")) if retry_dir.exists() else [] - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_rate_limit_discards_envelope(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( @@ -1023,14 +1014,14 @@ def test_http_retry_rate_limit_discards_envelope(cmake, httpserver): assert waiting.result # 429 discards, not retry - retry_files = list(retry_dir.glob("*.envelope")) if retry_dir.exists() else [] - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_multiple_success(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) @@ -1042,8 +1033,8 @@ def test_http_retry_multiple_success(cmake, httpserver): env=env_unreachable, ) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 10 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 10 env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) for _ in range(10): @@ -1061,14 +1052,14 @@ def test_http_retry_multiple_success(cmake, httpserver): assert waiting.result assert len(httpserver.log) == 10 - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_multiple_network_error(cmake): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) @@ -1080,8 +1071,8 @@ def test_http_retry_multiple_network_error(cmake): env=env, ) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 10 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 10 run( tmp_path, @@ -1091,16 +1082,16 @@ def test_http_retry_multiple_network_error(cmake): ) # first envelope retried and bumped, rest untouched (stop on failure) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 10 - assert len([f for f in retry_files if "-00-" in f.name]) == 9 - assert len([f for f in retry_files if "-01-" in f.name]) == 1 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 10 + assert len([f for f in cache_files if "-00-" in f.name]) == 9 + assert len([f for f in cache_files if "-01-" in f.name]) == 1 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_multiple_rate_limit(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) @@ -1112,8 +1103,8 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): env=env_unreachable, ) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 10 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 10 # rate limit response followed by discards for the rest (rate limiter # kicks in after the first 429) @@ -1130,5 +1121,5 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): ) # first envelope gets 429, rest are discarded by rate limiter - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 0 diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index e161340bc..abfd73d78 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -46,6 +46,7 @@ SENTRY_TEST(cache_keep) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); + sentry_options_set_http_retries(options, 0); sentry_init(options); sentry_path_t *cache_path @@ -243,6 +244,69 @@ SENTRY_TEST(cache_max_items) sentry_close(); } +SENTRY_TEST(cache_max_items_with_retry) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#endif + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_cache_keep(options, true); + sentry_options_set_cache_max_items(options, 7); + sentry_init(options); + + sentry_path_t *cache_path + = sentry__path_join_str(options->database_path, "cache"); + TEST_ASSERT(!!cache_path); + TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(cache_path) == 0); + + time_t now = time(NULL); + + // 5 cache-format files: 1,3,5,7,9 min old + for (int i = 0; i < 5; i++) { + sentry_uuid_t event_id = sentry_uuid_new_v4(); + char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); + TEST_ASSERT(!!filename); + sentry_path_t *filepath = sentry__path_join_str(cache_path, filename); + sentry_free(filename); + + TEST_ASSERT(sentry__path_touch(filepath) == 0); + TEST_ASSERT(set_file_mtime(filepath, now - ((i * 2 + 1) * 60)) == 0); + sentry__path_free(filepath); + } + + // 5 retry-format files: 0,2,4,6,8 min old + for (int i = 0; i < 5; i++) { + sentry_uuid_t event_id = sentry_uuid_new_v4(); + char uuid[37]; + sentry_uuid_as_string(&event_id, uuid); + char filename[128]; + snprintf(filename, sizeof(filename), "%" PRIu64 "-00-%.36s.envelope", + (uint64_t)now, uuid); + sentry_path_t *filepath = sentry__path_join_str(cache_path, filename); + + TEST_ASSERT(sentry__path_touch(filepath) == 0); + TEST_ASSERT(set_file_mtime(filepath, now - (i * 2 * 60)) == 0); + sentry__path_free(filepath); + } + + sentry__cleanup_cache(options); + + int total_count = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(cache_path); + const sentry_path_t *entry; + while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { + total_count++; + } + sentry__pathiter_free(iter); + + TEST_CHECK_INT_EQUAL(total_count, 7); + + sentry__path_free(cache_path); + sentry_close(); +} + SENTRY_TEST(cache_max_size_and_age) { #if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 738fa994d..05479ee6a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -82,19 +82,16 @@ test_send_cb(sentry_envelope_t *envelope, void *_ctx) SENTRY_TEST(retry_throttle) { - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-throttle"); - sentry__path_remove_all(db_path); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-throttle"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_http_retries(options, 5); + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry__path_remove_all(options->run->cache_path); + sentry__path_create_dir_all(options->run->cache_path); uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); @@ -104,108 +101,95 @@ SENTRY_TEST(retry_throttle) write_retry_file(retry, old_ts, 0, &ids[i]); } - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); + TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 4); retry_test_ctx_t ctx = { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 4); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 0); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); + sentry_close(); } SENTRY_TEST(retry_result) { - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-result"); - sentry__path_remove_all(db_path); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-result"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_http_retries(options, 2); + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); - // 1. Success (200) → removes from retry dir + // 1. Success (200) → removes write_retry_file(retry, old_ts, 0, &event_id); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 0); retry_test_ctx_t ctx = { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 2. Rate limited (429) → removes write_retry_file(retry, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { 429, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 3. Discard (0) → removes write_retry_file(retry, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { 0, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 4. Network error → bumps count write_retry_file(retry, old_ts, 0, &event_id); - TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 0); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 1); // 5. Network error at max count → exceeds max_retries=2, removed - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); write_retry_file(retry, old_ts, 1, &event_id); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); + sentry_close(); } SENTRY_TEST(retry_session) { - SENTRY_TEST_OPTIONS_NEW(init_options); - sentry_options_set_dsn(init_options, "https://foo@sentry.invalid/42"); - sentry_options_set_release(init_options, "test@1.0.0"); - sentry_init(init_options); - - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-session"); - sentry__path_remove_all(db_path); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-session"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_release(options, "test@1.0.0"); sentry_options_set_http_retries(options, 2); + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry__path_remove_all(options->run->cache_path); + sentry__path_create_dir_all(options->run->cache_path); sentry_session_t *session = sentry__session_new(); TEST_ASSERT(!!session); @@ -215,74 +199,66 @@ SENTRY_TEST(retry_session) // Session-only envelopes have no event_id → should not be written sentry__retry_write_envelope(retry, envelope); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 0); sentry_envelope_free(envelope); sentry__session_free(session); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); sentry_close(); } SENTRY_TEST(retry_cache) { - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache"); - sentry__path_remove_all(db_path); - sentry__path_create_dir_all(db_path); - - sentry_run_t *run = sentry__run_new(db_path); - TEST_ASSERT(!!run); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-cache"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_http_retries(options, 5); sentry_options_set_cache_keep(options, 1); - options->run = run; + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry, old_ts, 4, &event_id); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); + char uuid_str[37]; + sentry_uuid_as_string(&event_id, uuid_str); + char cache_name[46]; + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid_str); + sentry_path_t *cached = sentry__path_join_str(cache_path, cache_name); + + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK(!sentry__path_is_file(cached)); - // Network error on a file at count=4 with max_retries=5 → moves to cache + // Network error on a file at count=4 with max_retries=5 → renames to + // cache format (.envelope) retry_test_ctx_t ctx = { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK(sentry__path_is_file(cached)); - // Success on a file at count=4 → also moves to cache (cache_keep - // preserves all envelopes regardless of send outcome) + // Success on a file at count=4 → also renames to cache format + // (cache_keep preserves all envelopes regardless of send outcome) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); write_retry_file(retry, old_ts, 4, &event_id); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK(!sentry__path_is_file(cached)); ctx = (retry_test_ctx_t) { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK(sentry__path_is_file(cached)); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_free(cache_path); - sentry_options_free(options); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); + sentry__path_free(cached); + sentry_close(); } static int retry_func_calls = 0; @@ -324,19 +300,17 @@ SENTRY_TEST(transport_retry) SENTRY_TEST(retry_backoff) { - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-backoff"); - sentry__path_remove_all(db_path); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-backoff"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_http_retries(options, 5); + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); uint64_t base = sentry__retry_backoff(0); uint64_t ref = sentry__usec_time() / 1000 - 10 * base; @@ -361,13 +335,13 @@ SENTRY_TEST(retry_backoff) retry_test_ctx_t ctx = { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 2); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 2); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 2); // Startup scan (no backoff check): remaining 2 files are sent ctx = (retry_test_ctx_t) { 200, 0 }; sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 2); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // Verify backoff calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); @@ -379,7 +353,5 @@ SENTRY_TEST(retry_backoff) TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(6), base * 32); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); + sentry_close(); } diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 9619585da..710e02c87 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -43,6 +43,7 @@ XX(build_id_parser) XX(cache_keep) XX(cache_max_age) XX(cache_max_items) +XX(cache_max_items_with_retry) XX(cache_max_size) XX(cache_max_size_and_age) XX(capture_minidump_basic) From e6c8db4e10438646384ab49128b1af6f878b06b1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 09:51:52 +0100 Subject: [PATCH 048/103] fix(retry): own cache_path to prevent use-after-free on detached thread When bgworker is detached during shutdown timeout, retry_poll_task can access retry->run->cache_path after sentry_options_free frees the run. Clone the path so it outlives options and is freed with the bgworker. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6eda36524..60a01094c 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -13,7 +13,7 @@ #define SENTRY_RETRY_THROTTLE 100 struct sentry_retry_s { - const sentry_run_t *run; + sentry_path_t *cache_path; int max_retries; bool cache_keep; uint64_t startup_time; @@ -30,7 +30,7 @@ sentry__retry_new(const sentry_options_t *options) if (!retry) { return NULL; } - retry->run = options->run; + retry->cache_path = sentry__path_clone(options->run->cache_path); retry->max_retries = options->http_retries; retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; @@ -45,6 +45,7 @@ sentry__retry_free(sentry_retry_t *retry) if (!retry) { return; } + sentry__path_free(retry->cache_path); sentry_free(retry); } @@ -117,7 +118,7 @@ sentry__retry_make_path( char filename[128]; snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, count, uuid); - return sentry__path_join_str(retry->run->cache_path, filename); + return sentry__path_join_str(retry->cache_path, filename); } void @@ -174,9 +175,12 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) if (exhausted && retry->cache_keep) { char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); - if (!sentry__run_move_cache(retry->run, item->path, cache_name)) { + sentry_path_t *dest + = sentry__path_join_str(retry->cache_path, cache_name); + if (!dest || sentry__path_rename(item->path, dest) != 0) { sentry__path_remove(item->path); } + sentry__path_free(dest); return false; } @@ -188,8 +192,7 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_retry_send_func_t send_cb, void *data) { - sentry_pathiter_t *piter - = sentry__path_iter_directory(retry->run->cache_path); + sentry_pathiter_t *piter = sentry__path_iter_directory(retry->cache_path); if (!piter) { return 0; } From c5205751186d1f93533baa3639479e4178f31f9d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 09:33:34 +0100 Subject: [PATCH 049/103] fix(retry): don't consume shutdown timeout with bgworker flush The bgworker_flush in sentry__retry_flush would delay its flush_task by min(delayed_task_time, timeout) when a 15-minute delayed retry_poll_task existed. This consumed the entire shutdown timeout, leaving 0ms for bgworker_shutdown, which then detached the worker thread. On Windows, winhttp_client_shutdown would close handles still in use by the detached thread, causing a crash. The flush is unnecessary because retry_flush_task is an immediate task and bgworker_shutdown already processes all immediate tasks before the shutdown_task runs. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 +-- src/sentry_retry.h | 2 +- src/transports/sentry_http_transport.c | 7 ++----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 60a01094c..7123328c5 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -312,11 +312,10 @@ retry_flush_task(void *_retry, void *_state) } void -sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) +sentry__retry_flush(sentry_retry_t *retry) { if (retry) { sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); - sentry__bgworker_flush(retry->bgworker, timeout); } } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 12008259e..fc5ef7123 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -22,7 +22,7 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, /** * Flushes unprocessed previous-session retries. No-op if already polled. */ -void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); +void sentry__retry_flush(sentry_retry_t *retry); /** * Dumps queued envelopes to the retry dir and seals against further writes. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 04249a109..24710542e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -305,12 +305,9 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - uint64_t started = sentry__monotonic_time(); - sentry__retry_flush(state->retry, timeout); - uint64_t elapsed = sentry__monotonic_time() - started; - uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; + sentry__retry_flush(state->retry); - int rv = sentry__bgworker_shutdown(bgworker, remaining); + int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); if (state->shutdown_client) { From 79d17321cca53d92558d0aebc8b1a9545637284d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 10:24:17 +0100 Subject: [PATCH 050/103] fix(retry): flush in-flight retries before shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit removed bgworker_flush from retry_flush, which caused a race between WinHTTP connect timeout (~2s) and bgworker shutdown (2s). Restore the flush and pass the full timeout to both flush and shutdown — after flush drains in-flight work, shutdown completes near-instantly. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 ++- src/sentry_retry.h | 2 +- src/transports/sentry_http_transport.c | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 7123328c5..60a01094c 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -312,10 +312,11 @@ retry_flush_task(void *_retry, void *_state) } void -sentry__retry_flush(sentry_retry_t *retry) +sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) { if (retry) { sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); + sentry__bgworker_flush(retry->bgworker, timeout); } } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index fc5ef7123..12008259e 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -22,7 +22,7 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, /** * Flushes unprocessed previous-session retries. No-op if already polled. */ -void sentry__retry_flush(sentry_retry_t *retry); +void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); /** * Dumps queued envelopes to the retry dir and seals against further writes. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 24710542e..9827c2d3b 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -305,7 +305,8 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - sentry__retry_flush(state->retry); + // flush drains in-flight retries; shutdown is near-instant afterward + sentry__retry_flush(state->retry, timeout); int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0) { From 344dcc896d35281b8afead5b1d1c018cf11d87ba Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 13:33:53 +0100 Subject: [PATCH 051/103] refactor(retry): replace http_retries count with boolean http_retry Make retry count an internal constant (SENTRY_RETRY_ATTEMPTS = 5) and expose only a boolean toggle. Enabled by default. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- examples/example.c | 4 ++-- include/sentry.h | 13 +++++------ src/sentry_database.c | 2 +- src/sentry_options.c | 9 ++++---- src/sentry_options.h | 2 +- src/sentry_retry.c | 13 +++++------ src/transports/sentry_http_transport.c | 2 +- tests/test_integration_cache.py | 2 +- tests/test_integration_http.py | 32 +++++++++++++------------- tests/unit/test_cache.c | 2 +- tests/unit/test_retry.c | 16 +++++++------ 12 files changed, 50 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a94eb86..e23b571e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Features**: -- Add HTTP retry with exponential backoff: `sentry_options_set_http_retries()`. ([#1520](https://github.com/getsentry/sentry-native/pull/1520)) +- Add HTTP retry with exponential backoff. ([#1520](https://github.com/getsentry/sentry-native/pull/1520)) **Fixes**: diff --git a/examples/example.c b/examples/example.c index 5c2a0a85b..652bdc29f 100644 --- a/examples/example.c +++ b/examples/example.c @@ -659,8 +659,8 @@ main(int argc, char **argv) sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); // 5 days sentry_options_set_cache_max_items(options, 5); } - if (has_arg(argc, argv, "http-retry")) { - sentry_options_set_http_retries(options, 5); + if (has_arg(argc, argv, "no-http-retry")) { + sentry_options_set_http_retry(options, false); } if (has_arg(argc, argv, "enable-metrics")) { diff --git a/include/sentry.h b/include/sentry.h index 5e7e8427b..ea86bd3b4 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -948,8 +948,7 @@ SENTRY_API void sentry_transport_set_shutdown_func( /** * Retries sending all pending envelopes in the transport's retry queue, - * e.g. when coming back online. Only applicable for HTTP transports with - * retries enabled via `sentry_options_set_http_retries`. + * e.g. when coming back online. Only applicable for HTTP transports. */ SENTRY_EXPERIMENTAL_API void sentry_transport_retry( sentry_transport_t *transport); @@ -2267,12 +2266,12 @@ SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( const sentry_options_t *opts); /** - * Sets the maximum number of HTTP retry attempts for network failures. - * Set to 0 to disable retries (default). + * Enables or disables HTTP retry with exponential backoff for network failures. + * Enabled by default. */ -SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retries( - sentry_options_t *opts, int http_retries); -SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retries( +SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retry( + sentry_options_t *opts, int enabled); +SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retry( const sentry_options_t *opts); /** diff --git a/src/sentry_database.c b/src/sentry_database.c index 1d55278bd..34a7d9926 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -288,7 +288,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } bool can_cache = options->cache_keep - && (options->http_retries == 0 + && (!options->http_retry || !sentry__transport_can_retry(options->transport)); sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); diff --git a/src/sentry_options.c b/src/sentry_options.c index d8376f0d5..916b180d6 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -82,6 +82,7 @@ sentry_options_new(void) opts->crash_reporting_mode = SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP; // Default: best of // both worlds + opts->http_retry = true; return opts; } @@ -877,15 +878,15 @@ sentry_options_set_handler_strategy( #endif // SENTRY_PLATFORM_LINUX void -sentry_options_set_http_retries(sentry_options_t *opts, int http_retries) +sentry_options_set_http_retry(sentry_options_t *opts, int enabled) { - opts->http_retries = http_retries; + opts->http_retry = enabled; } int -sentry_options_get_http_retries(const sentry_options_t *opts) +sentry_options_get_http_retry(const sentry_options_t *opts) { - return opts->http_retries; + return opts->http_retry; } void diff --git a/src/sentry_options.h b/src/sentry_options.h index 77209314b..064c0d1fa 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -78,7 +78,7 @@ struct sentry_options_s { bool enable_metrics; sentry_before_send_metric_function_t before_send_metric_func; void *before_send_metric_data; - int http_retries; + bool http_retry; /* everything from here on down are options which are stored here but not exposed through the options API */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 60a01094c..e82d66c9f 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -9,12 +9,12 @@ #include #include +#define SENTRY_RETRY_ATTEMPTS 5 #define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) #define SENTRY_RETRY_THROTTLE 100 struct sentry_retry_s { sentry_path_t *cache_path; - int max_retries; bool cache_keep; uint64_t startup_time; volatile long sealed; @@ -31,7 +31,6 @@ sentry__retry_new(const sentry_options_t *options) return NULL; } retry->cache_path = sentry__path_clone(options->run->cache_path); - retry->max_retries = options->http_retries; retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; retry->sealed = 0; @@ -148,7 +147,7 @@ static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { // network failure with retries remaining: bump count & re-enqueue - if (item->count + 1 < retry->max_retries && status_code < 0) { + if (item->count + 1 < SENTRY_RETRY_ATTEMPTS && status_code < 0) { sentry_path_t *new_path = sentry__retry_make_path( retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { @@ -158,16 +157,16 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) return true; } - bool exhausted = item->count + 1 >= retry->max_retries; + bool exhausted = item->count + 1 >= SENTRY_RETRY_ATTEMPTS; // network failure with retries exhausted if (exhausted && status_code < 0) { if (retry->cache_keep) { SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", - retry->max_retries); + SENTRY_RETRY_ATTEMPTS); } else { SENTRY_WARNF("max retries (%d) reached, discarding envelope", - retry->max_retries); + SENTRY_RETRY_ATTEMPTS); } } @@ -254,7 +253,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, total--; } else { SENTRY_DEBUGF("retrying envelope (%d/%d)", items[i].count + 1, - retry->max_retries); + SENTRY_RETRY_ATTEMPTS); int status_code = send_cb(envelope, data); sentry_envelope_free(envelope); if (!handle_result(retry, &items[i], status_code)) { diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 9827c2d3b..f527116ed 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -282,7 +282,7 @@ http_transport_start(const sentry_options_t *options, void *transport_state) return rv; } - if (options->http_retries > 0) { + if (options->http_retry) { state->retry = sentry__retry_new(options); if (state->retry) { sentry__retry_start(state->retry, bgworker, retry_send_cb, state); diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index 354d12c11..6ba9fd225 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -226,7 +226,7 @@ def test_cache_max_items_with_retry(cmake, backend): run( tmp_path, "sentry_example", - ["log", "cache-keep", "http-retry", "no-setup"], + ["log", "cache-keep", "no-setup"], ) # max 5 items total in cache/ diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 509964593..fb70524b5 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -852,7 +852,7 @@ def test_http_retry_on_network_error(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-event"], + ["log", "capture-event"], env=env_unreachable, ) @@ -869,7 +869,7 @@ def test_http_retry_on_network_error(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env_reachable, ) assert waiting.result @@ -890,19 +890,19 @@ def test_http_retry_multiple_attempts(cmake, httpserver): unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) - run(tmp_path, "sentry_example", ["log", "http-retry", "capture-event"], env=env) + run(tmp_path, "sentry_example", ["log", "capture-event"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-00-" in str(cache_files[0].name) - run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-01-" in str(cache_files[0].name) - run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 @@ -910,7 +910,7 @@ def test_http_retry_multiple_attempts(cmake, httpserver): # exhaust remaining retries (max 5) for i in range(3): - run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) # discarded after max retries (cache_keep not enabled) cache_files = list(cache_dir.glob("*.envelope")) @@ -928,7 +928,7 @@ def test_http_retry_with_cache_keep(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "cache-keep", "capture-event"], + ["log", "cache-keep", "capture-event"], env=env_unreachable, ) @@ -942,7 +942,7 @@ def test_http_retry_with_cache_keep(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "cache-keep", "no-setup"], + ["log", "cache-keep", "no-setup"], env=env_reachable, ) assert waiting.result @@ -961,7 +961,7 @@ def test_http_retry_cache_keep_max_attempts(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "cache-keep", "capture-event"], + ["log", "cache-keep", "capture-event"], env=env, ) @@ -972,7 +972,7 @@ def test_http_retry_cache_keep_max_attempts(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "cache-keep", "no-setup"], + ["log", "cache-keep", "no-setup"], env=env, ) @@ -1029,7 +1029,7 @@ def test_http_retry_multiple_success(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-multiple"], + ["log", "capture-multiple"], env=env_unreachable, ) @@ -1046,7 +1046,7 @@ def test_http_retry_multiple_success(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env_reachable, ) assert waiting.result @@ -1067,7 +1067,7 @@ def test_http_retry_multiple_network_error(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-multiple"], + ["log", "capture-multiple"], env=env, ) @@ -1077,7 +1077,7 @@ def test_http_retry_multiple_network_error(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env, ) @@ -1099,7 +1099,7 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-multiple"], + ["log", "capture-multiple"], env=env_unreachable, ) @@ -1116,7 +1116,7 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env_reachable, ) diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index abfd73d78..f49637bc4 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -46,7 +46,7 @@ SENTRY_TEST(cache_keep) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); - sentry_options_set_http_retries(options, 0); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_path_t *cache_path diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 05479ee6a..764f33bcf 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -84,7 +84,7 @@ SENTRY_TEST(retry_throttle) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retries(options, 5); + sentry_options_set_http_retry(options, true); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -116,7 +116,7 @@ SENTRY_TEST(retry_result) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retries(options, 2); + sentry_options_set_http_retry(options, true); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -164,10 +164,12 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 1); - // 5. Network error at max count → exceeds max_retries=2, removed + // 5. Network error at last attempt → removed sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); - write_retry_file(retry, old_ts, 1, &event_id); + uint64_t very_old_ts + = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); + write_retry_file(retry, very_old_ts, 4, &event_id); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); @@ -182,7 +184,7 @@ SENTRY_TEST(retry_session) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_release(options, "test@1.0.0"); - sentry_options_set_http_retries(options, 2); + sentry_options_set_http_retry(options, true); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -211,7 +213,7 @@ SENTRY_TEST(retry_cache) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retries(options, 5); + sentry_options_set_http_retry(options, true); sentry_options_set_cache_keep(options, 1); sentry_init(options); @@ -302,7 +304,7 @@ SENTRY_TEST(retry_backoff) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retries(options, 5); + sentry_options_set_http_retry(options, true); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); From 5cdcc47615391c0fb01ef8b2b6d6024733d63fe3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 14:18:41 +0100 Subject: [PATCH 052/103] fix(transport): use explicit WinHTTP send/receive timeouts 0 means infinite, not default. Pass 30000ms to match WinHTTP defaults. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_winhttp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index fa18ac8c3..e73de7405 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -134,8 +134,8 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 1; } - // 15s resolve, 15s connect, default send/receive - WinHttpSetTimeouts(client->session, 15000, 15000, 0, 0); + // 15s resolve/connect, 30s send/receive (WinHTTP defaults) + WinHttpSetTimeouts(client->session, 15000, 15000, 30000, 30000); return 0; } From 913650034a9ac68b13ae270fb189fa23f4bc681e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 14:32:46 +0100 Subject: [PATCH 053/103] fix(retry): deduplicate poll tasks on concurrent envelope failures Use a 'scheduled' flag with atomic compare-and-swap to ensure at most one retry_poll_task is queued at a time. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index e82d66c9f..7040b51c7 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -18,6 +18,7 @@ struct sentry_retry_s { bool cache_keep; uint64_t startup_time; volatile long sealed; + volatile long scheduled; sentry_bgworker_t *bgworker; sentry_retry_send_func_t send_cb; void *send_data; @@ -283,6 +284,8 @@ retry_poll_task(void *_retry, void *_state) retry, retry->startup_time, retry->send_cb, retry->send_data)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); + } else { + sentry__atomic_store(&retry->scheduled, 0); } // subsequent polls use backoff instead of the startup time filter retry->startup_time = 0; @@ -295,6 +298,7 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, retry->bgworker = bgworker; retry->send_cb = send_cb; retry->send_data = send_data; + sentry__atomic_store(&retry->scheduled, 1); sentry__bgworker_submit_delayed( bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); } @@ -365,6 +369,8 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) sentry__retry_write_envelope(retry, envelope); // prevent the startup poll from re-processing this session's envelope retry->startup_time = 0; - sentry__bgworker_submit_delayed( - retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); + if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { + sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, + retry, SENTRY_RETRY_INTERVAL); + } } From 9b14dc2844b6cf80dde8a28107c7f477b2ae38f6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:13:38 +0100 Subject: [PATCH 054/103] fix(retry): set sealed flag before dumping queued envelopes Move `sealed = 1` before `foreach_matching` in `retry_dump_queue` to prevent the detached worker from writing duplicate envelopes via `retry_enqueue` while the main thread is dumping the queue. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 7040b51c7..e29f312ec 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -336,10 +336,10 @@ sentry__retry_dump_queue( sentry_retry_t *retry, sentry_task_exec_func_t task_func) { if (retry) { - sentry__bgworker_foreach_matching( - retry->bgworker, task_func, retry_dump_cb, retry); // prevent duplicate writes from a still-running detached worker sentry__atomic_store(&retry->sealed, 1); + sentry__bgworker_foreach_matching( + retry->bgworker, task_func, retry_dump_cb, retry); } } From e75f595d1ca50dcf8639918a975aff4bd4d56f49 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:27:38 +0100 Subject: [PATCH 055/103] fix(retry): prevent retry flush from consuming shutdown timeout Drop the delayed retry_poll_task before bgworker_flush to prevent it from delaying the flush_task by min(retry_interval, timeout). Subtract elapsed flush time from the shutdown timeout so the total is bounded. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 12 ++++++++++++ src/transports/sentry_http_transport.c | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index e29f312ec..3f3d6a1fa 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -314,10 +314,22 @@ retry_flush_task(void *_retry, void *_state) } } +static bool +drop_task_cb(void *_data, void *_ctx) +{ + (void)_data; + (void)_ctx; + return true; +} + void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) { if (retry) { + // drop the delayed poll that would stall bgworker_flush + sentry__bgworker_foreach_matching( + retry->bgworker, retry_poll_task, drop_task_cb, NULL); + sentry__atomic_store(&retry->scheduled, 0); sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); sentry__bgworker_flush(retry->bgworker, timeout); } diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index f527116ed..031a9be54 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -305,10 +305,12 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - // flush drains in-flight retries; shutdown is near-instant afterward + uint64_t started = sentry__monotonic_time(); sentry__retry_flush(state->retry, timeout); + uint64_t elapsed = sentry__monotonic_time() - started; + uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; - int rv = sentry__bgworker_shutdown(bgworker, timeout); + int rv = sentry__bgworker_shutdown(bgworker, remaining); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); if (state->shutdown_client) { From 136fabf52ea0052bd98334e2e91cc51cb532359b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:58:10 +0100 Subject: [PATCH 056/103] fix(retry): zero-initialize retry struct after malloc Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 3f3d6a1fa..6ffaf2c52 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -31,10 +31,10 @@ sentry__retry_new(const sentry_options_t *options) if (!retry) { return NULL; } + memset(retry, 0, sizeof(sentry_retry_t)); retry->cache_path = sentry__path_clone(options->run->cache_path); retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; - retry->sealed = 0; sentry__path_create_dir_all(options->run->cache_path); return retry; } From 1a0d99b7d50ee01e8fd4426a34ce3fa3b4a905e8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 17:03:59 +0100 Subject: [PATCH 057/103] fix(retry): skip flush task after seal to prevent duplicate sends When the bgworker is detached after shutdown timeout, retry_dump_queue writes retry files and sets sealed=1. The detached thread could then run retry_flush_task and re-send those files, causing duplicates. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6ffaf2c52..46d04a706 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -308,7 +308,7 @@ retry_flush_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (retry->startup_time > 0) { + if (retry->startup_time > 0 && !sentry__atomic_fetch(&retry->sealed)) { sentry__retry_send(retry, UINT64_MAX, retry->send_cb, retry->send_data); retry->startup_time = 0; } From ed28b85ee03d9347968f442f6dad1f37809e2550 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 17:26:26 +0100 Subject: [PATCH 058/103] refactor(database): remove unused sentry__run_write_cache The retry system writes cache files directly via its own paths. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 11 ----------- src/sentry_database.h | 8 -------- 2 files changed, 19 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 34a7d9926..e11244c49 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -163,17 +163,6 @@ sentry__run_write_external( return write_envelope(run->external_path, envelope); } -bool -sentry__run_write_cache( - const sentry_run_t *run, const sentry_envelope_t *envelope) -{ - if (sentry__path_create_dir_all(run->cache_path) != 0) { - SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); - return false; - } - return write_envelope(run->cache_path, envelope); -} - bool sentry__run_move_cache( const sentry_run_t *run, const sentry_path_t *src, const char *dst) diff --git a/src/sentry_database.h b/src/sentry_database.h index f92fff8f8..fd44d598d 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -64,14 +64,6 @@ bool sentry__run_write_session( */ bool sentry__run_clear_session(const sentry_run_t *run); -/** - * This will serialize and write the given envelope to disk into a file named - * like so: - * `/cache/.envelope` - */ -bool sentry__run_write_cache( - const sentry_run_t *run, const sentry_envelope_t *envelope); - /** * Moves `src` to `/cache/`. If `dst` is NULL, the filename of * `src` is used. From 62999c39a6c0fff23abb78bf7b5a9e545a82e3ef Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 10:11:08 +0100 Subject: [PATCH 059/103] fix(retry): make trigger one-shot to prevent rapid retry exhaustion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit retry_trigger_task recursively re-triggered itself on network failure, bypassing exponential backoff (UINT64_MAX skips the backoff check) and burning through all 5 retry attempts in milliseconds. Since sentry__retry_send already processes all cached envelopes in a single call, the re-trigger is only ever reached on network failure — exactly the case where it's harmful. Make the trigger one-shot; failed items are left for the regular poll task which respects backoff. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 5 +---- tests/unit/test_retry.c | 41 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 46d04a706..d6e4b8002 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -360,10 +360,7 @@ retry_trigger_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (sentry__retry_send( - retry, UINT64_MAX, retry->send_cb, retry->send_data)) { - sentry__retry_trigger(retry); - } + sentry__retry_send(retry, UINT64_MAX, retry->send_cb, retry->send_data); } void diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 764f33bcf..21792b13a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -357,3 +357,44 @@ SENTRY_TEST(retry_backoff) sentry__retry_free(retry); sentry_close(); } + +SENTRY_TEST(retry_trigger) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_http_retry(options, true); + sentry_init(options); + + sentry_retry_t *retry = sentry__retry_new(options); + TEST_ASSERT(!!retry); + + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); + + uint64_t old_ts + = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(retry, old_ts, 0, &event_id); + + // UINT64_MAX (trigger mode) bypasses backoff: bumps count + retry_test_ctx_t ctx = { -1, 0 }; + sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 1); + + // second call: bumps again because UINT64_MAX skips backoff + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 2); + + // before=0 (poll mode) respects backoff: item is skipped + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 0); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 2); + + sentry__retry_free(retry); + sentry_close(); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 710e02c87..473391251 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -197,6 +197,7 @@ XX(retry_cache) XX(retry_result) XX(retry_session) XX(retry_throttle) +XX(retry_trigger) XX(ringbuffer_append) XX(ringbuffer_append_invalid_decref_value) XX(ringbuffer_append_null_decref_value) From 70b1040f87f0a28f269902abf975e6074b8509de Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 10:14:49 +0100 Subject: [PATCH 060/103] fix(core): check http_retry option instead of transport capability cleanup_cache was gated on sentry__transport_can_retry, which checks for retry_func. Since retry_func is unconditionally set for all HTTP transports, this ran cleanup_cache even with http_retry disabled. Check the option directly instead. Co-Authored-By: Claude Opus 4.6 --- src/sentry_core.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index 3522f935a..14dba4a2d 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -292,8 +292,7 @@ sentry_init(sentry_options_t *options) backend->prune_database_func(backend); } - if (options->cache_keep - || sentry__transport_can_retry(options->transport)) { + if (options->cache_keep || options->http_retry) { sentry__cleanup_cache(options); } From 794df4be97d9fde3ae5906542791a185411c4e27 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 11:04:45 +0100 Subject: [PATCH 061/103] fix(retry): prevent UB from negative count in backoff shift Reject negative counts in parse_filename (a corrupted filename like 123--01-.envelope parses count=-1 via strtol). Also clamp the count in sentry__retry_backoff to prevent left-shift by a negative amount. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 4 ++-- tests/unit/test_retry.c | 34 ++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d6e4b8002..d33a5ab73 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -67,7 +67,7 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, const char *count_str = end + 1; long count = strtol(count_str, &end, 10); - if (*end != '-') { + if (*end != '-' || count < 0) { return false; } @@ -87,7 +87,7 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, uint64_t sentry__retry_backoff(int count) { - return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(count, 5); + return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(MAX(count, 0), 5); } typedef struct { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 21792b13a..5b9b9dc22 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -80,6 +80,39 @@ test_send_cb(sentry_envelope_t *envelope, void *_ctx) return ctx->status_code; } +SENTRY_TEST(retry_filename) +{ + uint64_t ts; + int count; + const char *uuid; + + TEST_CHECK(sentry__retry_parse_filename( + "1234567890-00-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, + &count, &uuid)); + TEST_CHECK_UINT64_EQUAL(ts, 1234567890); + TEST_CHECK_INT_EQUAL(count, 0); + TEST_CHECK(strncmp(uuid, "abcdefab-1234-5678-9abc-def012345678", 36) == 0); + + TEST_CHECK(sentry__retry_parse_filename( + "999-04-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, + &uuid)); + TEST_CHECK_UINT64_EQUAL(ts, 999); + TEST_CHECK_INT_EQUAL(count, 4); + + // negative count + TEST_CHECK(!sentry__retry_parse_filename( + "123--01-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, + &uuid)); + + // cache filename (no timestamp/count) + TEST_CHECK(!sentry__retry_parse_filename( + "abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); + + // missing .envelope suffix + TEST_CHECK(!sentry__retry_parse_filename( + "123-00-abcdefab-1234-5678-9abc-def012345678.txt", &ts, &count, &uuid)); +} + SENTRY_TEST(retry_throttle) { SENTRY_TEST_OPTIONS_NEW(options); @@ -353,6 +386,7 @@ SENTRY_TEST(retry_backoff) TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 16); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(5), base * 32); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(6), base * 32); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(-1), base); sentry__retry_free(retry); sentry_close(); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 473391251..d0a7c257e 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -194,6 +194,7 @@ XX(read_write_envelope_to_invalid_path) XX(recursive_paths) XX(retry_backoff) XX(retry_cache) +XX(retry_filename) XX(retry_result) XX(retry_session) XX(retry_throttle) From 83475e446b7d105ad841d5a4f36c03409cfdc42c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 11:48:55 +0100 Subject: [PATCH 062/103] fix(options): normalize http_retry with !! to match other boolean setters Co-Authored-By: Claude Opus 4.6 --- src/sentry_options.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_options.c b/src/sentry_options.c index 916b180d6..43e4ed467 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -880,7 +880,7 @@ sentry_options_set_handler_strategy( void sentry_options_set_http_retry(sentry_options_t *opts, int enabled) { - opts->http_retry = enabled; + opts->http_retry = !!enabled; } int From 8aae7469a6675fc826898677755165cbbcebed6f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 23 Feb 2026 15:34:15 +0100 Subject: [PATCH 063/103] revert(database): restore original variable names and whitespace in write_envelope Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index e11244c49..4bc8784fa 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -115,7 +115,7 @@ sentry__run_free(sentry_run_t *run) } static bool -write_envelope(const sentry_path_t *dir, const sentry_envelope_t *envelope) +write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); @@ -125,23 +125,24 @@ write_envelope(const sentry_path_t *dir, const sentry_envelope_t *envelope) event_id = sentry_uuid_new_v4(); } - char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); - if (!filename) { + char *envelope_filename = sentry__uuid_as_filename(&event_id, ".envelope"); + if (!envelope_filename) { return false; } - sentry_path_t *path = sentry__path_join_str(dir, filename); - sentry_free(filename); - if (!path) { + sentry_path_t *output_path = sentry__path_join_str(path, envelope_filename); + sentry_free(envelope_filename); + if (!output_path) { return false; } - int rv = sentry_envelope_write_to_path(envelope, path); - sentry__path_free(path); + int rv = sentry_envelope_write_to_path(envelope, output_path); + sentry__path_free(output_path); if (rv) { SENTRY_WARN("writing envelope to file failed"); return false; } + return true; } @@ -160,6 +161,7 @@ sentry__run_write_external( SENTRY_ERRORF("mkdir failed: \"%s\"", run->external_path->path); return false; } + return write_envelope(run->external_path, envelope); } From 791d598766f5c7472a98571a3db4158bb4ceb8d7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 14:36:39 +0100 Subject: [PATCH 064/103] docs: clarify sentry_transport_retry behavior and limitations - Document the 5-attempt retry limit - Note there is no rate limiting between attempts - Warn about potential event loss during extended network outages --- include/sentry.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/include/sentry.h b/include/sentry.h index ea86bd3b4..ae5df82bb 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -949,6 +949,17 @@ SENTRY_API void sentry_transport_set_shutdown_func( /** * Retries sending all pending envelopes in the transport's retry queue, * e.g. when coming back online. Only applicable for HTTP transports. + * + * Note: The SDK automatically retries failed envelopes on next application + * startup. This function allows manual triggering of pending retries at + * runtime. Each envelope is retried up to 5 times. If all attempts are + * exhausted during intermittent connectivity, events will be discarded + * (or moved to cache if enabled via sentry_options_set_cache_keep). + * + * Warning: This function has no rate limiting - it will immediately + * attempt to send all pending envelopes. Calling this repeatedly during + * extended network outages may exhaust retry attempts that might have + * succeeded with the SDK's built-in exponential backoff. */ SENTRY_EXPERIMENTAL_API void sentry_transport_retry( sentry_transport_t *transport); From a2efe25633543408023bf6a056b84cafbd92fe07 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 14:42:23 +0100 Subject: [PATCH 065/103] docs(retry): document retry behavior for network failures vs HTTP responses Only network failures (negative status codes) trigger retries. HTTP responses including 5xx (500, 502, 503, 504) are discarded. --- src/sentry_retry.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d33a5ab73..4b832f787 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -147,6 +147,10 @@ sentry__retry_write_envelope( static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { + // Only network failures (status_code < 0) trigger retries. HTTP responses + // including 5xx (500, 502, 503, 504) are discarded: + // https://develop.sentry.dev/sdk/expected-features/#dealing-with-network-failures + // network failure with retries remaining: bump count & re-enqueue if (item->count + 1 < SENTRY_RETRY_ATTEMPTS && status_code < 0) { sentry_path_t *new_path = sentry__retry_make_path( From 4b1e7d7e1cd4f6b2aaf8944bb268375fe5aaa702 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 15:21:00 +0100 Subject: [PATCH 066/103] fix(retry): only clear startup_time when envelope is written - Change sentry__retry_write_envelope to return bool indicating success - Return false for nil event IDs (session envelopes) and write failures - sentry__retry_enqueue now returns early if write fails, preserving startup_time for session envelopes so retry_flush_task can flush them --- src/sentry_retry.c | 23 ++++++++++++++--------- src/sentry_retry.h | 3 ++- tests/unit/test_retry.c | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 4b832f787..578306a9c 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -121,13 +121,13 @@ sentry__retry_make_path( return sentry__path_join_str(retry->cache_path, filename); } -void +bool sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope) { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); if (sentry_uuid_is_nil(&event_id)) { - return; + return false; } char uuid[37]; @@ -135,13 +135,16 @@ sentry__retry_write_envelope( sentry_path_t *path = sentry__retry_make_path(retry, sentry__usec_time() / 1000, 0, uuid); - if (path) { - if (sentry_envelope_write_to_path(envelope, path) != 0) { - SENTRY_WARNF( - "failed to write retry envelope to \"%s\"", path->path); - } - sentry__path_free(path); + if (!path) { + return false; + } + + int rv = sentry_envelope_write_to_path(envelope, path); + if (rv != 0) { + SENTRY_WARNF("failed to write retry envelope to \"%s\"", path->path); } + sentry__path_free(path); + return rv == 0; } static bool @@ -379,7 +382,9 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) if (sentry__atomic_fetch(&retry->sealed)) { return; } - sentry__retry_write_envelope(retry, envelope); + if (!sentry__retry_write_envelope(retry, envelope)) { + return; + } // prevent the startup poll from re-processing this session's envelope retry->startup_time = 0; if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 12008259e..50f39e9ac 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -38,8 +38,9 @@ void sentry__retry_enqueue( /** * Writes an event envelope to the retry dir. Non-event envelopes are skipped. + * Returns true if an envelope was written, false otherwise. */ -void sentry__retry_write_envelope( +bool sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); /** diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 5b9b9dc22..9cdde2f6e 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -233,7 +233,7 @@ SENTRY_TEST(retry_session) sentry__envelope_add_session(envelope, session); // Session-only envelopes have no event_id → should not be written - sentry__retry_write_envelope(retry, envelope); + TEST_CHECK(!sentry__retry_write_envelope(retry, envelope)); TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 0); sentry_envelope_free(envelope); From 6064f1d120f52542ef918471b5ea49dbc534f9b7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 15:36:58 +0100 Subject: [PATCH 067/103] fix(retry): check for NULL from sentry__path_clone Add null check after cloning cache_path to prevent dereferencing null later in sentry__retry_send when iterating directory or joining paths. --- src/sentry_retry.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 578306a9c..c961e01cc 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -33,9 +33,13 @@ sentry__retry_new(const sentry_options_t *options) } memset(retry, 0, sizeof(sentry_retry_t)); retry->cache_path = sentry__path_clone(options->run->cache_path); + if (!retry->cache_path) { + sentry_free(retry); + return NULL; + } retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; - sentry__path_create_dir_all(options->run->cache_path); + sentry__path_create_dir_all(retry->cache_path); return retry; } From 45ee8860190b697dfea3ba59cd79a6701eb3c814 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 17:08:37 +0100 Subject: [PATCH 068/103] fix(retry): apply backoff when system clock moves backward When system clock moves backward (now < ts), the condition now >= ts was false, causing the backoff check to be skipped entirely. This made items immediately eligible for retry regardless of their count. Now checks if now < ts (clock skew) OR if backoff hasn't elapsed. --- src/sentry_retry.c | 3 ++- tests/unit/test_retry.c | 28 ++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index c961e01cc..67919daf9 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -232,7 +232,8 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, continue; } total++; - if (!before && now >= ts && (now - ts) < sentry__retry_backoff(count)) { + if (!before + && (now < ts || (now - ts) < sentry__retry_backoff(count))) { continue; } if (eligible == item_cap) { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 9cdde2f6e..d0798087d 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -145,6 +145,34 @@ SENTRY_TEST(retry_throttle) sentry_close(); } +SENTRY_TEST(retry_skew) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_http_retry(options, true); + sentry_init(options); + + sentry_retry_t *retry = sentry__retry_new(options); + TEST_ASSERT(!!retry); + + sentry__path_remove_all(options->run->cache_path); + sentry__path_create_dir_all(options->run->cache_path); + + // future timestamp simulates clock moving backward + uint64_t future_ts = sentry__usec_time() / 1000 + 1000000; + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(retry, future_ts, 0, &event_id); + + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + + // item should NOT be processed due to backoff (clock backward) + TEST_CHECK_INT_EQUAL(ctx.count, 0); + + sentry__retry_free(retry); + sentry_close(); +} + SENTRY_TEST(retry_result) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index d0a7c257e..2318ff8be 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -197,6 +197,7 @@ XX(retry_cache) XX(retry_filename) XX(retry_result) XX(retry_session) +XX(retry_skew) XX(retry_throttle) XX(retry_trigger) XX(ringbuffer_append) From ec59d5e53343693822a492aad6ffe3928e3191bb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 18:12:46 +0100 Subject: [PATCH 069/103] fix(retry): increase SENTRY_RETRY_ATTEMPTS to 6 to match Crashpad Crashpad's kRetryAttempts=5 with `upload_attempts > kRetryAttempts` (checked before post-increment) allows upload_attempts 0-5, giving 6 retries with backoffs: 15m, 30m, 1h, 2h, 4h, 8h. sentry-native's `count + 1 < SENTRY_RETRY_ATTEMPTS` with the old value of 5 only allowed counts 0-3 to be re-enqueued, so the max backoff reached was 4h. Bumping to 6 gives the same 6 retries and the full backoff sequence up to 8h. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- tests/test_integration_http.py | 4 ++-- tests/unit/test_retry.c | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 67919daf9..911afb045 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -9,7 +9,7 @@ #include #include -#define SENTRY_RETRY_ATTEMPTS 5 +#define SENTRY_RETRY_ATTEMPTS 6 #define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) #define SENTRY_RETRY_THROTTLE 100 diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index fb70524b5..e2b2ceac8 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -908,8 +908,8 @@ def test_http_retry_multiple_attempts(cmake, httpserver): assert len(cache_files) == 1 assert "-02-" in str(cache_files[0].name) - # exhaust remaining retries (max 5) - for i in range(3): + # exhaust remaining retries (max 6) + for i in range(4): run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) # discarded after max retries (cache_keep not enabled) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index d0798087d..553179932 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -229,8 +229,8 @@ SENTRY_TEST(retry_result) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); uint64_t very_old_ts - = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); - write_retry_file(retry, very_old_ts, 4, &event_id); + = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); + write_retry_file(retry, very_old_ts, 5, &event_id); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); @@ -285,9 +285,9 @@ SENTRY_TEST(retry_cache) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); - uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); + uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry, old_ts, 4, &event_id); + write_retry_file(retry, old_ts, 5, &event_id); char uuid_str[37]; sentry_uuid_as_string(&event_id, uuid_str); @@ -298,7 +298,7 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); TEST_CHECK(!sentry__path_is_file(cached)); - // Network error on a file at count=4 with max_retries=5 → renames to + // Network error on a file at count=5 with max_retries=6 → renames to // cache format (.envelope) retry_test_ctx_t ctx = { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); @@ -306,11 +306,11 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); TEST_CHECK(sentry__path_is_file(cached)); - // Success on a file at count=4 → also renames to cache format + // Success on a file at count=5 → also renames to cache format // (cache_keep preserves all envelopes regardless of send outcome) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); - write_retry_file(retry, old_ts, 4, &event_id); + write_retry_file(retry, old_ts, 5, &event_id); TEST_CHECK(!sentry__path_is_file(cached)); ctx = (retry_test_ctx_t) { 200, 0 }; From 00bef0171e2af65f9b53253574d45495519e89c3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 18:28:46 +0100 Subject: [PATCH 070/103] fix(retry): avoid retry flush consuming entire shutdown timeout Rename sentry__retry_flush to sentry__retry_shutdown and remove the bgworker_flush call so bgworker_shutdown gets the full timeout. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 +-- src/sentry_retry.h | 4 ++-- src/transports/sentry_http_transport.c | 7 ++----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 911afb045..016f59de8 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -335,7 +335,7 @@ drop_task_cb(void *_data, void *_ctx) } void -sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) +sentry__retry_shutdown(sentry_retry_t *retry) { if (retry) { // drop the delayed poll that would stall bgworker_flush @@ -343,7 +343,6 @@ sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) retry->bgworker, retry_poll_task, drop_task_cb, NULL); sentry__atomic_store(&retry->scheduled, 0); sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); - sentry__bgworker_flush(retry->bgworker, timeout); } } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 50f39e9ac..5789eb26a 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -20,9 +20,9 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data); /** - * Flushes unprocessed previous-session retries. No-op if already polled. + * Prepares retry for shutdown: drops pending polls and submits a flush task. */ -void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); +void sentry__retry_shutdown(sentry_retry_t *retry); /** * Dumps queued envelopes to the retry dir and seals against further writes. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 031a9be54..ac1b38a8a 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -305,12 +305,9 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - uint64_t started = sentry__monotonic_time(); - sentry__retry_flush(state->retry, timeout); - uint64_t elapsed = sentry__monotonic_time() - started; - uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; + sentry__retry_shutdown(state->retry); - int rv = sentry__bgworker_shutdown(bgworker, remaining); + int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); if (state->shutdown_client) { From d96beb1532aa1b7071d97f9fbe5b959f68cb5e2f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:21:49 +0100 Subject: [PATCH 071/103] fix(retry): warn on failed retry envelope rename Check the sentry__path_rename return value and log a warning on failure instead of silently ignoring it. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 016f59de8..6fce3a011 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -163,7 +163,10 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) sentry_path_t *new_path = sentry__retry_make_path( retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { - sentry__path_rename(item->path, new_path); + if (sentry__path_rename(item->path, new_path) != 0) { + SENTRY_WARNF( + "failed to rename retry envelope \"%s\"", item->path->path); + } sentry__path_free(new_path); } return true; From 985b3daf06c929f6e02454b575802b58be41c64b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:24:15 +0100 Subject: [PATCH 072/103] fix(retry): check for NULL from sentry__path_clone in retry send Move eligible++ after all item fields are populated so a NULL path from allocation failure does not leave a half-initialized item in the array. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6fce3a011..6ebbee398 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -249,12 +249,16 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_free(items); items = tmp; } - retry_item_t *item = &items[eligible++]; + retry_item_t *item = &items[eligible]; item->path = sentry__path_clone(p); + if (!item->path) { + break; + } item->ts = ts; item->count = count; memcpy(item->uuid, uuid, 36); item->uuid[36] = '\0'; + eligible++; } sentry__pathiter_free(piter); From 87532a30101b24003e8bcce46198f2be11f8ea5c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:45:02 +0100 Subject: [PATCH 073/103] fix(retry): fix data race on startup_time between threads Replace mutable startup_time + sealed with a state enum so that the startup flag is managed via atomic operations instead of clearing a uint64_t that may tear on 32-bit systems. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6ebbee398..48cd06f12 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -13,11 +13,17 @@ #define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) #define SENTRY_RETRY_THROTTLE 100 +typedef enum { + SENTRY_RETRY_STARTUP = 0, + SENTRY_RETRY_RUNNING = 1, + SENTRY_RETRY_SEALED = 2 +} sentry_retry_state_t; + struct sentry_retry_s { sentry_path_t *cache_path; bool cache_keep; uint64_t startup_time; - volatile long sealed; + volatile long state; volatile long scheduled; sentry_bgworker_t *bgworker; sentry_retry_send_func_t send_cb; @@ -299,15 +305,19 @@ retry_poll_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (sentry__retry_send( - retry, retry->startup_time, retry->send_cb, retry->send_data)) { + uint64_t before + = sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_STARTUP + ? retry->startup_time + : 0; + if (sentry__retry_send(retry, before, retry->send_cb, retry->send_data)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } else { sentry__atomic_store(&retry->scheduled, 0); } // subsequent polls use backoff instead of the startup time filter - retry->startup_time = 0; + sentry__atomic_compare_swap( + &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING); } void @@ -327,9 +337,9 @@ retry_flush_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (retry->startup_time > 0 && !sentry__atomic_fetch(&retry->sealed)) { + if (sentry__atomic_compare_swap( + &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING)) { sentry__retry_send(retry, UINT64_MAX, retry->send_cb, retry->send_data); - retry->startup_time = 0; } } @@ -367,7 +377,7 @@ sentry__retry_dump_queue( { if (retry) { // prevent duplicate writes from a still-running detached worker - sentry__atomic_store(&retry->sealed, 1); + sentry__atomic_store(&retry->state, SENTRY_RETRY_SEALED); sentry__bgworker_foreach_matching( retry->bgworker, task_func, retry_dump_cb, retry); } @@ -390,14 +400,14 @@ sentry__retry_trigger(sentry_retry_t *retry) void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { - if (sentry__atomic_fetch(&retry->sealed)) { + if (sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_SEALED) { return; } if (!sentry__retry_write_envelope(retry, envelope)) { return; } - // prevent the startup poll from re-processing this session's envelope - retry->startup_time = 0; + sentry__atomic_compare_swap( + &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING); if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); From 754a65d95f9f3c41e8648cd914329c20a29d70e5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 20:31:48 +0100 Subject: [PATCH 074/103] fix(retry): clear retry_func when retry fails to initialize If sentry__retry_new() fails, retry_func was still set on the transport, causing can_retry to return true and envelopes to be dropped instead of cached. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index ac1b38a8a..230df598b 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -286,6 +286,10 @@ http_transport_start(const sentry_options_t *options, void *transport_state) state->retry = sentry__retry_new(options); if (state->retry) { sentry__retry_start(state->retry, bgworker, retry_send_cb, state); + } else { + // cannot retry, clear retry_func so envelopes get cached instead of + // dropped + sentry__transport_set_retry_func(options->transport, NULL); } } From 0e641152b7c8efcd43bbfdacc77538a5ed17abc2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 09:00:13 +0100 Subject: [PATCH 075/103] fix(retry): persist non-event envelopes to the retry cache Envelopes without an event_id (e.g. sessions) were silently dropped by sentry__retry_write_envelope. This was a workaround (cd57ff4d) for the old retry_write_envelope approach that regenerated a random UUID on each attempt, orphaning files on disk. The rewrite to rename-based counter bumps in handle_result (0f371772) made this safe: the UUID is parsed from the filename and preserved across renames, so a random UUID assigned at initial write stays stable through all retry cycles. Generate a random UUIDv4 for nil event_id envelopes instead of skipping them. Extract unreachable_dsn to module level. Add UUID consistency assertions to existing retry tests. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- src/sentry_retry.h | 2 +- tests/test_integration_http.py | 72 ++++++++++++++++++++++++++++++---- tests/unit/test_retry.c | 5 +-- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 48cd06f12..4d976d569 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -137,7 +137,7 @@ sentry__retry_write_envelope( { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); if (sentry_uuid_is_nil(&event_id)) { - return false; + event_id = sentry_uuid_new_v4(); } char uuid[37]; diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 5789eb26a..a171ccdf4 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -37,7 +37,7 @@ void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); /** - * Writes an event envelope to the retry dir. Non-event envelopes are skipped. + * Writes an envelope to the retry dir. * Returns true if an envelope was written, false otherwise. */ bool sentry__retry_write_envelope( diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index e2b2ceac8..0561f98dc 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -59,6 +59,8 @@ def get_asan_crash_env(env): pytestmark = pytest.mark.skipif(not has_http, reason="tests need http") +unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + # fmt: off auth_header = ( f"Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/{SENTRY_VERSION}" @@ -846,7 +848,6 @@ def test_http_retry_on_network_error(cmake, httpserver): cache_dir = tmp_path.joinpath(".sentry-native/cache") # unreachable port triggers CURLE_COULDNT_CONNECT - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -860,6 +861,7 @@ def test_http_retry_on_network_error(cmake, httpserver): cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-00-" in str(cache_files[0].name) + envelope_uuid = cache_files[0].stem[-36:] # retry on next run with working server env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) @@ -876,6 +878,7 @@ def test_http_retry_on_network_error(cmake, httpserver): assert len(httpserver.log) == 1 envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + assert envelope.headers["event_id"] == envelope_uuid assert_meta(envelope, integration="inproc") cache_files = list(cache_dir.glob("*.envelope")) @@ -887,7 +890,6 @@ def test_http_retry_multiple_attempts(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) run(tmp_path, "sentry_example", ["log", "capture-event"], env=env) @@ -895,18 +897,23 @@ def test_http_retry_multiple_attempts(cmake, httpserver): cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-00-" in str(cache_files[0].name) + envelope_uuid = cache_files[0].stem[-36:] + envelope = Envelope.deserialize(cache_files[0].read_bytes()) + assert envelope.headers["event_id"] == envelope_uuid run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-01-" in str(cache_files[0].name) + assert cache_files[0].stem[-36:] == envelope_uuid run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-02-" in str(cache_files[0].name) + assert cache_files[0].stem[-36:] == envelope_uuid # exhaust remaining retries (max 6) for i in range(4): @@ -922,7 +929,6 @@ def test_http_retry_with_cache_keep(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -955,7 +961,6 @@ def test_http_retry_cache_keep_max_attempts(cmake): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -1023,7 +1028,6 @@ def test_http_retry_multiple_success(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -1061,7 +1065,6 @@ def test_http_retry_multiple_network_error(cmake): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -1093,7 +1096,6 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -1123,3 +1125,59 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): # first envelope gets 429, rest are discarded by rate limiter cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_session_on_network_error(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "start-session"], + env=env_unreachable, + ) + + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-00-" in str(cache_files[0].name) + envelope_uuid = cache_files[0].stem[-36:] + + # second and third attempts still fail — envelope gets renamed each time + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env_unreachable) + + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-01-" in str(cache_files[0].name) + assert cache_files[0].stem[-36:] == envelope_uuid + + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env_unreachable) + + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-02-" in str(cache_files[0].name) + assert cache_files[0].stem[-36:] == envelope_uuid + + # succeed on fourth attempt + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "no-setup"], + env=env_reachable, + ) + assert waiting.result + + assert len(httpserver.log) == 1 + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + assert_session(envelope, {"init": True, "status": "exited", "errors": 0}) + + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 0 diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 553179932..aa9f2474d 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -260,9 +260,8 @@ SENTRY_TEST(retry_session) TEST_ASSERT(!!envelope); sentry__envelope_add_session(envelope, session); - // Session-only envelopes have no event_id → should not be written - TEST_CHECK(!sentry__retry_write_envelope(retry, envelope)); - TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 0); + TEST_CHECK(sentry__retry_write_envelope(retry, envelope)); + TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 1); sentry_envelope_free(envelope); sentry__session_free(session); From 9ab3e11ca9d2d8e0d0d6f7c191f593b62fc769aa Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 09:42:01 +0100 Subject: [PATCH 076/103] fix(retry): close race between poll task and enqueue on scheduled flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clear the scheduled flag before scanning so that a concurrent retry_enqueue always sees 0 and successfully arms a new poll via CAS. Previously, the flag was cleared after the scan returned, creating a window where enqueue could see 1, skip scheduling, and then the poll would clear the flag — stranding the newly enqueued item. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 4d976d569..d28852d6d 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -309,11 +309,12 @@ retry_poll_task(void *_retry, void *_state) = sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_STARTUP ? retry->startup_time : 0; - if (sentry__retry_send(retry, before, retry->send_cb, retry->send_data)) { + // clear before scanning so a concurrent enqueue sees 0 and arms a poll + sentry__atomic_store(&retry->scheduled, 0); + if (sentry__retry_send(retry, before, retry->send_cb, retry->send_data) + && sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); - } else { - sentry__atomic_store(&retry->scheduled, 0); } // subsequent polls use backoff instead of the startup time filter sentry__atomic_compare_swap( From 5a100446709ba59451b231a87a16926998ec8438 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:51:20 +0100 Subject: [PATCH 077/103] refactor(database): add retry_count to write_envelope, add sentry__run_write_cache Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 35 +++++++++++++++++++++++++++-------- src/sentry_database.h | 8 ++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 4bc8784fa..16a551a85 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -5,6 +5,7 @@ #include "sentry_options.h" #include "sentry_session.h" #include "sentry_transport.h" +#include "sentry_utils.h" #include "sentry_uuid.h" #include #include @@ -115,7 +116,8 @@ sentry__run_free(sentry_run_t *run) } static bool -write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) +write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope, + int retry_count) { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); @@ -125,13 +127,18 @@ write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) event_id = sentry_uuid_new_v4(); } - char *envelope_filename = sentry__uuid_as_filename(&event_id, ".envelope"); - if (!envelope_filename) { - return false; + char uuid[37]; + sentry_uuid_as_string(&event_id, uuid); + + char filename[128]; + if (retry_count >= 0) { + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", + sentry__usec_time() / 1000, retry_count, uuid); + } else { + snprintf(filename, sizeof(filename), "%.36s.envelope", uuid); } - sentry_path_t *output_path = sentry__path_join_str(path, envelope_filename); - sentry_free(envelope_filename); + sentry_path_t *output_path = sentry__path_join_str(path, filename); if (!output_path) { return false; } @@ -150,7 +157,7 @@ bool sentry__run_write_envelope( const sentry_run_t *run, const sentry_envelope_t *envelope) { - return write_envelope(run->run_path, envelope); + return write_envelope(run->run_path, envelope, -1); } bool @@ -162,7 +169,19 @@ sentry__run_write_external( return false; } - return write_envelope(run->external_path, envelope); + return write_envelope(run->external_path, envelope, -1); +} + +bool +sentry__run_write_cache( + const sentry_run_t *run, const sentry_envelope_t *envelope, int retry_count) +{ + if (sentry__path_create_dir_all(run->cache_path) != 0) { + SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); + return false; + } + + return write_envelope(run->cache_path, envelope, retry_count); } bool diff --git a/src/sentry_database.h b/src/sentry_database.h index fd44d598d..e28c62fe6 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -64,6 +64,14 @@ bool sentry__run_write_session( */ bool sentry__run_clear_session(const sentry_run_t *run); +/** + * This will serialize and write the given envelope to disk into the cache + * directory. When retry_count >= 0 the filename uses retry format + * `--.envelope`, otherwise `.envelope`. + */ +bool sentry__run_write_cache(const sentry_run_t *run, + const sentry_envelope_t *envelope, int retry_count); + /** * Moves `src` to `/cache/`. If `dst` is NULL, the filename of * `src` is used. From f35817d6e72cb3cd31c42715cf0c7bf553a432b6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:53:36 +0100 Subject: [PATCH 078/103] refactor(database): make sentry_run_t refcounted Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 13 ++++++++++- src/sentry_database.h | 6 +++++ src/sentry_retry.c | 50 +++++++++-------------------------------- src/sentry_retry.h | 7 ------ tests/unit/test_retry.c | 3 ++- 5 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 16a551a85..59d2e9bc8 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -4,6 +4,7 @@ #include "sentry_json.h" #include "sentry_options.h" #include "sentry_session.h" +#include "sentry_sync.h" #include "sentry_transport.h" #include "sentry_utils.h" #include "sentry_uuid.h" @@ -72,6 +73,7 @@ sentry__run_new(const sentry_path_t *database_path) return NULL; } + run->refcount = 1; run->uuid = uuid; run->run_path = run_path; run->session_path = session_path; @@ -94,6 +96,15 @@ sentry__run_new(const sentry_path_t *database_path) return NULL; } +sentry_run_t * +sentry__run_incref(sentry_run_t *run) +{ + if (run) { + sentry__atomic_fetch_and_add(&run->refcount, 1); + } + return run; +} + void sentry__run_clean(sentry_run_t *run) { @@ -104,7 +115,7 @@ sentry__run_clean(sentry_run_t *run) void sentry__run_free(sentry_run_t *run) { - if (!run) { + if (!run || sentry__atomic_fetch_and_add(&run->refcount, -1) != 1) { return; } sentry__path_free(run->run_path); diff --git a/src/sentry_database.h b/src/sentry_database.h index e28c62fe6..e461c25f3 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -13,6 +13,7 @@ typedef struct sentry_run_s { sentry_path_t *external_path; sentry_path_t *cache_path; sentry_filelock_t *lock; + long refcount; } sentry_run_t; /** @@ -23,6 +24,11 @@ typedef struct sentry_run_s { */ sentry_run_t *sentry__run_new(const sentry_path_t *database_path); +/** + * Increment the refcount and return the run pointer. + */ +sentry_run_t *sentry__run_incref(sentry_run_t *run); + /** * This will clean up all the files belonging to this run. */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d28852d6d..9f0cc2c00 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -20,7 +20,7 @@ typedef enum { } sentry_retry_state_t; struct sentry_retry_s { - sentry_path_t *cache_path; + sentry_run_t *run; bool cache_keep; uint64_t startup_time; volatile long state; @@ -38,14 +38,9 @@ sentry__retry_new(const sentry_options_t *options) return NULL; } memset(retry, 0, sizeof(sentry_retry_t)); - retry->cache_path = sentry__path_clone(options->run->cache_path); - if (!retry->cache_path) { - sentry_free(retry); - return NULL; - } + retry->run = sentry__run_incref(options->run); retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; - sentry__path_create_dir_all(retry->cache_path); return retry; } @@ -55,7 +50,7 @@ sentry__retry_free(sentry_retry_t *retry) if (!retry) { return; } - sentry__path_free(retry->cache_path); + sentry__run_free(retry->run); sentry_free(retry); } @@ -128,33 +123,7 @@ sentry__retry_make_path( char filename[128]; snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, count, uuid); - return sentry__path_join_str(retry->cache_path, filename); -} - -bool -sentry__retry_write_envelope( - sentry_retry_t *retry, const sentry_envelope_t *envelope) -{ - sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); - if (sentry_uuid_is_nil(&event_id)) { - event_id = sentry_uuid_new_v4(); - } - - char uuid[37]; - sentry_uuid_as_string(&event_id, uuid); - - sentry_path_t *path - = sentry__retry_make_path(retry, sentry__usec_time() / 1000, 0, uuid); - if (!path) { - return false; - } - - int rv = sentry_envelope_write_to_path(envelope, path); - if (rv != 0) { - SENTRY_WARNF("failed to write retry envelope to \"%s\"", path->path); - } - sentry__path_free(path); - return rv == 0; + return sentry__path_join_str(retry->run->cache_path, filename); } static bool @@ -196,7 +165,7 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); sentry_path_t *dest - = sentry__path_join_str(retry->cache_path, cache_name); + = sentry__path_join_str(retry->run->cache_path, cache_name); if (!dest || sentry__path_rename(item->path, dest) != 0) { sentry__path_remove(item->path); } @@ -212,7 +181,8 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_retry_send_func_t send_cb, void *data) { - sentry_pathiter_t *piter = sentry__path_iter_directory(retry->cache_path); + sentry_pathiter_t *piter + = sentry__path_iter_directory(retry->run->cache_path); if (!piter) { return 0; } @@ -367,8 +337,8 @@ sentry__retry_shutdown(sentry_retry_t *retry) static bool retry_dump_cb(void *_envelope, void *_retry) { - sentry__retry_write_envelope( - (sentry_retry_t *)_retry, (sentry_envelope_t *)_envelope); + sentry_retry_t *retry = (sentry_retry_t *)_retry; + sentry__run_write_cache(retry->run, (sentry_envelope_t *)_envelope, 0); return true; } @@ -404,7 +374,7 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) if (sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_SEALED) { return; } - if (!sentry__retry_write_envelope(retry, envelope)) { + if (!sentry__run_write_cache(retry->run, envelope, 0)) { return; } sentry__atomic_compare_swap( diff --git a/src/sentry_retry.h b/src/sentry_retry.h index a171ccdf4..edde49d8f 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -36,13 +36,6 @@ void sentry__retry_dump_queue( void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); -/** - * Writes an envelope to the retry dir. - * Returns true if an envelope was written, false otherwise. - */ -bool sentry__retry_write_envelope( - sentry_retry_t *retry, const sentry_envelope_t *envelope); - /** * Sends eligible retry files via `send_cb`. `before > 0`: send files with * ts < before (startup). `before == 0`: use backoff. Returns remaining file diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index aa9f2474d..b5edb383e 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -1,3 +1,4 @@ +#include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" @@ -260,7 +261,7 @@ SENTRY_TEST(retry_session) TEST_ASSERT(!!envelope); sentry__envelope_add_session(envelope, session); - TEST_CHECK(sentry__retry_write_envelope(retry, envelope)); + TEST_CHECK(sentry__run_write_cache(options->run, envelope, 0)); TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 1); sentry_envelope_free(envelope); From 123a7be4267152252ed39e724c7ec8b98ad9dee5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:55:10 +0100 Subject: [PATCH 079/103] refactor(cache): strip retry prefix in move_cache and simplify handle_result Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 26 +++++++++++++++++++++----- src/sentry_database.h | 7 ++++--- src/sentry_retry.c | 7 +------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 59d2e9bc8..0d869a325 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -197,14 +197,30 @@ sentry__run_write_cache( bool sentry__run_move_cache( - const sentry_run_t *run, const sentry_path_t *src, const char *dst) + const sentry_run_t *run, const sentry_path_t *src, int retry_count) { if (sentry__path_create_dir_all(run->cache_path) != 0) { SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); return false; } - const char *filename = dst ? dst : sentry__path_filename(src); + char filename[128]; + const char *src_name = sentry__path_filename(src); + if (retry_count >= 0) { + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", + sentry__usec_time() / 1000, retry_count, src_name); + } else { + // Strip the retry prefix if present. Envelope filenames are either + // ".envelope" (45 chars) or "--.envelope" + // (>45 chars). The last 45 chars are always ".envelope". + size_t len = strlen(src_name); + if (len > 45) { + snprintf(filename, sizeof(filename), "%s", src_name + len - 45); + } else { + snprintf(filename, sizeof(filename), "%s", src_name); + } + } + sentry_path_t *dst_path = sentry__path_join_str(run->cache_path, filename); if (!dst_path) { return false; @@ -213,10 +229,10 @@ sentry__run_move_cache( int rv = sentry__path_rename(src, dst_path); sentry__path_free(dst_path); if (rv != 0) { - SENTRY_WARNF( - "failed to cache envelope \"%s\"", sentry__path_filename(src)); + SENTRY_WARNF("failed to cache envelope \"%s\"", src_name); return false; } + return true; } @@ -358,7 +374,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry__capture_envelope(options->transport, envelope); if (can_cache - && sentry__run_move_cache(options->run, file, NULL)) { + && sentry__run_move_cache(options->run, file, -1)) { continue; } } diff --git a/src/sentry_database.h b/src/sentry_database.h index e461c25f3..0e0152423 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -79,11 +79,12 @@ bool sentry__run_write_cache(const sentry_run_t *run, const sentry_envelope_t *envelope, int retry_count); /** - * Moves `src` to `/cache/`. If `dst` is NULL, the filename of - * `src` is used. + * Moves a file into the cache directory. When retry_count >= 0 the + * destination uses retry format `--.envelope`, + * otherwise the original filename is preserved. */ bool sentry__run_move_cache( - const sentry_run_t *run, const sentry_path_t *src, const char *dst); + const sentry_run_t *run, const sentry_path_t *src, int retry_count); /** * This function is essential to send crash reports from previous runs of the diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 9f0cc2c00..7cca087ae 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -162,14 +162,9 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) // cache on last attempt if (exhausted && retry->cache_keep) { - char cache_name[46]; - snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); - sentry_path_t *dest - = sentry__path_join_str(retry->run->cache_path, cache_name); - if (!dest || sentry__path_rename(item->path, dest) != 0) { + if (!sentry__run_move_cache(retry->run, item->path, -1)) { sentry__path_remove(item->path); } - sentry__path_free(dest); return false; } From acfd148462bf81f174b4831747db8dc28bab1d2a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 18:01:07 +0100 Subject: [PATCH 080/103] fix(cache): use cache_name instead of src_name for UUID in move_cache The retry_count >= 0 branch passed the full source filename to %.36s, which would grab the timestamp prefix instead of the UUID for retry- format filenames. Extract the cache name (last 45 chars) before either branch so both use the correct UUID. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 0d869a325..c46c1dcaa 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -206,19 +206,16 @@ sentry__run_move_cache( char filename[128]; const char *src_name = sentry__path_filename(src); + // Strip the retry prefix if present. Envelope filenames are either + // ".envelope" (45 chars) or "--.envelope" + // (>45 chars). The last 45 chars are always ".envelope". + size_t src_len = strlen(src_name); + const char *cache_name = src_len > 45 ? src_name + src_len - 45 : src_name; if (retry_count >= 0) { snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", - sentry__usec_time() / 1000, retry_count, src_name); + sentry__usec_time() / 1000, retry_count, cache_name); } else { - // Strip the retry prefix if present. Envelope filenames are either - // ".envelope" (45 chars) or "--.envelope" - // (>45 chars). The last 45 chars are always ".envelope". - size_t len = strlen(src_name); - if (len > 45) { - snprintf(filename, sizeof(filename), "%s", src_name + len - 45); - } else { - snprintf(filename, sizeof(filename), "%s", src_name); - } + snprintf(filename, sizeof(filename), "%s", cache_name); } sentry_path_t *dst_path = sentry__path_join_str(run->cache_path, filename); From 019e0e9b0ba1196ed887dce6b21ca72b3fd7caae Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 08:47:05 +0100 Subject: [PATCH 081/103] fix(retry): prevent duplicate cache writes during shutdown race Use a mutex (sealed_lock) to serialize the SEALED check in retry_enqueue with the SEALED set in retry_dump_queue. Store the envelope address as uintptr_t so retry_dump_cb can skip envelopes already written by retry_enqueue without risking accidental dereferencing. The address is safe to compare because the task holds a ref that keeps the envelope alive during foreach_matching. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 7cca087ae..f10a98431 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -28,6 +28,8 @@ struct sentry_retry_s { sentry_bgworker_t *bgworker; sentry_retry_send_func_t send_cb; void *send_data; + sentry_mutex_t sealed_lock; + uintptr_t sealed_envelope; }; sentry_retry_t * @@ -38,6 +40,7 @@ sentry__retry_new(const sentry_options_t *options) return NULL; } memset(retry, 0, sizeof(sentry_retry_t)); + sentry__mutex_init(&retry->sealed_lock); retry->run = sentry__run_incref(options->run); retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; @@ -50,6 +53,7 @@ sentry__retry_free(sentry_retry_t *retry) if (!retry) { return; } + sentry__mutex_free(&retry->sealed_lock); sentry__run_free(retry->run); sentry_free(retry); } @@ -333,7 +337,10 @@ static bool retry_dump_cb(void *_envelope, void *_retry) { sentry_retry_t *retry = (sentry_retry_t *)_retry; - sentry__run_write_cache(retry->run, (sentry_envelope_t *)_envelope, 0); + sentry_envelope_t *envelope = (sentry_envelope_t *)_envelope; + if ((uintptr_t)envelope != retry->sealed_envelope) { + sentry__run_write_cache(retry->run, envelope, 0); + } return true; } @@ -343,7 +350,10 @@ sentry__retry_dump_queue( { if (retry) { // prevent duplicate writes from a still-running detached worker + sentry__mutex_lock(&retry->sealed_lock); sentry__atomic_store(&retry->state, SENTRY_RETRY_SEALED); + sentry__mutex_unlock(&retry->sealed_lock); + sentry__bgworker_foreach_matching( retry->bgworker, task_func, retry_dump_cb, retry); } @@ -366,12 +376,18 @@ sentry__retry_trigger(sentry_retry_t *retry) void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { + sentry__mutex_lock(&retry->sealed_lock); if (sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_SEALED) { + sentry__mutex_unlock(&retry->sealed_lock); return; } if (!sentry__run_write_cache(retry->run, envelope, 0)) { + sentry__mutex_unlock(&retry->sealed_lock); return; } + retry->sealed_envelope = (uintptr_t)envelope; + sentry__mutex_unlock(&retry->sealed_lock); + sentry__atomic_compare_swap( &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING); if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { From 0e83c47dc149ab7b79213a68b30fc6dca06b00f6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:35:50 +0100 Subject: [PATCH 082/103] fix(cache): replace length heuristic with proper filename parsing in move_cache Move sentry__retry_parse_filename and sentry__retry_make_path into sentry_database as sentry__parse_cache_filename and sentry__run_make_cache_path. This consolidates cache filename format knowledge in one module and replaces the fragile `src_len > 45` heuristic in sentry__run_move_cache with proper parsing. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 57 +++++++++++++++++++++++++++++++++++++---- src/sentry_database.h | 13 ++++++++++ src/sentry_retry.c | 51 +++--------------------------------- src/sentry_retry.h | 12 --------- tests/unit/test_retry.c | 44 +++++++++++++++---------------- 5 files changed, 90 insertions(+), 87 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index c46c1dcaa..a7114e3bc 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -195,6 +195,51 @@ sentry__run_write_cache( return write_envelope(run->cache_path, envelope, retry_count); } +bool +sentry__parse_cache_filename(const char *filename, uint64_t *ts_out, + int *count_out, const char **uuid_out) +{ + // Minimum retry filename: --.envelope (49+ chars). + // Cache filenames are exactly 45 chars (.envelope). + if (strlen(filename) <= 45) { + return false; + } + + char *end; + uint64_t ts = strtoull(filename, &end, 10); + if (*end != '-') { + return false; + } + + const char *count_str = end + 1; + long count = strtol(count_str, &end, 10); + if (*end != '-' || count < 0) { + return false; + } + + const char *uuid = end + 1; + size_t tail_len = strlen(uuid); + // 36 chars UUID (with dashes) + ".envelope" + if (tail_len != 36 + 9 || strcmp(uuid + 36, ".envelope") != 0) { + return false; + } + + *ts_out = ts; + *count_out = (int)count; + *uuid_out = uuid; + return true; +} + +sentry_path_t * +sentry__run_make_cache_path( + const sentry_run_t *run, uint64_t ts, int count, const char *uuid) +{ + char filename[128]; + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, + count, uuid); + return sentry__path_join_str(run->cache_path, filename); +} + bool sentry__run_move_cache( const sentry_run_t *run, const sentry_path_t *src, int retry_count) @@ -206,11 +251,13 @@ sentry__run_move_cache( char filename[128]; const char *src_name = sentry__path_filename(src); - // Strip the retry prefix if present. Envelope filenames are either - // ".envelope" (45 chars) or "--.envelope" - // (>45 chars). The last 45 chars are always ".envelope". - size_t src_len = strlen(src_name); - const char *cache_name = src_len > 45 ? src_name + src_len - 45 : src_name; + uint64_t parsed_ts; + int parsed_count; + const char *parsed_uuid; + const char *cache_name = sentry__parse_cache_filename(src_name, &parsed_ts, + &parsed_count, &parsed_uuid) + ? parsed_uuid + : src_name; if (retry_count >= 0) { snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", sentry__usec_time() / 1000, retry_count, cache_name); diff --git a/src/sentry_database.h b/src/sentry_database.h index 0e0152423..0e355f482 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -86,6 +86,12 @@ bool sentry__run_write_cache(const sentry_run_t *run, bool sentry__run_move_cache( const sentry_run_t *run, const sentry_path_t *src, int retry_count); +/** + * Builds a cache path: `/cache/--.envelope`. + */ +sentry_path_t *sentry__run_make_cache_path( + const sentry_run_t *run, uint64_t ts, int count, const char *uuid); + /** * This function is essential to send crash reports from previous runs of the * program. @@ -101,6 +107,13 @@ bool sentry__run_move_cache( void sentry__process_old_runs( const sentry_options_t *options, uint64_t last_crash); +/** + * Parses a retry cache filename: `--.envelope`. + * Returns false for plain cache filenames (`.envelope`). + */ +bool sentry__parse_cache_filename(const char *filename, uint64_t *ts_out, + int *count_out, const char **uuid_out); + /** * Cleans up the cache based on options.cache_max_items, * options.cache_max_size and options.cache_max_age. diff --git a/src/sentry_retry.c b/src/sentry_retry.c index f10a98431..bff474963 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -58,41 +58,6 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } -bool -sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, - int *count_out, const char **uuid_out) -{ - // Minimum retry filename: --.envelope (49+ chars). - // Cache filenames are exactly 45 chars (.envelope). - if (strlen(filename) <= 45) { - return false; - } - - char *end; - uint64_t ts = strtoull(filename, &end, 10); - if (*end != '-') { - return false; - } - - const char *count_str = end + 1; - long count = strtol(count_str, &end, 10); - if (*end != '-' || count < 0) { - return false; - } - - const char *uuid = end + 1; - size_t tail_len = strlen(uuid); - // 36 chars UUID (with dashes) + ".envelope" - if (tail_len != 36 + 9 || strcmp(uuid + 36, ".envelope") != 0) { - return false; - } - - *ts_out = ts; - *count_out = (int)count; - *uuid_out = uuid; - return true; -} - uint64_t sentry__retry_backoff(int count) { @@ -120,16 +85,6 @@ compare_retry_items(const void *a, const void *b) return strcmp(ia->uuid, ib->uuid); } -sentry_path_t * -sentry__retry_make_path( - sentry_retry_t *retry, uint64_t ts, int count, const char *uuid) -{ - char filename[128]; - snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, - count, uuid); - return sentry__path_join_str(retry->run->cache_path, filename); -} - static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { @@ -139,8 +94,8 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) // network failure with retries remaining: bump count & re-enqueue if (item->count + 1 < SENTRY_RETRY_ATTEMPTS && status_code < 0) { - sentry_path_t *new_path = sentry__retry_make_path( - retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); + sentry_path_t *new_path = sentry__run_make_cache_path(retry->run, + sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { if (sentry__path_rename(item->path, new_path) != 0) { SENTRY_WARNF( @@ -203,7 +158,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, uint64_t ts; int count; const char *uuid; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { + if (!sentry__parse_cache_filename(fname, &ts, &count, &uuid)) { continue; } if (before > 0 && ts >= before) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index edde49d8f..5e5222097 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -49,18 +49,6 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, */ uint64_t sentry__retry_backoff(int count); -/** - * /cache/--.envelope - */ -sentry_path_t *sentry__retry_make_path( - sentry_retry_t *retry, uint64_t ts, int count, const char *uuid); - -/** - * --.envelope - */ -bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, - int *count_out, const char **uuid_out); - /** * Submits a delayed retry poll task on the background worker. */ diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index b5edb383e..94c4f638a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -40,7 +40,7 @@ find_envelope_attempt(const sentry_path_t *dir) uint64_t ts; int attempt; const char *uuid; - if (sentry__retry_parse_filename(name, &ts, &attempt, &uuid)) { + if (sentry__parse_cache_filename(name, &ts, &attempt, &uuid)) { sentry__pathiter_free(iter); return attempt; } @@ -50,7 +50,7 @@ find_envelope_attempt(const sentry_path_t *dir) } static void -write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, +write_retry_file(const sentry_run_t *run, uint64_t timestamp, int retry_count, const sentry_uuid_t *event_id) { sentry_envelope_t *envelope = sentry__envelope_new(); @@ -61,7 +61,7 @@ write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, sentry_uuid_as_string(event_id, uuid); sentry_path_t *path - = sentry__retry_make_path(retry, timestamp, retry_count, uuid); + = sentry__run_make_cache_path(run, timestamp, retry_count, uuid); (void)sentry_envelope_write_to_path(envelope, path); sentry__path_free(path); sentry_envelope_free(envelope); @@ -87,30 +87,30 @@ SENTRY_TEST(retry_filename) int count; const char *uuid; - TEST_CHECK(sentry__retry_parse_filename( + TEST_CHECK(sentry__parse_cache_filename( "1234567890-00-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); TEST_CHECK_UINT64_EQUAL(ts, 1234567890); TEST_CHECK_INT_EQUAL(count, 0); TEST_CHECK(strncmp(uuid, "abcdefab-1234-5678-9abc-def012345678", 36) == 0); - TEST_CHECK(sentry__retry_parse_filename( + TEST_CHECK(sentry__parse_cache_filename( "999-04-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); TEST_CHECK_UINT64_EQUAL(ts, 999); TEST_CHECK_INT_EQUAL(count, 4); // negative count - TEST_CHECK(!sentry__retry_parse_filename( + TEST_CHECK(!sentry__parse_cache_filename( "123--01-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); // cache filename (no timestamp/count) - TEST_CHECK(!sentry__retry_parse_filename( + TEST_CHECK(!sentry__parse_cache_filename( "abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); // missing .envelope suffix - TEST_CHECK(!sentry__retry_parse_filename( + TEST_CHECK(!sentry__parse_cache_filename( "123-00-abcdefab-1234-5678-9abc-def012345678.txt", &ts, &count, &uuid)); } @@ -132,7 +132,7 @@ SENTRY_TEST(retry_throttle) sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); - write_retry_file(retry, old_ts, 0, &ids[i]); + write_retry_file(options->run, old_ts, 0, &ids[i]); } TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 4); @@ -162,7 +162,7 @@ SENTRY_TEST(retry_skew) // future timestamp simulates clock moving backward uint64_t future_ts = sentry__usec_time() / 1000 + 1000000; sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry, future_ts, 0, &event_id); + write_retry_file(options->run, future_ts, 0, &event_id); retry_test_ctx_t ctx = { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); @@ -193,7 +193,7 @@ SENTRY_TEST(retry_result) sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 0); @@ -203,21 +203,21 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 2. Rate limited (429) → removes - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { 429, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 3. Discard (0) → removes - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { 0, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 4. Network error → bumps count - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 0); ctx = (retry_test_ctx_t) { -1, 0 }; @@ -231,7 +231,7 @@ SENTRY_TEST(retry_result) sentry__path_create_dir_all(cache_path); uint64_t very_old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); - write_retry_file(retry, very_old_ts, 5, &event_id); + write_retry_file(options->run, very_old_ts, 5, &event_id); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); @@ -287,7 +287,7 @@ SENTRY_TEST(retry_cache) uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry, old_ts, 5, &event_id); + write_retry_file(options->run, old_ts, 5, &event_id); char uuid_str[37]; sentry_uuid_as_string(&event_id, uuid_str); @@ -310,7 +310,7 @@ SENTRY_TEST(retry_cache) // (cache_keep preserves all envelopes regardless of send outcome) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); - write_retry_file(retry, old_ts, 5, &event_id); + write_retry_file(options->run, old_ts, 5, &event_id); TEST_CHECK(!sentry__path_is_file(cached)); ctx = (retry_test_ctx_t) { 200, 0 }; @@ -380,19 +380,19 @@ SENTRY_TEST(retry_backoff) // retry 0: 10*base old, eligible (backoff=base) sentry_uuid_t id1 = sentry_uuid_new_v4(); - write_retry_file(retry, ref, 0, &id1); + write_retry_file(options->run, ref, 0, &id1); // retry 1: 1*base old, not yet eligible (backoff=2*base) sentry_uuid_t id2 = sentry_uuid_new_v4(); - write_retry_file(retry, ref + 9 * base, 1, &id2); + write_retry_file(options->run, ref + 9 * base, 1, &id2); // retry 1: 10*base old, eligible (backoff=2*base) sentry_uuid_t id3 = sentry_uuid_new_v4(); - write_retry_file(retry, ref, 1, &id3); + write_retry_file(options->run, ref, 1, &id3); // retry 2: 2*base old, not eligible (backoff=4*base) sentry_uuid_t id4 = sentry_uuid_new_v4(); - write_retry_file(retry, ref + 8 * base, 2, &id4); + write_retry_file(options->run, ref + 8 * base, 2, &id4); // With backoff: only eligible ones (id1 and id3) are sent retry_test_ctx_t ctx = { 200, 0 }; @@ -437,7 +437,7 @@ SENTRY_TEST(retry_trigger) uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); // UINT64_MAX (trigger mode) bypasses backoff: bumps count retry_test_ctx_t ctx = { -1, 0 }; From 5f8e4526314142de1021e835571ca8df004f410b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:43:56 +0100 Subject: [PATCH 083/103] =?UTF-8?q?docs:=20fix=20retry=20count=205=20?= =?UTF-8?q?=E2=86=92=206=20in=20sentry=5Ftransport=5Fretry=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SENTRY_RETRY_ATTEMPTS constant was bumped from 5 to 6 in 81d0f68a but the public API documentation was not updated to match. Co-Authored-By: Claude Opus 4.6 --- include/sentry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/sentry.h b/include/sentry.h index ae5df82bb..00bc3cab4 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -952,7 +952,7 @@ SENTRY_API void sentry_transport_set_shutdown_func( * * Note: The SDK automatically retries failed envelopes on next application * startup. This function allows manual triggering of pending retries at - * runtime. Each envelope is retried up to 5 times. If all attempts are + * runtime. Each envelope is retried up to 6 times. If all attempts are * exhausted during intermittent connectivity, events will be discarded * (or moved to cache if enabled via sentry_options_set_cache_keep). * From 69ffc09ef5fb03bea56b7e91743ba4ec09e8b63b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:47:16 +0100 Subject: [PATCH 084/103] fix(retry): prevent poll task from re-arming after shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use a SENTRY_POLL_SHUTDOWN sentinel so that a concurrent retry_poll_task cannot resubmit the delayed poll that shutdown just dropped. The CAS(SCHEDULED→IDLE) in retry_poll_task is a no-op when scheduled is SHUTDOWN, and the subsequent CAS(IDLE→SCHEDULED) also fails. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index bff474963..74502d2a4 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -19,6 +19,12 @@ typedef enum { SENTRY_RETRY_SEALED = 2 } sentry_retry_state_t; +typedef enum { + SENTRY_POLL_IDLE = 0, + SENTRY_POLL_SCHEDULED = 1, + SENTRY_POLL_SHUTDOWN = 2 +} sentry_poll_state_t; + struct sentry_retry_s { sentry_run_t *run; bool cache_keep; @@ -233,10 +239,12 @@ retry_poll_task(void *_retry, void *_state) = sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_STARTUP ? retry->startup_time : 0; - // clear before scanning so a concurrent enqueue sees 0 and arms a poll - sentry__atomic_store(&retry->scheduled, 0); + // CAS instead of unconditional store to preserve SENTRY_POLL_SHUTDOWN + sentry__atomic_compare_swap( + &retry->scheduled, SENTRY_POLL_SCHEDULED, SENTRY_POLL_IDLE); if (sentry__retry_send(retry, before, retry->send_cb, retry->send_data) - && sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { + && sentry__atomic_compare_swap( + &retry->scheduled, SENTRY_POLL_IDLE, SENTRY_POLL_SCHEDULED)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } @@ -252,7 +260,7 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, retry->bgworker = bgworker; retry->send_cb = send_cb; retry->send_data = send_data; - sentry__atomic_store(&retry->scheduled, 1); + sentry__atomic_store(&retry->scheduled, SENTRY_POLL_SCHEDULED); sentry__bgworker_submit_delayed( bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); } @@ -280,10 +288,10 @@ void sentry__retry_shutdown(sentry_retry_t *retry) { if (retry) { - // drop the delayed poll that would stall bgworker_flush + // drop the delayed poll and prevent retry_poll_task from re-arming sentry__bgworker_foreach_matching( retry->bgworker, retry_poll_task, drop_task_cb, NULL); - sentry__atomic_store(&retry->scheduled, 0); + sentry__atomic_store(&retry->scheduled, SENTRY_POLL_SHUTDOWN); sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); } } @@ -345,7 +353,8 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) sentry__atomic_compare_swap( &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING); - if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { + if (sentry__atomic_compare_swap( + &retry->scheduled, SENTRY_POLL_IDLE, SENTRY_POLL_SCHEDULED)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } From e50dcf571645986664f5e6ae403d03a8d22fdfea Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 14:40:00 +0100 Subject: [PATCH 085/103] fix(winhttp): cancel in-flight request before shutdown to unblock worker On Windows, WinHTTP TCP connect to an unreachable host takes ~2s, which can exceed the shutdown timeout. Add a cancel_client callback that closes just the WinHTTP request handle, unblocking the worker thread so it can process the failure and shut down cleanly. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 12 ++++++++++++ src/transports/sentry_http_transport.h | 2 ++ src/transports/sentry_http_transport_winhttp.c | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 230df598b..aa7574f33 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -30,6 +30,7 @@ typedef struct { void (*free_client)(void *); int (*start_client)(void *, const sentry_options_t *); sentry_http_send_func_t send_func; + void (*cancel_client)(void *client); void (*shutdown_client)(void *client); sentry_retry_t *retry; } http_transport_state_t; @@ -311,6 +312,10 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry__retry_shutdown(state->retry); + if (state->cancel_client) { + state->cancel_client(state->client); + } + int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); @@ -414,6 +419,13 @@ sentry__http_transport_set_start_client(sentry_transport_t *transport, http_transport_get_state(transport)->start_client = start_client; } +void +sentry__http_transport_set_cancel_client( + sentry_transport_t *transport, void (*cancel_client)(void *)) +{ + http_transport_get_state(transport)->cancel_client = cancel_client; +} + void sentry__http_transport_set_shutdown_client( sentry_transport_t *transport, void (*shutdown_client)(void *)) diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index 30493d564..b186b9eeb 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -47,6 +47,8 @@ void sentry__http_transport_set_free_client( sentry_transport_t *transport, void (*free_client)(void *)); void sentry__http_transport_set_start_client(sentry_transport_t *transport, int (*start_client)(void *, const sentry_options_t *)); +void sentry__http_transport_set_cancel_client( + sentry_transport_t *transport, void (*cancel_client)(void *)); void sentry__http_transport_set_shutdown_client( sentry_transport_t *transport, void (*shutdown_client)(void *)); diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index e73de7405..e086af017 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -140,6 +140,16 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 0; } +static void +winhttp_client_cancel(void *_client) +{ + winhttp_client_t *client = _client; + if (client->request) { + WinHttpCloseHandle(client->request); + client->request = NULL; + } +} + static void winhttp_client_shutdown(void *_client) { @@ -333,6 +343,7 @@ sentry__transport_new_default(void) } sentry__http_transport_set_free_client(transport, winhttp_client_free); sentry__http_transport_set_start_client(transport, winhttp_client_start); + sentry__http_transport_set_cancel_client(transport, winhttp_client_cancel); sentry__http_transport_set_shutdown_client( transport, winhttp_client_shutdown); return transport; From db196795759a43e3ba83b5d6f00935f12b628f82 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 14:54:37 +0100 Subject: [PATCH 086/103] fix(winhttp): fix double-close race on client->request between cancel and worker Use InterlockedExchangePointer to atomically swap client->request to NULL in cancel, shutdown, and worker exit cleanup. Whichever thread wins the swap closes the handle; the loser gets NULL and skips. The worker also snapshots client->request into a local variable right after WinHttpOpenRequest and uses the local for all subsequent API calls, so it never reads NULL from the struct if cancel fires mid-function. Co-Authored-By: Claude Opus 4.6 --- .../sentry_http_transport_winhttp.c | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index e086af017..dcd6d3c98 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -144,9 +144,9 @@ static void winhttp_client_cancel(void *_client) { winhttp_client_t *client = _client; - if (client->request) { - WinHttpCloseHandle(client->request); - client->request = NULL; + HINTERNET request; + if ((request = InterlockedExchangePointer(&client->request, NULL))) { + WinHttpCloseHandle(request); } } @@ -167,9 +167,9 @@ winhttp_client_shutdown(void *_client) WinHttpCloseHandle(client->session); client->session = NULL; } - if (client->request) { - WinHttpCloseHandle(client->request); - client->request = NULL; + HINTERNET request; + if ((request = InterlockedExchangePointer(&client->request, NULL))) { + WinHttpCloseHandle(request); } } @@ -218,7 +218,8 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, client->request = WinHttpOpenRequest(client->connect, L"POST", url_components.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, is_secure ? WINHTTP_FLAG_SECURE : 0); - if (!client->request) { + HINTERNET request = client->request; + if (!request) { SENTRY_WARNF( "`WinHttpOpenRequest` failed with code `%d`", GetLastError()); goto exit; @@ -249,22 +250,22 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, } if (client->proxy_username && client->proxy_password) { - WinHttpSetCredentials(client->request, WINHTTP_AUTH_TARGET_PROXY, + WinHttpSetCredentials(request, WINHTTP_AUTH_TARGET_PROXY, WINHTTP_AUTH_SCHEME_BASIC, client->proxy_username, client->proxy_password, 0); } - if ((result = WinHttpSendRequest(client->request, headers, (DWORD)-1, - (LPVOID)req->body, (DWORD)req->body_len, (DWORD)req->body_len, - 0))) { - WinHttpReceiveResponse(client->request, NULL); + if ((result + = WinHttpSendRequest(request, headers, (DWORD)-1, (LPVOID)req->body, + (DWORD)req->body_len, (DWORD)req->body_len, 0))) { + WinHttpReceiveResponse(request, NULL); if (client->debug) { // this is basically the example from: // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpqueryheaders#examples DWORD dwSize = 0; LPVOID lpOutBuffer = NULL; - WinHttpQueryHeaders(client->request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, WINHTTP_NO_HEADER_INDEX); @@ -274,7 +275,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, // Now, use WinHttpQueryHeaders to retrieve the header. if (lpOutBuffer - && WinHttpQueryHeaders(client->request, + && WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, lpOutBuffer, &dwSize, WINHTTP_NO_HEADER_INDEX)) { @@ -292,17 +293,17 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, DWORD status_code = 0; DWORD status_code_size = sizeof(status_code); - WinHttpQueryHeaders(client->request, + WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX); resp->status_code = (int)status_code; - if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, + if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, L"x-sentry-rate-limits", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { resp->x_sentry_rate_limits = sentry__string_from_wstr(buf); - } else if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, + } else if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, L"retry-after", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { resp->retry_after = sentry__string_from_wstr(buf); @@ -316,9 +317,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, SENTRY_DEBUGF("request handled in %llums", now - started); exit: - if (client->request) { - HINTERNET request = client->request; - client->request = NULL; + if ((request = InterlockedExchangePointer(&client->request, NULL))) { WinHttpCloseHandle(request); } sentry_free(url); From 898308db578a66126b22bbcf30f7ca17b83c79f1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 17:37:10 +0100 Subject: [PATCH 087/103] fix(winhttp): use on_timeout callback to unblock worker instead of cancel Replace the unconditional cancel_client call before bgworker_shutdown with an on_timeout callback that fires only when the shutdown timeout expires. This avoids aborting in-flight requests that would have completed within the timeout, while still unblocking the worker when it's stuck (e.g. WinHTTP connect to unreachable host). The callback closes session/connect handles to cancel pending WinHTTP operations, then the shutdown loop falls through to the !running check which joins the worker thread. This ensures handle_result runs and the retry counter is properly bumped on disk. Co-Authored-By: Claude Opus 4.6 --- src/sentry_sync.c | 23 +++++++++++----- src/sentry_sync.h | 14 +++++++++- src/transports/sentry_http_transport.c | 27 ++++++++----------- src/transports/sentry_http_transport.h | 2 -- .../sentry_http_transport_winhttp.c | 11 -------- 5 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index c0d903f03..350235a7a 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -425,7 +425,8 @@ shutdown_task(void *task_data, void *UNUSED(state)) } int -sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout) +sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, + void (*on_timeout)(void *), void *on_timeout_data) { if (!sentry__atomic_fetch(&bgw->running)) { SENTRY_WARN("trying to shut down non-running thread"); @@ -442,11 +443,21 @@ sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout) uint64_t now = sentry__monotonic_time(); if (now > started && now - started > timeout) { sentry__atomic_store(&bgw->running, 0); - sentry__thread_detach(bgw->thread_id); - sentry__mutex_unlock(&bgw->task_lock); - SENTRY_WARN( - "background thread failed to shut down cleanly within timeout"); - return 1; + if (on_timeout) { + // Unblock the worker (e.g. close transport handles) and + // let it finish in-flight work like handle_result. + sentry__mutex_unlock(&bgw->task_lock); + on_timeout(on_timeout_data); + on_timeout = NULL; + sentry__mutex_lock(&bgw->task_lock); + // fall through to !running check below + } else { + sentry__thread_detach(bgw->thread_id); + sentry__mutex_unlock(&bgw->task_lock); + SENTRY_WARN("background thread failed to shut down cleanly " + "within timeout"); + return 1; + } } if (!sentry__atomic_fetch(&bgw->running)) { diff --git a/src/sentry_sync.h b/src/sentry_sync.h index eab47cd59..d4f6001e7 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -469,8 +469,20 @@ int sentry__bgworker_flush(sentry_bgworker_t *bgw, uint64_t timeout); /** * This will try to shut down the background worker thread, with a `timeout`. * Returns 0 on success. + * + * The `_cb` variant accepts an `on_timeout` callback that is invoked when + * the timeout expires, just before detaching the thread. This gives the + * caller a chance to unblock the worker (e.g. by closing transport handles) + * so it can finish in-flight work. */ -int sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout); +int sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, + void (*on_timeout)(void *), void *on_timeout_data); + +static inline int +sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout) +{ + return sentry__bgworker_shutdown_cb(bgw, timeout, NULL, NULL); +} /** * This will set a preferable thread name for background worker. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index aa7574f33..237052689 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -30,7 +30,6 @@ typedef struct { void (*free_client)(void *); int (*start_client)(void *, const sentry_options_t *); sentry_http_send_func_t send_func; - void (*cancel_client)(void *client); void (*shutdown_client)(void *client); sentry_retry_t *retry; } http_transport_state_t; @@ -260,6 +259,15 @@ http_send_task(void *_envelope, void *_state) } } +static void +http_transport_shutdown_timeout(void *_state) +{ + http_transport_state_t *state = _state; + if (state->shutdown_client) { + state->shutdown_client(state->client); + } +} + static int http_transport_start(const sentry_options_t *options, void *transport_state) { @@ -312,16 +320,10 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry__retry_shutdown(state->retry); - if (state->cancel_client) { - state->cancel_client(state->client); - } - - int rv = sentry__bgworker_shutdown(bgworker, timeout); + int rv = sentry__bgworker_shutdown_cb( + bgworker, timeout, http_transport_shutdown_timeout, state); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); - if (state->shutdown_client) { - state->shutdown_client(state->client); - } } return rv; } @@ -419,13 +421,6 @@ sentry__http_transport_set_start_client(sentry_transport_t *transport, http_transport_get_state(transport)->start_client = start_client; } -void -sentry__http_transport_set_cancel_client( - sentry_transport_t *transport, void (*cancel_client)(void *)) -{ - http_transport_get_state(transport)->cancel_client = cancel_client; -} - void sentry__http_transport_set_shutdown_client( sentry_transport_t *transport, void (*shutdown_client)(void *)) diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index b186b9eeb..30493d564 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -47,8 +47,6 @@ void sentry__http_transport_set_free_client( sentry_transport_t *transport, void (*free_client)(void *)); void sentry__http_transport_set_start_client(sentry_transport_t *transport, int (*start_client)(void *, const sentry_options_t *)); -void sentry__http_transport_set_cancel_client( - sentry_transport_t *transport, void (*cancel_client)(void *)); void sentry__http_transport_set_shutdown_client( sentry_transport_t *transport, void (*shutdown_client)(void *)); diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index dcd6d3c98..6783cdf65 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -140,16 +140,6 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 0; } -static void -winhttp_client_cancel(void *_client) -{ - winhttp_client_t *client = _client; - HINTERNET request; - if ((request = InterlockedExchangePointer(&client->request, NULL))) { - WinHttpCloseHandle(request); - } -} - static void winhttp_client_shutdown(void *_client) { @@ -342,7 +332,6 @@ sentry__transport_new_default(void) } sentry__http_transport_set_free_client(transport, winhttp_client_free); sentry__http_transport_set_start_client(transport, winhttp_client_start); - sentry__http_transport_set_cancel_client(transport, winhttp_client_cancel); sentry__http_transport_set_shutdown_client( transport, winhttp_client_shutdown); return transport; From 7db327459c41241f97c8a66c2493a14b52bcf362 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 17:53:56 +0100 Subject: [PATCH 088/103] fix(winhttp): remove unnecessary local request snapshot in winhttp_send_task The local `HINTERNET request = client->request` snapshot was only needed for the cancel_client approach. Since shutdown_client only fires at the timeout point, mid-function reads of client->request are safe. Keep only the InterlockedExchangePointer in the exit block to prevent double-close. Co-Authored-By: Claude Opus 4.6 --- .../sentry_http_transport_winhttp.c | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index 6783cdf65..f358fa6cb 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -157,8 +157,8 @@ winhttp_client_shutdown(void *_client) WinHttpCloseHandle(client->session); client->session = NULL; } - HINTERNET request; - if ((request = InterlockedExchangePointer(&client->request, NULL))) { + HINTERNET request = InterlockedExchangePointer(&client->request, NULL); + if (request) { WinHttpCloseHandle(request); } } @@ -208,8 +208,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, client->request = WinHttpOpenRequest(client->connect, L"POST", url_components.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, is_secure ? WINHTTP_FLAG_SECURE : 0); - HINTERNET request = client->request; - if (!request) { + if (!client->request) { SENTRY_WARNF( "`WinHttpOpenRequest` failed with code `%d`", GetLastError()); goto exit; @@ -240,22 +239,22 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, } if (client->proxy_username && client->proxy_password) { - WinHttpSetCredentials(request, WINHTTP_AUTH_TARGET_PROXY, + WinHttpSetCredentials(client->request, WINHTTP_AUTH_TARGET_PROXY, WINHTTP_AUTH_SCHEME_BASIC, client->proxy_username, client->proxy_password, 0); } - if ((result - = WinHttpSendRequest(request, headers, (DWORD)-1, (LPVOID)req->body, - (DWORD)req->body_len, (DWORD)req->body_len, 0))) { - WinHttpReceiveResponse(request, NULL); + if ((result = WinHttpSendRequest(client->request, headers, (DWORD)-1, + (LPVOID)req->body, (DWORD)req->body_len, (DWORD)req->body_len, + 0))) { + WinHttpReceiveResponse(client->request, NULL); if (client->debug) { // this is basically the example from: // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpqueryheaders#examples DWORD dwSize = 0; LPVOID lpOutBuffer = NULL; - WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WinHttpQueryHeaders(client->request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, WINHTTP_NO_HEADER_INDEX); @@ -265,7 +264,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, // Now, use WinHttpQueryHeaders to retrieve the header. if (lpOutBuffer - && WinHttpQueryHeaders(request, + && WinHttpQueryHeaders(client->request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, lpOutBuffer, &dwSize, WINHTTP_NO_HEADER_INDEX)) { @@ -283,17 +282,17 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, DWORD status_code = 0; DWORD status_code_size = sizeof(status_code); - WinHttpQueryHeaders(request, + WinHttpQueryHeaders(client->request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX); resp->status_code = (int)status_code; - if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, + if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, L"x-sentry-rate-limits", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { resp->x_sentry_rate_limits = sentry__string_from_wstr(buf); - } else if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, + } else if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, L"retry-after", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { resp->retry_after = sentry__string_from_wstr(buf); @@ -306,8 +305,9 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, uint64_t now = sentry__monotonic_time(); SENTRY_DEBUGF("request handled in %llums", now - started); -exit: - if ((request = InterlockedExchangePointer(&client->request, NULL))) { +exit:; + HINTERNET request = InterlockedExchangePointer(&client->request, NULL); + if (request) { WinHttpCloseHandle(request); } sentry_free(url); From 5b3277993de1091cc8907a3eb26bdfb74bc5482b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 18:01:02 +0100 Subject: [PATCH 089/103] fix(sync): don't force running=0 before on_timeout callback Move sentry__atomic_store(&bgw->running, 0) from before the on_timeout callback to the else branch (detach path). This lets the worker's shutdown_task set running=0 naturally after finishing in-flight work, making the dump_queue safety-net reachable if the callback fails to unblock the worker within another 250ms cycle. Co-Authored-By: Claude Opus 4.6 --- src/sentry_sync.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 350235a7a..8e9067b99 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -442,7 +442,6 @@ sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, while (true) { uint64_t now = sentry__monotonic_time(); if (now > started && now - started > timeout) { - sentry__atomic_store(&bgw->running, 0); if (on_timeout) { // Unblock the worker (e.g. close transport handles) and // let it finish in-flight work like handle_result. @@ -452,6 +451,7 @@ sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, sentry__mutex_lock(&bgw->task_lock); // fall through to !running check below } else { + sentry__atomic_store(&bgw->running, 0); sentry__thread_detach(bgw->thread_id); sentry__mutex_unlock(&bgw->task_lock); SENTRY_WARN("background thread failed to shut down cleanly " From 9c8d803dd26b293f8a283e36221ae0f295f5f519 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 18:50:13 +0100 Subject: [PATCH 090/103] fix(test): disable transport retry in unit tests to fix valgrind flakiness Tests that directly call sentry__retry_send were racing with the transport's background retry worker polling the same cache directory. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 94c4f638a..d707dd162 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -118,7 +118,7 @@ SENTRY_TEST(retry_throttle) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -150,7 +150,7 @@ SENTRY_TEST(retry_skew) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -178,7 +178,7 @@ SENTRY_TEST(retry_result) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -246,7 +246,7 @@ SENTRY_TEST(retry_session) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_release(options, "test@1.0.0"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -274,7 +274,7 @@ SENTRY_TEST(retry_cache) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_options_set_cache_keep(options, 1); sentry_init(options); @@ -365,7 +365,7 @@ SENTRY_TEST(retry_backoff) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -424,7 +424,7 @@ SENTRY_TEST(retry_trigger) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); From a91842a48b61fb470e05bf51ce114ad66d1cb206 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 19:23:21 +0100 Subject: [PATCH 091/103] fix(retry): clear sealed_envelope after match to prevent address-reuse data loss After retry_enqueue writes an envelope and stores its address in sealed_envelope, the envelope is freed when the bgworker task completes. If a subsequent envelope is allocated at the same address and is still pending during shutdown, retry_dump_cb would incorrectly skip it, losing the envelope. Clear sealed_envelope after the first match so later iterations cannot false-match a reused address. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 74502d2a4..6e2509a34 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -303,6 +303,8 @@ retry_dump_cb(void *_envelope, void *_retry) sentry_envelope_t *envelope = (sentry_envelope_t *)_envelope; if ((uintptr_t)envelope != retry->sealed_envelope) { sentry__run_write_cache(retry->run, envelope, 0); + } else { + retry->sealed_envelope = 0; } return true; } From c17329ad1cbd353feeed62d121640d0c5ebe1e88 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Mar 2026 14:38:56 +0100 Subject: [PATCH 092/103] ref: consolidate filename formatting into make_cache_path Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 18 ++++++++---------- src/sentry_database.h | 4 +++- tests/unit/test_retry.c | 28 ++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index a7114e3bc..e0826ceba 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -235,8 +235,12 @@ sentry__run_make_cache_path( const sentry_run_t *run, uint64_t ts, int count, const char *uuid) { char filename[128]; - snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, - count, uuid); + if (count >= 0) { + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", + ts, count, uuid); + } else { + snprintf(filename, sizeof(filename), "%.36s.envelope", uuid); + } return sentry__path_join_str(run->cache_path, filename); } @@ -249,7 +253,6 @@ sentry__run_move_cache( return false; } - char filename[128]; const char *src_name = sentry__path_filename(src); uint64_t parsed_ts; int parsed_count; @@ -258,14 +261,9 @@ sentry__run_move_cache( &parsed_count, &parsed_uuid) ? parsed_uuid : src_name; - if (retry_count >= 0) { - snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", - sentry__usec_time() / 1000, retry_count, cache_name); - } else { - snprintf(filename, sizeof(filename), "%s", cache_name); - } - sentry_path_t *dst_path = sentry__path_join_str(run->cache_path, filename); + sentry_path_t *dst_path = sentry__run_make_cache_path( + run, sentry__usec_time() / 1000, retry_count, cache_name); if (!dst_path) { return false; } diff --git a/src/sentry_database.h b/src/sentry_database.h index 0e355f482..33e6735ce 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -87,7 +87,9 @@ bool sentry__run_move_cache( const sentry_run_t *run, const sentry_path_t *src, int retry_count); /** - * Builds a cache path: `/cache/--.envelope`. + * Builds a cache path. When count >= 0 the result is + * `/cache/--.envelope`, otherwise + * `/cache/.envelope`. */ sentry_path_t *sentry__run_make_cache_path( const sentry_run_t *run, uint64_t ts, int count, const char *uuid); diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index d707dd162..80dadc153 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -114,6 +114,34 @@ SENTRY_TEST(retry_filename) "123-00-abcdefab-1234-5678-9abc-def012345678.txt", &ts, &count, &uuid)); } +SENTRY_TEST(retry_make_cache_path) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#endif + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_http_retry(options, false); + sentry_init(options); + + const char *uuid = "abcdefab-1234-5678-9abc-def012345678"; + + // count >= 0 → retry format + sentry_path_t *path + = sentry__run_make_cache_path(options->run, 1000, 2, uuid); + TEST_CHECK_STRING_EQUAL(sentry__path_filename(path), + "1000-02-abcdefab-1234-5678-9abc-def012345678.envelope"); + sentry__path_free(path); + + // count < 0 → cache format + path = sentry__run_make_cache_path(options->run, 0, -1, uuid); + TEST_CHECK_STRING_EQUAL(sentry__path_filename(path), + "abcdefab-1234-5678-9abc-def012345678.envelope"); + sentry__path_free(path); + + sentry_close(); +} + SENTRY_TEST(retry_throttle) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 2318ff8be..97269c1d3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -195,6 +195,7 @@ XX(recursive_paths) XX(retry_backoff) XX(retry_cache) XX(retry_filename) +XX(retry_make_cache_path) XX(retry_result) XX(retry_session) XX(retry_skew) From 3461f52462c303c72d8cb3f79df6b8628ccfa75d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 9 Mar 2026 17:23:06 +0100 Subject: [PATCH 093/103] Adapt to sentry__session_new() change --- tests/unit/test_retry.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 80dadc153..8d099a875 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -4,6 +4,7 @@ #include "sentry_options.h" #include "sentry_path.h" #include "sentry_retry.h" +#include "sentry_scope.h" #include "sentry_session.h" #include "sentry_testsupport.h" #include "sentry_transport.h" @@ -283,7 +284,10 @@ SENTRY_TEST(retry_session) sentry__path_remove_all(options->run->cache_path); sentry__path_create_dir_all(options->run->cache_path); - sentry_session_t *session = sentry__session_new(); + sentry_session_t *session = NULL; + SENTRY_WITH_SCOPE (scope) { + session = sentry__session_new(scope); + } TEST_ASSERT(!!session); sentry_envelope_t *envelope = sentry__envelope_new(); TEST_ASSERT(!!envelope); From 36568c19231cd33446d003bc9d0adba8356e8917 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 10 Mar 2026 20:02:48 +0100 Subject: [PATCH 094/103] ref: cache envelopes only on failed HTTP send Move cache_keep logic into http_send_task so envelopes are cached only when sending actually fails, rather than unconditionally in process_old_runs. For non-HTTP transports, process_old_runs still handles caching since there is no http_send_task. - Add cache_keep/run fields to http_transport_state_t - Cache in http_send_task when send fails and retry is unavailable - Simplify can_cache in process_old_runs to check transport capability - Fix NULL transport dereference in sentry__transport_flush - Update sentry_options_set_cache_keep/http_retry docs Co-Authored-By: Claude Opus 4.6 --- include/sentry.h | 12 +++++++----- src/sentry_database.c | 3 +-- src/sentry_transport.c | 2 +- src/transports/sentry_http_transport.c | 7 +++++++ tests/unit/test_cache.c | 1 + 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 00bc3cab4..a2dc3613b 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1494,12 +1494,11 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( const sentry_options_t *opts); /** - * Enables or disables storing envelopes in a persistent cache. + * Enables or disables storing failed envelopes in a persistent cache. * - * When enabled, envelopes are written to a `cache/` subdirectory within the - * database directory and retained regardless of send success or failure. - * The cache is cleared on startup based on the cache_max_items, cache_max_size, - * and cache_max_age options. + * When enabled, envelopes that fail to send are written to a `cache/` + * subdirectory within the database directory. The cache is cleared on startup + * based on the cache_max_items, cache_max_size, and cache_max_age options. * * Disabled by default. */ @@ -2278,6 +2277,9 @@ SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( /** * Enables or disables HTTP retry with exponential backoff for network failures. + * + * Only applicable for HTTP transports. + * * Enabled by default. */ SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retry( diff --git a/src/sentry_database.c b/src/sentry_database.c index e0826ceba..23b69fd31 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -367,8 +367,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } bool can_cache = options->cache_keep - && (!options->http_retry - || !sentry__transport_can_retry(options->transport)); + && !sentry__transport_can_retry(options->transport); sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; diff --git a/src/sentry_transport.c b/src/sentry_transport.c index 1b81cb652..f8fb34f1f 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -93,7 +93,7 @@ sentry__transport_startup( int sentry__transport_flush(sentry_transport_t *transport, uint64_t timeout) { - if (transport->flush_func && transport->running) { + if (transport && transport->flush_func && transport->running) { SENTRY_DEBUG("flushing transport"); return transport->flush_func(timeout, transport->state); } diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 237052689..1df7b7c00 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -32,6 +32,8 @@ typedef struct { sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); sentry_retry_t *retry; + bool cache_keep; + sentry_run_t *run; } http_transport_state_t; #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -244,6 +246,7 @@ http_transport_state_free(void *_state) sentry_free(state->user_agent); sentry__rate_limiter_free(state->ratelimiter); sentry__retry_free(state->retry); + sentry__run_free(state->run); sentry_free(state); } @@ -256,6 +259,8 @@ http_send_task(void *_envelope, void *_state) int status_code = http_send_envelope(state, envelope); if (status_code < 0 && state->retry) { sentry__retry_enqueue(state->retry, envelope); + } else if (status_code < 0 && state->cache_keep) { + sentry__run_write_cache(state->run, envelope, -1); } } @@ -278,6 +283,8 @@ http_transport_start(const sentry_options_t *options, void *transport_state) state->dsn = sentry__dsn_incref(options->dsn); state->user_agent = sentry__string_clone(options->user_agent); + state->cache_keep = options->cache_keep; + state->run = sentry__run_incref(options->run); if (state->start_client) { int rv = state->start_client(state->client, options); diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index f49637bc4..7c31a290a 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -81,6 +81,7 @@ SENTRY_TEST(cache_keep) TEST_ASSERT(!sentry__path_is_file(cached_envelope_path)); sentry__process_old_runs(options, 0); + sentry_flush(5000); TEST_ASSERT(!sentry__path_is_file(old_envelope_path)); TEST_ASSERT(sentry__path_is_file(cached_envelope_path)); From 1d85dd8272bc538dc41784503a15d60bb37350fe Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 10 Mar 2026 21:06:34 +0100 Subject: [PATCH 095/103] Clarify bgworker shutdown timeout comment Co-Authored-By: Claude Opus 4.6 --- src/sentry_sync.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 8e9067b99..bf6c9a9d8 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -443,8 +443,9 @@ sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, uint64_t now = sentry__monotonic_time(); if (now > started && now - started > timeout) { if (on_timeout) { - // Unblock the worker (e.g. close transport handles) and - // let it finish in-flight work like handle_result. + // fire on_timeout to cancel the ongoing task, and give the + // worker an extra loop cycle up to 250ms to handle the + // cancellation sentry__mutex_unlock(&bgw->task_lock); on_timeout(on_timeout_data); on_timeout = NULL; From 168a807c3192572feddd02331752daa886fe9c3c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 14:50:15 +0100 Subject: [PATCH 096/103] Cache envelopes only on failed HTTP send Move envelope caching from process_old_runs (synchronous, before send) into http_send_task (on HTTP failure). This ensures envelopes are only cached when the send actually fails, rather than unconditionally. Add transport cleanup_func to submit cache cleanup as a FIFO task on the bg worker, guaranteeing it runs after pending send tasks from process_old_runs. Add explicit flush argument to sentry_example for deterministic test behavior across platforms with varying HTTP timeout characteristics. Co-Authored-By: Claude Opus 4.6 --- examples/example.c | 4 ++ include/sentry.h | 2 + src/sentry_core.c | 4 +- src/sentry_database.c | 8 --- src/sentry_transport.c | 19 ++++++ src/sentry_transport.h | 18 +++++ src/transports/sentry_http_transport.c | 19 ++++++ tests/test_integration_cache.py | 94 ++++++++++++++++---------- tests/test_unit.py | 4 +- tests/unit/test_cache.c | 1 + 10 files changed, 128 insertions(+), 45 deletions(-) diff --git a/examples/example.c b/examples/example.c index 652bdc29f..5111e47e6 100644 --- a/examples/example.c +++ b/examples/example.c @@ -943,6 +943,10 @@ main(int argc, char **argv) sentry_reinstall_backend(); } + if (has_arg(argc, argv, "flush")) { + sentry_flush(10000); + } + if (has_arg(argc, argv, "sleep")) { sleep_s(10); } diff --git a/include/sentry.h b/include/sentry.h index a2dc3613b..f20c8848d 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1500,6 +1500,8 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( * subdirectory within the database directory. The cache is cleared on startup * based on the cache_max_items, cache_max_size, and cache_max_age options. * + * Only applicable for HTTP transports. + * * Disabled by default. */ SENTRY_API void sentry_options_set_cache_keep( diff --git a/src/sentry_core.c b/src/sentry_core.c index 14dba4a2d..4880bd1a1 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -293,7 +293,9 @@ sentry_init(sentry_options_t *options) } if (options->cache_keep || options->http_retry) { - sentry__cleanup_cache(options); + if (!sentry__transport_submit_cleanup(options->transport, options)) { + sentry__cleanup_cache(options); + } } if (options->auto_session_tracking) { diff --git a/src/sentry_database.c b/src/sentry_database.c index 23b69fd31..05abb173b 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -366,9 +366,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) continue; } - bool can_cache = options->cache_keep - && !sentry__transport_can_retry(options->transport); - sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) { @@ -413,11 +410,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } else if (sentry__path_ends_with(file, ".envelope")) { sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - - if (can_cache - && sentry__run_move_cache(options->run, file, -1)) { - continue; - } } sentry__path_remove(file); diff --git a/src/sentry_transport.c b/src/sentry_transport.c index f8fb34f1f..94ac6d906 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -11,6 +11,7 @@ struct sentry_transport_s { void (*free_func)(void *state); size_t (*dump_func)(sentry_run_t *run, void *state); void (*retry_func)(void *state); + void (*cleanup_func)(const sentry_options_t *options, void *state); void *state; bool running; }; @@ -169,3 +170,21 @@ sentry__transport_can_retry(sentry_transport_t *transport) { return transport && transport->retry_func; } + +void +sentry__transport_set_cleanup_func(sentry_transport_t *transport, + void (*cleanup_func)(const sentry_options_t *options, void *state)) +{ + transport->cleanup_func = cleanup_func; +} + +bool +sentry__transport_submit_cleanup( + sentry_transport_t *transport, const sentry_options_t *options) +{ + if (transport && transport->cleanup_func && transport->running) { + transport->cleanup_func(options, transport->state); + return true; + } + return false; +} diff --git a/src/sentry_transport.h b/src/sentry_transport.h index 5ed1e7b81..17427600d 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -61,4 +61,22 @@ void sentry__transport_set_retry_func( sentry_transport_t *transport, void (*retry_func)(void *state)); bool sentry__transport_can_retry(sentry_transport_t *transport); +/** + * Sets the cleanup function of the transport. + * + * This function submits cache cleanup as a task on the transport's background + * worker, so it runs after any pending send tasks from process_old_runs. + */ +void sentry__transport_set_cleanup_func(sentry_transport_t *transport, + void (*cleanup_func)(const sentry_options_t *options, void *state)); + +/** + * Submits cache cleanup to the transport's background worker. + * + * Returns true if cleanup was submitted, false if the transport does not + * support async cleanup (caller should run cleanup synchronously). + */ +bool sentry__transport_submit_cleanup( + sentry_transport_t *transport, const sentry_options_t *options); + #endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 1df7b7c00..257b4feff 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -376,6 +376,23 @@ http_transport_retry(void *transport_state) } } +static void +http_cleanup_cache_task(void *task_data, void *_state) +{ + (void)_state; + const sentry_options_t *options = task_data; + sentry__cleanup_cache(options); +} + +static void +http_transport_submit_cleanup( + const sentry_options_t *options, void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + sentry__bgworker_submit( + bgworker, http_cleanup_cache_task, NULL, (void *)options); +} + sentry_transport_t * sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) { @@ -410,6 +427,8 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) sentry_transport_set_shutdown_func(transport, http_transport_shutdown); sentry__transport_set_dump_func(transport, http_dump_queue); sentry__transport_set_retry_func(transport, http_transport_retry); + sentry__transport_set_cleanup_func( + transport, http_transport_submit_cleanup); return transport; } diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index 6ba9fd225..f704e302f 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -3,9 +3,14 @@ import pytest from . import run -from .conditions import has_breakpad, has_files +from .conditions import has_breakpad, has_files, has_http -pytestmark = pytest.mark.skipif(not has_files, reason="tests need local filesystem") +pytestmark = [ + pytest.mark.skipif(not has_files, reason="tests need local filesystem"), + pytest.mark.skipif(not has_http, reason="tests need http transport"), +] + +unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" @pytest.mark.parametrize("cache_keep", [True, False]) @@ -22,10 +27,9 @@ ], ) def test_cache_keep(cmake, backend, cache_keep): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) # capture run( @@ -33,6 +37,7 @@ def test_cache_keep(cmake, backend, cache_keep): "sentry_example", ["log", "crash"] + (["cache-keep"] if cache_keep else []), expect_failure=True, + env=env, ) assert not cache_dir.exists() or len(list(cache_dir.glob("*.envelope"))) == 0 @@ -42,6 +47,7 @@ def test_cache_keep(cmake, backend, cache_keep): tmp_path, "sentry_example", ["log", "no-setup"] + (["cache-keep"] if cache_keep else []), + env=env, ) assert cache_dir.exists() or cache_keep is False @@ -63,34 +69,42 @@ def test_cache_keep(cmake, backend, cache_keep): ], ) def test_cache_max_size(cmake, backend): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) - # 5 x 2mb for i in range(5): run( tmp_path, "sentry_example", ["log", "cache-keep", "crash"], expect_failure=True, + env=env, ) - if cache_dir.exists(): - cache_files = list(cache_dir.glob("*.envelope")) - for f in cache_files: - with open(f, "r+b") as file: - file.truncate(2 * 1024 * 1024) - + # flush + cache run( tmp_path, "sentry_example", - ["log", "cache-keep", "no-setup"], + ["log", "no-http-retry", "cache-keep", "flush", "no-setup"], + env=env, ) - # max 4mb + # 5 x 2mb assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + for f in cache_files: + with open(f, "r+b") as file: + file.truncate(2 * 1024 * 1024) + + # max 4mb + run( + tmp_path, + "sentry_example", + ["log", "no-http-retry", "cache-keep", "no-setup"], + env=env, + ) + cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) <= 2 assert sum(f.stat().st_size for f in cache_files) <= 4 * 1024 * 1024 @@ -109,19 +123,27 @@ def test_cache_max_size(cmake, backend): ], ) def test_cache_max_age(cmake, backend): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) for i in range(5): run( tmp_path, "sentry_example", - ["log", "cache-keep", "crash"], + ["log", "no-http-retry", "cache-keep", "crash"], expect_failure=True, + env=env, ) + # flush + cache + run( + tmp_path, + "sentry_example", + ["log", "no-http-retry", "cache-keep", "flush", "no-setup"], + env=env, + ) + # 2,4,6,8,10 days old assert cache_dir.exists() cache_files = list(cache_dir.glob("*.envelope")) @@ -129,16 +151,16 @@ def test_cache_max_age(cmake, backend): mtime = time.time() - ((i + 1) * 2 * 24 * 60 * 60) os.utime(str(f), (mtime, mtime)) - # 0 days old + # cleanup (max 5 days) run( tmp_path, "sentry_example", - ["log", "cache-keep", "no-setup"], + ["log", "no-http-retry", "cache-keep", "no-setup"], + env=env, ) - # max 5 days cache_files = list(cache_dir.glob("*.envelope")) - assert len(cache_files) == 3 + assert len(cache_files) == 2 for f in cache_files: assert time.time() - f.stat().st_mtime <= 5 * 24 * 60 * 60 @@ -156,10 +178,9 @@ def test_cache_max_age(cmake, backend): ], ) def test_cache_max_items(cmake, backend): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) for i in range(6): run( @@ -167,12 +188,15 @@ def test_cache_max_items(cmake, backend): "sentry_example", ["log", "cache-keep", "crash"], expect_failure=True, + env=env, ) + # flush + cache run( tmp_path, "sentry_example", - ["log", "cache-keep", "no-setup"], + ["log", "cache-keep", "flush", "no-setup"], + env=env, ) # max 5 items @@ -194,10 +218,9 @@ def test_cache_max_items(cmake, backend): ], ) def test_cache_max_items_with_retry(cmake, backend): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) # Create cache files via crash+restart cycles for i in range(4): @@ -206,13 +229,15 @@ def test_cache_max_items_with_retry(cmake, backend): "sentry_example", ["log", "cache-keep", "crash"], expect_failure=True, + env=env, ) - # Move envelopes into cache + # flush + cache run( tmp_path, "sentry_example", - ["log", "cache-keep", "no-setup"], + ["log", "cache-keep", "flush", "no-setup"], + env=env, ) # Pre-populate cache/ with retry-format envelope files @@ -227,6 +252,7 @@ def test_cache_max_items_with_retry(cmake, backend): tmp_path, "sentry_example", ["log", "cache-keep", "no-setup"], + env=env, ) # max 5 items total in cache/ diff --git a/tests/test_unit.py b/tests/test_unit.py index c38fffb91..daa571257 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -5,7 +5,7 @@ def test_unit(cmake, unittest): - if unittest in ["basic_transport_thread_name"]: + if unittest in ["basic_transport_thread_name", "cache_keep"]: pytest.skip("excluded from unit test-suite") cwd = cmake( ["sentry_test_unit"], @@ -33,7 +33,7 @@ def test_unit_transport(cmake, unittest): def test_unit_with_test_path(cmake, unittest): - if unittest in ["basic_transport_thread_name"]: + if unittest in ["basic_transport_thread_name", "cache_keep"]: pytest.skip("excluded from unit test-suite") cwd = cmake( ["sentry_test_unit"], diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 7c31a290a..9cb4e045f 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -44,6 +44,7 @@ SENTRY_TEST(cache_keep) SKIP_TEST(); #endif SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(!!options->transport); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); sentry_options_set_http_retry(options, false); From c0f920d4026e5e476bc38d3495a6623485e364f7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 15:27:52 +0100 Subject: [PATCH 097/103] Remove unused sentry__transport_can_retry Its only production use was the can_cache check in process_old_runs, which was removed in the previous commit. Co-Authored-By: Claude Opus 4.6 --- src/sentry_transport.c | 6 ------ src/sentry_transport.h | 1 - tests/unit/test_retry.c | 5 ++--- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/sentry_transport.c b/src/sentry_transport.c index 94ac6d906..cc9542293 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -165,12 +165,6 @@ sentry__transport_set_retry_func( transport->retry_func = retry_func; } -bool -sentry__transport_can_retry(sentry_transport_t *transport) -{ - return transport && transport->retry_func; -} - void sentry__transport_set_cleanup_func(sentry_transport_t *transport, void (*cleanup_func)(const sentry_options_t *options, void *state)) diff --git a/src/sentry_transport.h b/src/sentry_transport.h index 17427600d..a4ce8197e 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -59,7 +59,6 @@ void *sentry__transport_get_state(sentry_transport_t *transport); void sentry__transport_set_retry_func( sentry_transport_t *transport, void (*retry_func)(void *state)); -bool sentry__transport_can_retry(sentry_transport_t *transport); /** * Sets the cleanup function of the transport. diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 8d099a875..118795d78 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -376,13 +376,12 @@ SENTRY_TEST(transport_retry) { // no retry_func → no-op sentry_transport_t *transport = sentry_transport_new(noop_send); - TEST_CHECK(!sentry__transport_can_retry(transport)); + retry_func_calls = 0; sentry_transport_retry(transport); + TEST_CHECK_INT_EQUAL(retry_func_calls, 0); // with retry_func → calls it - retry_func_calls = 0; sentry__transport_set_retry_func(transport, mock_retry_func); - TEST_CHECK(sentry__transport_can_retry(transport)); sentry_transport_retry(transport); TEST_CHECK_INT_EQUAL(retry_func_calls, 1); From bb06d6f2cc37a45856447a72b03aa4f30048c261 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 16:27:45 +0100 Subject: [PATCH 098/103] fix: incref options in cleanup task to prevent use-after-free When bgworker_shutdown_cb times out, the thread is detached and sentry_close frees options while the cleanup task may still be queued. Incref options when submitting and use sentry_options_free as the task cleanup function to ensure safe lifetime. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 257b4feff..aa18eec18 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -380,7 +380,7 @@ static void http_cleanup_cache_task(void *task_data, void *_state) { (void)_state; - const sentry_options_t *options = task_data; + sentry_options_t *options = task_data; sentry__cleanup_cache(options); } @@ -389,8 +389,9 @@ http_transport_submit_cleanup( const sentry_options_t *options, void *transport_state) { sentry_bgworker_t *bgworker = transport_state; - sentry__bgworker_submit( - bgworker, http_cleanup_cache_task, NULL, (void *)options); + sentry__bgworker_submit(bgworker, http_cleanup_cache_task, + (void (*)(void *))sentry_options_free, + sentry__options_incref((sentry_options_t *)options)); } sentry_transport_t * From 884044451e2eafc73374054d2c9a54a422caf23a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Mar 2026 15:23:37 +0100 Subject: [PATCH 099/103] Update sentry docs URL for network failure handling --- src/sentry_retry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6e2509a34..d97063ac6 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -96,7 +96,7 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { // Only network failures (status_code < 0) trigger retries. HTTP responses // including 5xx (500, 502, 503, 504) are discarded: - // https://develop.sentry.dev/sdk/expected-features/#dealing-with-network-failures + // https://develop.sentry.dev/sdk/foundations/transport/offline-caching/#dealing-with-network-failures // network failure with retries remaining: bump count & re-enqueue if (item->count + 1 < SENTRY_RETRY_ATTEMPTS && status_code < 0) { From 93d884a04aa201d4d084dd2a38ea4791ce157387 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Mar 2026 16:36:52 +0100 Subject: [PATCH 100/103] Replace pointer comparison with envelope tag in retry seal check Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_envelope.c | 21 +++++++++++++++++++++ src/sentry_envelope.h | 5 +++++ src/sentry_retry.c | 8 ++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 27a836765..3656b0252 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -7,6 +7,7 @@ #include "sentry_ratelimiter.h" #include "sentry_scope.h" #include "sentry_string.h" +#include "sentry_sync.h" #include "sentry_transport.h" #include "sentry_value.h" #include @@ -21,7 +22,20 @@ struct sentry_envelope_item_s { sentry_envelope_item_t *next; }; +static long +next_tag(void) +{ + static volatile long counter = 0; + long tag = sentry__atomic_fetch_and_add(&counter, 1); + if (tag == 0) { + // skip 0-sentinel on overflow + tag = sentry__atomic_fetch_and_add(&counter, 1); + } + return tag; +} + struct sentry_envelope_s { + long tag; bool is_raw; union { struct { @@ -199,6 +213,7 @@ sentry__envelope_new_with_dsn(const sentry_dsn_t *dsn) return NULL; } + rv->tag = next_tag(); rv->is_raw = false; rv->contents.items.first_item = NULL; rv->contents.items.last_item = NULL; @@ -236,6 +251,12 @@ sentry__envelope_from_path(const sentry_path_t *path) return envelope; } +long +sentry__envelope_get_tag(const sentry_envelope_t *envelope) +{ + return envelope->tag; +} + sentry_uuid_t sentry__envelope_get_event_id(const sentry_envelope_t *envelope) { diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index f0e9bbc73..b047bfc85 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -31,6 +31,11 @@ sentry_envelope_t *sentry__envelope_new_with_dsn(const sentry_dsn_t *dsn); */ sentry_envelope_t *sentry__envelope_from_path(const sentry_path_t *path); +/** + * Returns a unique non-zero tag assigned at envelope creation. + */ +long sentry__envelope_get_tag(const sentry_envelope_t *envelope); + /** * This returns the UUID of the event associated with this envelope. * If there is no event inside this envelope, the empty nil UUID will be diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d97063ac6..c9fe931de 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -35,7 +35,7 @@ struct sentry_retry_s { sentry_retry_send_func_t send_cb; void *send_data; sentry_mutex_t sealed_lock; - uintptr_t sealed_envelope; + long sealed_tag; }; sentry_retry_t * @@ -301,10 +301,10 @@ retry_dump_cb(void *_envelope, void *_retry) { sentry_retry_t *retry = (sentry_retry_t *)_retry; sentry_envelope_t *envelope = (sentry_envelope_t *)_envelope; - if ((uintptr_t)envelope != retry->sealed_envelope) { + if (sentry__envelope_get_tag(envelope) != retry->sealed_tag) { sentry__run_write_cache(retry->run, envelope, 0); } else { - retry->sealed_envelope = 0; + retry->sealed_tag = 0; } return true; } @@ -350,7 +350,7 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) sentry__mutex_unlock(&retry->sealed_lock); return; } - retry->sealed_envelope = (uintptr_t)envelope; + retry->sealed_tag = sentry__envelope_get_tag(envelope); sentry__mutex_unlock(&retry->sealed_lock); sentry__atomic_compare_swap( From 93db90035832209e4d881588db3ad307e8cd62b6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Mar 2026 16:41:11 +0100 Subject: [PATCH 101/103] Add TODO for retry jitter and shorter poll interval --- src/sentry_retry.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index c9fe931de..16321c676 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -67,6 +67,8 @@ sentry__retry_free(sentry_retry_t *retry) uint64_t sentry__retry_backoff(int count) { + // TODO: consider adding jitter and shortening the poll interval to spread + // out retries when multiple envelopes (esp. large attachments) pile up. return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(MAX(count, 0), 5); } From 8f104d2ea96d518f2bef8b740225cbb0f627a04f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Mar 2026 16:08:58 +0000 Subject: [PATCH 102/103] Initialize tag for raw envelopes loaded from disk --- src/sentry_envelope.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 3656b0252..e69c05eba 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -244,6 +244,7 @@ sentry__envelope_from_path(const sentry_path_t *path) return NULL; } + envelope->tag = next_tag(); envelope->is_raw = true; envelope->contents.raw.payload = buf; envelope->contents.raw.payload_len = buf_len; From 878ace52f70360e15966ad663346e9326849157f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 18 Mar 2026 21:16:22 +0100 Subject: [PATCH 103/103] Implement on_timeout for curl transport Use a progress callback (CURLOPT_XFERINFOFUNCTION) that checks an atomic shutdown flag. When http_transport_shutdown_timeout fires, it sets the flag via curl_client_shutdown, causing curl_easy_perform to abort promptly. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/transports/sentry_http_transport_curl.c | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index eec48f8e4..1954c8e5b 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -4,6 +4,7 @@ #include "sentry_http_transport.h" #include "sentry_options.h" #include "sentry_string.h" +#include "sentry_sync.h" #include "sentry_transport.h" #include "sentry_utils.h" @@ -20,6 +21,7 @@ typedef struct { char *proxy; char *ca_certs; bool debug; + long shutdown; #ifdef SENTRY_PLATFORM_NX void *nx_state; #endif @@ -117,6 +119,22 @@ curl_client_start(void *_client, const sentry_options_t *options) return 0; } +static void +curl_client_shutdown(void *_client) +{ + curl_client_t *client = _client; + sentry__atomic_store(&client->shutdown, 1); +} + +static int +progress_callback(void *clientp, curl_off_t UNUSED(dltotal), + curl_off_t UNUSED(dlnow), curl_off_t UNUSED(ultotal), + curl_off_t UNUSED(ulnow)) +{ + curl_client_t *client = clientp; + return sentry__atomic_fetch(&client->shutdown) ? 1 : 0; +} + static size_t swallow_data( char *UNUSED(ptr), size_t size, size_t nmemb, void *UNUSED(userdata)) @@ -190,6 +208,9 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 15000L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, client); char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; @@ -254,5 +275,6 @@ sentry__transport_new_default(void) } sentry__http_transport_set_free_client(transport, curl_client_free); sentry__http_transport_set_start_client(transport, curl_client_start); + sentry__http_transport_set_shutdown_client(transport, curl_client_shutdown); return transport; }