From 8d0033b5e3e05204e22d86bde09e6aa76811dcb2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 11:05:27 +0100 Subject: [PATCH 01/91] 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 50d30e3f2..c9917ac3a 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2146,6 +2146,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 bdcd644d6..7e710a1ef 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -836,6 +836,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 ca186389b..184b6eb64 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 0f3717729ba83e357ceb3092f8b0cc6dc34dd0d2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:38:49 +0100 Subject: [PATCH 02/91] 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 70ce402c7..83cffe001 100644 --- a/examples/example.c +++ b/examples/example.c @@ -633,6 +633,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 d488ae4cd..87098e2f3 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 a78d2e322..15e2a295d 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -725,3 +725,296 @@ def test_discarding_before_breadcrumb_http(cmake, httpserver): assert_event(envelope) assert_no_breadcrumbs(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 359ec43f9..a3b88efa5 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -42,6 +42,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 1df1c220b..1aabe98e9 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -166,6 +166,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 f321bc63219fce824b5b511bf39304894058753e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:51:01 +0100 Subject: [PATCH 03/91] 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 f5e8c5f8a1a6d8541f21bd7d9c1bc45b9a3ec63e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:54:09 +0100 Subject: [PATCH 04/91] 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 9fb83b8126220915bb8acdc65fd77c3697b45ef4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:03:50 +0100 Subject: [PATCH 05/91] 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 315b79ef70666345c041f313aa073bef59036610 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:14:51 +0100 Subject: [PATCH 06/91] 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 7c7d06c29e1d3638c4ce98235a48fd6327988977 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:21:26 +0100 Subject: [PATCH 07/91] 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 22c26da6c93d7c5f84e9bcb2cbd7f09061f2e966 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:38:57 +0100 Subject: [PATCH 08/91] 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 5d706c6ad19563d376d58062b1bb3d1ae6166e43 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 14:19:41 +0100 Subject: [PATCH 09/91] 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 d29c20ccaff43d0ea3223bbac9534f25b38ee3e9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 14:49:31 +0100 Subject: [PATCH 10/91] 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 aeef825e08f545b9760739c37732d511b489d8f4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 15:08:40 +0100 Subject: [PATCH 11/91] 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 5c20a62271120dcbd5ae5a49d8abb9a22734230f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 15:18:00 +0100 Subject: [PATCH 12/91] 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 a411b296fce1b79d88ff0c3c4234d87a64899fdf Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:38:31 +0100 Subject: [PATCH 13/91] 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 38af3470973dab430395e034e79a11993281ada5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:41:57 +0100 Subject: [PATCH 14/91] 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 58e6f89a772f61efcda5807486711c23983ef788 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:48:12 +0100 Subject: [PATCH 15/91] 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 7dc961eafbd6943a80ab3764abba768cb8acc05f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:55:04 +0100 Subject: [PATCH 16/91] 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 633a347933fe6fff51eaa04713e6bd5f2fef1bbc Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:59:33 +0100 Subject: [PATCH 17/91] 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 1aabe98e9..fb20fb0ed 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -168,7 +168,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 32dfe4cad8bf22fa45b168b11bd0f6c3e19692c5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:02:22 +0100 Subject: [PATCH 18/91] 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 dc7c627014151c76794bc42b8a1e6ef303ec2b0f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:20:24 +0100 Subject: [PATCH 19/91] 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 d94191a7972ded8802f0d1af3f81712677b13c53 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:47:36 +0100 Subject: [PATCH 20/91] 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 325cef5bded8a54875eba76d6a6b75a3f24e49cd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 20:03:19 +0100 Subject: [PATCH 21/91] 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 15e2a295d..bfd033af5 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -840,7 +840,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 7ad242d2be1951956b7a8073df1c08f1abdf6cf3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 21:19:40 +0100 Subject: [PATCH 22/91] 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 2e5a599330764f03da13b68aacb68abd25bd09eb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 21:36:59 +0100 Subject: [PATCH 23/91] 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 3b7af08bf4ef63c1c14d78d219add8a562ea12c9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:08:46 +0100 Subject: [PATCH 24/91] 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 cc729dcefe55779d14f6d924743dbcd5de137907 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:10:39 +0100 Subject: [PATCH 25/91] 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 91aa134e58754689babfba868a65087b086bf810 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:11:30 +0100 Subject: [PATCH 26/91] 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 0036998902db99f01b61480983b6614a598078c1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 13 Feb 2026 17:14:04 +0100 Subject: [PATCH 27/91] 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 a20116219cc4dc08bdd895ec259fe5548bf6071b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 10:40:22 +0100 Subject: [PATCH 28/91] 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 10dc2da09dbb7acbfcdf698e61da6dfe6093eeb7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 15:37:21 +0100 Subject: [PATCH 29/91] 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 f464c0c04273246c4ce8eb73132b3a20fa781335 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:16:03 +0100 Subject: [PATCH 30/91] 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 dd11aa271b09e61eb097ebc7a8431cfe8e6b52a2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:16:08 +0100 Subject: [PATCH 31/91] 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 5c05a53c75e9bec8151b5099b5e035f83a50fea1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:28:13 +0100 Subject: [PATCH 32/91] 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 7bff90a78e3d78197ee356e5979af1f0532e118a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:43:59 +0100 Subject: [PATCH 33/91] 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 bfd033af5..ebd8b88f0 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -977,11 +977,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 4db204aecab66ce3209cf2c2ebe828111100b823 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:52:03 +0100 Subject: [PATCH 34/91] 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 2d31e52ce386fef54a047f308eedf3dad0a2da35 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:58:35 +0100 Subject: [PATCH 35/91] 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 329acce0de7ce5cb4bbb4d7ae64754ed68de07ca Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 17:45:36 +0100 Subject: [PATCH 36/91] 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, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88f1fd349..4dc321c66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Features**: + +- Add HTTP retry with exponential backoff: `sentry_options_set_http_retries()`. ([#1520](https://github.com/getsentry/sentry-native/pull/1520)) + ## 0.13.0 **Breaking**: diff --git a/include/sentry.h b/include/sentry.h index c9917ac3a..71285c75a 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2147,7 +2147,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 e097a7d3cdf5f3fb89f6cfd3bb034ed2682e6ae3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 19:04:46 +0100 Subject: [PATCH 37/91] 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 6dbb1844b9d63a471e8518f88b3b0181ded823bf Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 19:28:49 +0100 Subject: [PATCH 38/91] 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 15ae77f8fcabe281a63f205d9fe687a668712559 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 09:55:45 +0100 Subject: [PATCH 39/91] 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 3931a7cedb4694f574a4b14e28ad43ade8b0d72c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 10:04:33 +0100 Subject: [PATCH 40/91] 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 669d884579d08772802c6bb546b4443183ff4ee3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 11:08:31 +0100 Subject: [PATCH 41/91] 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 791c30d9f..7246e3461 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 50ac89d7c31178b3d5b976a31159478d7a0f3d37 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:00:36 +0100 Subject: [PATCH 42/91] 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 b3f20a8dcbc5bcc2531ff1ea0c8c0b99204ac626 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:03:26 +0100 Subject: [PATCH 43/91] 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 31e48a62a24c600e741264a69494c58bc016b8f7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:25:39 +0100 Subject: [PATCH 44/91] 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 e1a7ad8610c231ab176cc926ddc248fd9c5fd0a1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:53:01 +0100 Subject: [PATCH 45/91] 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 6027d423c51530a96b333464426d1c3c44d3f199 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 13:54:06 +0100 Subject: [PATCH 46/91] 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 71285c75a..0feef73aa 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 fb20fb0ed..ae806a234 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -219,6 +219,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 7672ec1bb235ce19d64513426669e44001440093 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 17:37:37 +0100 Subject: [PATCH 47/91] 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 1c70c9ecc..01f3727db 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -290,7 +290,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 184b6eb64..c95de111f 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 ebd8b88f0..d9d414894 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -730,7 +730,7 @@ def test_discarding_before_breadcrumb_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" @@ -743,10 +743,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)) @@ -765,53 +765,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" @@ -824,8 +819,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") @@ -839,15 +834,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" @@ -860,8 +852,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( @@ -871,7 +863,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 @@ -879,7 +870,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( @@ -891,14 +882,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( @@ -910,14 +901,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) @@ -929,8 +920,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): @@ -948,14 +939,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) @@ -967,8 +958,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, @@ -978,16 +969,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) @@ -999,8 +990,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) @@ -1017,5 +1008,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 ae806a234..6b580902b 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 5f2b64dc7f08bba2878600b0dec672669e32329d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 09:51:52 +0100 Subject: [PATCH 48/91] 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 5209671860801663ce9eb7241231005deac7fd09 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 09:33:34 +0100 Subject: [PATCH 49/91] 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 51e7ff5d99fda868164a2e38e187ef7c97f0f035 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 10:24:17 +0100 Subject: [PATCH 50/91] 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 44d9e9d8fbafc16743b5ae3c686e7f9abf4343e5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 13:33:53 +0100 Subject: [PATCH 51/91] 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 4dc321c66..c6347e359 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)) ## 0.13.0 diff --git a/examples/example.c b/examples/example.c index 83cffe001..72d80cef0 100644 --- a/examples/example.c +++ b/examples/example.c @@ -633,8 +633,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 0feef73aa..08ebea494 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); @@ -2155,12 +2154,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 7e710a1ef..a041eb0ff 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -75,6 +75,7 @@ sentry_options_new(void) opts->traces_sample_rate = 0.0; opts->max_spans = SENTRY_SPANS_MAX; opts->handler_strategy = SENTRY_HANDLER_STRATEGY_DEFAULT; + opts->http_retry = true; return opts; } @@ -837,15 +838,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 c95de111f..9cda3d00a 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 d9d414894..2b1afd396 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -739,7 +739,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, ) @@ -756,7 +756,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 @@ -777,19 +777,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 @@ -797,7 +797,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")) @@ -815,7 +815,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, ) @@ -829,7 +829,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 @@ -848,7 +848,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, ) @@ -859,7 +859,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, ) @@ -916,7 +916,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, ) @@ -933,7 +933,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 @@ -954,7 +954,7 @@ def test_http_retry_multiple_network_error(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-multiple"], + ["log", "capture-multiple"], env=env, ) @@ -964,7 +964,7 @@ def test_http_retry_multiple_network_error(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env, ) @@ -986,7 +986,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, ) @@ -1003,7 +1003,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 7a949f65b11fd71fbd9c5e077f3532314a69621c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 14:18:41 +0100 Subject: [PATCH 52/91] 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 fdccfd584f887adb2c64f2b14e1b906a48d07f0a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 14:32:46 +0100 Subject: [PATCH 53/91] 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 feb9b64d22682783b1284307e7dd960d4df2737e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:13:38 +0100 Subject: [PATCH 54/91] 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 278231e0ddb3c15818ace48c774e346ac2bc0eaf Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:27:38 +0100 Subject: [PATCH 55/91] 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 b5b74853c4ee8e8764a1148fb84e9ef6368aacf3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:58:10 +0100 Subject: [PATCH 56/91] 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 a9715ffd565392e7923156c68ae2b5265ec83178 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 17:03:59 +0100 Subject: [PATCH 57/91] 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 2c9489c39ac76b7d39935c3fc550011d3a050471 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 17:26:26 +0100 Subject: [PATCH 58/91] 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 7246e3461..f8d60322d 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 3dbd17aa67d6a6e1dbfa543867e794b97a25c38a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 10:11:08 +0100 Subject: [PATCH 59/91] 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 6b580902b..8dbaa952f 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -172,6 +172,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 0eb79d03a8ed6a167439eba9702c7875f5b0f3eb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 10:14:49 +0100 Subject: [PATCH 60/91] 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 01f3727db..135acd1e9 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -290,8 +290,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 4260ca3cad2ac354cdab9e6b9702bc4a24c1fa16 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 11:04:45 +0100 Subject: [PATCH 61/91] 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 8dbaa952f..32c3086b3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -169,6 +169,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 60c5e77450c623e57b445e2a4d2dc6c2dd359c25 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 11:48:55 +0100 Subject: [PATCH 62/91] 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 a041eb0ff..258e0d9eb 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -840,7 +840,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 750fd59867f8192358cb16e3171ed1595268be9b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 23 Feb 2026 15:34:15 +0100 Subject: [PATCH 63/91] 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 a710a78cce5975d6e497f462ddc669d6c7d6516f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 14:36:39 +0100 Subject: [PATCH 64/91] 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 08ebea494..6d5e60bdf 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 282bd4683ba925dd492cd0a2153a37954aa14ebb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 14:42:23 +0100 Subject: [PATCH 65/91] 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 f8a4f07165d21105de71961049c87e7c18c53f17 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 15:21:00 +0100 Subject: [PATCH 66/91] 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 b6260c0e77e92f05e095d3361e176a0bf1ab016b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 15:36:58 +0100 Subject: [PATCH 67/91] 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 a55b944b7c903ad5bbbe0e9da6e6dfd853d7aa9c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 17:08:37 +0100 Subject: [PATCH 68/91] 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 32c3086b3..685438f83 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -172,6 +172,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 81d0f68a8cbbc85924e7d3c7de9c7939d0da0f70 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 18:12:46 +0100 Subject: [PATCH 69/91] 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 2b1afd396..267b962a9 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -795,8 +795,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 e28b88ec54dcf76961b76491bc07270ba517fbca Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 18:28:46 +0100 Subject: [PATCH 70/91] 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 0b78591826e9d89cd28c6f961272efc9946b56fc Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:21:49 +0100 Subject: [PATCH 71/91] 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 4fa84104e9ab8738ee0ac282962a40646a3ca262 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:24:15 +0100 Subject: [PATCH 72/91] 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 cd376db2e11510f918fadf5f1701d3b669a06bec Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:45:02 +0100 Subject: [PATCH 73/91] 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 889ba353d6ffc4cf08087df1e8a755e3d9431f30 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 20:31:48 +0100 Subject: [PATCH 74/91] 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 ce89de01bee57f38f766c523e20842cff0255302 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 09:00:13 +0100 Subject: [PATCH 75/91] 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 267b962a9..c85da1702 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -37,6 +37,8 @@ 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}" @@ -733,7 +735,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( @@ -747,6 +748,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)) @@ -763,6 +765,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")) @@ -774,7 +777,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) @@ -782,18 +784,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): @@ -809,7 +816,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( @@ -842,7 +848,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( @@ -910,7 +915,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( @@ -948,7 +952,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( @@ -980,7 +983,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( @@ -1010,3 +1012,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 1881212da23fc38be5fc6ef817a4d167f61be1ed Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 09:42:01 +0100 Subject: [PATCH 76/91] 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 b35273b03303d7c871edb7f02e08125ef44ef7ae Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:51:20 +0100 Subject: [PATCH 77/91] 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 f8d60322d..33269885c 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 77cc3ddc6fa446bfbe7cb33c2193e27b7ca4612b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:53:36 +0100 Subject: [PATCH 78/91] 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 33269885c..f3d809b9d 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 4c108f4cc564eef0c78ec08b63258aa9f7ea5845 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:55:10 +0100 Subject: [PATCH 79/91] 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 f3d809b9d..a31e60298 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 e57c6fb7096fc568e4fa83f82b4a7a44fa5da48d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 18:01:07 +0100 Subject: [PATCH 80/91] 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 5e80d4b600ddbed3a017025c1623dc09c00622e2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 08:47:05 +0100 Subject: [PATCH 81/91] 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 a5bb080576688c3b81f4a1c719aed996fe67ee5b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:35:50 +0100 Subject: [PATCH 82/91] 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 a31e60298..056752d01 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.max_cache_size and * options.max_cache_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 30dbb7145e48f27b8e4789b890977658b2513f7b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:43:56 +0100 Subject: [PATCH 83/91] =?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 6d5e60bdf..618a26d5b 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 a3c584c8eeda8f1b46cb06fb821353e388fc72cd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:47:16 +0100 Subject: [PATCH 84/91] 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 b9615736fdbcd57dd28bbb48f89d139bcb5fbc91 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 14:40:00 +0100 Subject: [PATCH 85/91] 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 8bf1503b0ed753e79e252eca1fcc830e640c8b5d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 14:54:37 +0100 Subject: [PATCH 86/91] 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 bdc936aebd5fb05ab4fca5453a9fc1e221bce03b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 17:37:10 +0100 Subject: [PATCH 87/91] 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 73f0b8e92..f110a81ca 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 02e47f1e9a29b52c3defde3b5ae6fb2b363bdd46 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 17:53:56 +0100 Subject: [PATCH 88/91] 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 173040664f2fa4f166bdde5630f855cfe6a70401 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 18:01:02 +0100 Subject: [PATCH 89/91] 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 b373e708980a335aae4171c098774fabcefc11f8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 18:50:13 +0100 Subject: [PATCH 90/91] 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 fcb96f2a287fd8af17b207799b9adeb0a766b067 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 19:23:21 +0100 Subject: [PATCH 91/91] 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; }