Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
8d0033b
feat(transport): add HTTP retry logic with exponential backoff
jpnurmi Feb 12, 2026
0f37177
fix(retry): filter startup scan by timestamp, create cache dir
jpnurmi Feb 12, 2026
f321bc6
fix(retry): use wall clock time instead of monotonic time
jpnurmi Feb 12, 2026
f5e8c5f
ref(retry): define backoff base in seconds
jpnurmi Feb 12, 2026
9fb83b8
ref(retry): replace scan+free_paths with foreach callback API
jpnurmi Feb 12, 2026
315b79e
feat(transport): set 15s request timeout for curl and winhttp
jpnurmi Feb 12, 2026
7c7d06c
fix(retry): avoid duplicate delayed retry tasks on startup
jpnurmi Feb 12, 2026
22c26da
ref(retry): take options in sentry__retry_new, own path construction
jpnurmi Feb 12, 2026
5d706c6
ref(retry): set startup_time at creation, remove setter
jpnurmi Feb 12, 2026
d29c20c
fix(retry): return total file count so polling continues during backoff
jpnurmi Feb 12, 2026
aeef825
fix(retry): use callback return value to track remaining retry files
jpnurmi Feb 12, 2026
5c20a62
ref(retry): rename constants to SENTRY_RETRY_INTERVAL and SENTRY_RETR…
jpnurmi Feb 12, 2026
a411b29
ref(retry): encapsulate retry scheduling into the retry module
jpnurmi Feb 12, 2026
38af347
ref(transport): remove unnecessary includes, restore blank line
jpnurmi Feb 12, 2026
58e6f89
ref(retry): move precondition checks to callers
jpnurmi Feb 12, 2026
7dc961e
ref(transport): extract http_send_envelope helper
jpnurmi Feb 12, 2026
633a347
test(retry): remove redundant retry_no_duplicate_rescan test
jpnurmi Feb 12, 2026
32dfe4c
ref(curl): use CURLOPT_TIMEOUT_MS for consistency with winhttp and cr…
jpnurmi Feb 12, 2026
dc7c627
ref(retry): unify startup and poll into a single task
jpnurmi Feb 12, 2026
d94191a
ref(retry): extract sentry__retry_make_path helper
jpnurmi Feb 12, 2026
325cef5
fix(retry): prevent envelope duplication between retry and cache
jpnurmi Feb 12, 2026
7ad242d
ref(database): derive can_cache flag to skip cache dir creation early
jpnurmi Feb 12, 2026
2e5a599
ref(retry): change send callback to envelope-based API
jpnurmi Feb 12, 2026
3b7af08
test(retry): verify cache_keep preserves envelopes on successful send
jpnurmi Feb 12, 2026
cc729dc
fix(retry): use PRIu64 format specifier for uint64_t
jpnurmi Feb 12, 2026
91aa134
fix(retry): guard against unsigned underflow in backoff check
jpnurmi Feb 12, 2026
0036998
fix(retry): prevent startup poll from re-processing same-session enve…
jpnurmi Feb 13, 2026
a201162
fix(retry): flush pending retries on shutdown
jpnurmi Feb 14, 2026
10dc2da
ref(retry): use millisecond timestamps for retry filenames
jpnurmi Feb 14, 2026
f464c0c
fix(retry): flush pending retries synchronously before shutdown
jpnurmi Feb 14, 2026
dd11aa2
fix(retry): stop retrying on network failure
jpnurmi Feb 14, 2026
5c05a53
fix(retry): dump unsent envelopes to retry dir on shutdown timeout
jpnurmi Feb 14, 2026
7bff90a
test(retry): update expectations for stop-on-failure behavior
jpnurmi Feb 14, 2026
4db204a
style(retry): fix line length in unit tests
jpnurmi Feb 14, 2026
2d31e52
fix(retry): prevent duplicate envelope writes from detached worker
jpnurmi Feb 14, 2026
329acce
docs: add changelog entry for HTTP retry feature
jpnurmi Feb 14, 2026
e097a7d
test(retry): use sentry__retry_send instead of duplicated eligibility…
jpnurmi Feb 14, 2026
6dbb184
fix(retry): raise backoff cap from 2h to 8h to match crashpad
jpnurmi Feb 14, 2026
15ae77f
refactor(retry): introduce retry_item_t to avoid re-parsing filenames
jpnurmi Feb 15, 2026
3931a7c
feat(retry): add debug and warning output for HTTP retries
jpnurmi Feb 15, 2026
669d884
refactor(cache): add cache_path to sentry_run_t and centralize cache …
jpnurmi Feb 15, 2026
50ac89d
fix(transport): use connect-only timeouts for curl and winhttp
jpnurmi Feb 15, 2026
b3f20a8
fix(retry): decrement total count when removing corrupt envelope files
jpnurmi Feb 15, 2026
31e48a6
fix(retry): only warn about exhausted retries on network failure
jpnurmi Feb 15, 2026
e1a7ad8
docs(retry): add doc comments to sentry_retry.h declarations
jpnurmi Feb 15, 2026
6027d42
feat(transport): add sentry_transport_retry()
jpnurmi Feb 15, 2026
7672ec1
refactor(retry): store retry envelopes in cache/ directory
jpnurmi Feb 15, 2026
5f2b64d
fix(retry): own cache_path to prevent use-after-free on detached thread
jpnurmi Feb 16, 2026
5209671
fix(retry): don't consume shutdown timeout with bgworker flush
jpnurmi Feb 16, 2026
51e7ff5
fix(retry): flush in-flight retries before shutdown
jpnurmi Feb 16, 2026
44d9e9d
refactor(retry): replace http_retries count with boolean http_retry
jpnurmi Feb 16, 2026
7a949f6
fix(transport): use explicit WinHTTP send/receive timeouts
jpnurmi Feb 16, 2026
fdccfd5
fix(retry): deduplicate poll tasks on concurrent envelope failures
jpnurmi Feb 16, 2026
feb9b64
fix(retry): set sealed flag before dumping queued envelopes
jpnurmi Feb 16, 2026
278231e
fix(retry): prevent retry flush from consuming shutdown timeout
jpnurmi Feb 16, 2026
b5b7485
fix(retry): zero-initialize retry struct after malloc
jpnurmi Feb 16, 2026
a9715ff
fix(retry): skip flush task after seal to prevent duplicate sends
jpnurmi Feb 16, 2026
2c9489c
refactor(database): remove unused sentry__run_write_cache
jpnurmi Feb 16, 2026
3dbd17a
fix(retry): make trigger one-shot to prevent rapid retry exhaustion
jpnurmi Feb 17, 2026
0eb79d0
fix(core): check http_retry option instead of transport capability
jpnurmi Feb 17, 2026
4260ca3
fix(retry): prevent UB from negative count in backoff shift
jpnurmi Feb 17, 2026
60c5e77
fix(options): normalize http_retry with !! to match other boolean set…
jpnurmi Feb 17, 2026
750fd59
revert(database): restore original variable names and whitespace in w…
jpnurmi Feb 23, 2026
9feac1b
Merge remote-tracking branch 'upstream/master' into jpnurmi/feat/http…
jpnurmi Feb 24, 2026
a710a78
docs: clarify sentry_transport_retry behavior and limitations
jpnurmi Feb 24, 2026
282bd46
docs(retry): document retry behavior for network failures vs HTTP res…
jpnurmi Feb 24, 2026
f8a4f07
fix(retry): only clear startup_time when envelope is written
jpnurmi Feb 24, 2026
b6260c0
fix(retry): check for NULL from sentry__path_clone
jpnurmi Feb 24, 2026
a55b944
fix(retry): apply backoff when system clock moves backward
jpnurmi Feb 24, 2026
81d0f68
fix(retry): increase SENTRY_RETRY_ATTEMPTS to 6 to match Crashpad
jpnurmi Feb 24, 2026
e28b88e
fix(retry): avoid retry flush consuming entire shutdown timeout
jpnurmi Feb 24, 2026
0b78591
fix(retry): warn on failed retry envelope rename
jpnurmi Feb 24, 2026
4fa8410
fix(retry): check for NULL from sentry__path_clone in retry send
jpnurmi Feb 24, 2026
cd376db
fix(retry): fix data race on startup_time between threads
jpnurmi Feb 24, 2026
889ba35
fix(retry): clear retry_func when retry fails to initialize
jpnurmi Feb 24, 2026
ce89de0
fix(retry): persist non-event envelopes to the retry cache
jpnurmi Feb 25, 2026
1881212
fix(retry): close race between poll task and enqueue on scheduled flag
jpnurmi Feb 25, 2026
ca270ea
Merge branch 'master' into jpnurmi/feat/http-retry
jpnurmi Feb 25, 2026
b35273b
refactor(database): add retry_count to write_envelope, add sentry__ru…
jpnurmi Feb 25, 2026
77cc3dd
refactor(database): make sentry_run_t refcounted
jpnurmi Feb 25, 2026
4c108f4
refactor(cache): strip retry prefix in move_cache and simplify handle…
jpnurmi Feb 25, 2026
e57c6fb
fix(cache): use cache_name instead of src_name for UUID in move_cache
jpnurmi Feb 25, 2026
5e80d4b
fix(retry): prevent duplicate cache writes during shutdown race
jpnurmi Feb 26, 2026
a5bb080
fix(cache): replace length heuristic with proper filename parsing in …
jpnurmi Feb 26, 2026
30dbb71
docs: fix retry count 5 → 6 in sentry_transport_retry docs
jpnurmi Feb 26, 2026
a3c584c
fix(retry): prevent poll task from re-arming after shutdown
jpnurmi Feb 26, 2026
b961573
fix(winhttp): cancel in-flight request before shutdown to unblock worker
jpnurmi Feb 26, 2026
8bf1503
fix(winhttp): fix double-close race on client->request between cancel…
jpnurmi Feb 26, 2026
bdc936a
fix(winhttp): use on_timeout callback to unblock worker instead of ca…
jpnurmi Feb 26, 2026
02e47f1
fix(winhttp): remove unnecessary local request snapshot in winhttp_se…
jpnurmi Feb 26, 2026
1730406
fix(sync): don't force running=0 before on_timeout callback
jpnurmi Feb 26, 2026
b373e70
fix(test): disable transport retry in unit tests to fix valgrind flak…
jpnurmi Feb 26, 2026
fcb96f2
fix(retry): clear sealed_envelope after match to prevent address-reus…
jpnurmi Feb 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

**Features**:

- Add HTTP retry with exponential backoff. ([#1520](https://github.com/getsentry/sentry-native/pull/1520))
- Support `SENTRY_SAMPLE_RATE` and `SENTRY_TRACES_SAMPLE_RATE` environment variables. ([#1540](https://github.com/getsentry/sentry-native/pull/1540))

**Fixes**:
Expand Down
3 changes: 3 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -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, "no-http-retry")) {
sentry_options_set_http_retry(options, false);
}

if (has_arg(argc, argv, "enable-metrics")) {
sentry_options_set_enable_metrics(options, true);
Expand Down
27 changes: 27 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,24 @@ 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.
*
* 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 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).
*
* 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);

/**
* Generic way to free transport.
*/
Expand Down Expand Up @@ -2146,6 +2164,15 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs(
SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs(
const sentry_options_t *opts);

/**
* Enables or disables HTTP retry with exponential backoff for network failures.
* Enabled by default.
*/
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);

/**
* Enables or disables custom attributes parsing for structured logging.
*
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 2 additions & 6 deletions src/backends/sentry_backend_crashpad.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -593,8 +591,6 @@ process_completed_reports(
sentry__path_free(out_path);
sentry_envelope_free(envelope);
}

sentry__path_free(cache_dir);
}

static int
Expand Down
2 changes: 1 addition & 1 deletion src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ sentry_init(sentry_options_t *options)
backend->prune_database_func(backend);
}

if (options->cache_keep) {
if (options->cache_keep || options->http_retry) {
sentry__cleanup_cache(options);
}

Expand Down
169 changes: 143 additions & 26 deletions src/sentry_database.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#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"
#include <errno.h>
#include <stdlib.h>
Expand Down Expand Up @@ -50,19 +53,32 @@ sentry__run_new(const sentry_path_t *database_path)
return NULL;
}

// `<db>/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;
}

run->refcount = 1;
run->uuid = uuid;
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;
Expand All @@ -80,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)
{
Expand All @@ -90,18 +115,20 @@ 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);
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 *path, const sentry_envelope_t *envelope,
int retry_count)
{
sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope);

Expand All @@ -111,13 +138,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;
}
Expand All @@ -136,7 +168,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
Expand All @@ -148,7 +180,104 @@ 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
sentry__parse_cache_filename(const char *filename, uint64_t *ts_out,
int *count_out, const char **uuid_out)
{
// Minimum retry filename: <ts>-<count>-<uuid>.envelope (49+ chars).
// Cache filenames are exactly 45 chars (<uuid>.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)
{
if (sentry__path_create_dir_all(run->cache_path) != 0) {
SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path);
return false;
}

char filename[128];
const char *src_name = sentry__path_filename(src);
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);
} else {
snprintf(filename, sizeof(filename), "%s", cache_name);
}

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\"", src_name);
return false;
}

return true;
}

bool
Expand Down Expand Up @@ -239,13 +368,9 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
continue;
}

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);
}
}
bool can_cache = options->cache_keep
&& (!options->http_retry
|| !sentry__transport_can_retry(options->transport));

sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir);
const sentry_path_t *file;
Expand Down Expand Up @@ -292,15 +417,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, -1)) {
continue;
}
}
Expand All @@ -309,7 +427,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);
}
Expand Down
Loading
Loading