Skip to content

Commit 0fbf22c

Browse files
JoshuaMoelansjpnurmi
authored andcommitted
feat: offline caching + tests for inproc & breakpad (#1490)
#1490
1 parent 3eb7ad1 commit 0fbf22c

15 files changed

Lines changed: 577 additions & 5 deletions

examples/example.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,11 @@ main(int argc, char **argv)
503503
if (has_arg(argc, argv, "log-attributes")) {
504504
sentry_options_set_logs_with_attributes(options, true);
505505
}
506+
if (has_arg(argc, argv, "cache-keep")) {
507+
sentry_options_set_cache_keep(options, true);
508+
sentry_options_set_cache_max_size(options, 4 * 1024 * 1024);
509+
sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60);
510+
}
506511

507512
if (0 != sentry_init(options)) {
508513
return EXIT_FAILURE;

include/sentry.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,6 +1375,37 @@ SENTRY_API void sentry_options_set_symbolize_stacktraces(
13751375
SENTRY_API int sentry_options_get_symbolize_stacktraces(
13761376
const sentry_options_t *opts);
13771377

1378+
/**
1379+
* Enables or disables storing envelopes in a persistent cache.
1380+
*
1381+
* When enabled, envelopes are written to a `cache/` subdirectory within the
1382+
* database directory and retained regardless of send success or failure.
1383+
* The cache is cleared on startup based on the cache_max_size and cache_max_age
1384+
* options.
1385+
*/
1386+
SENTRY_API void sentry_options_set_cache_keep(
1387+
sentry_options_t *opts, int enabled);
1388+
1389+
/**
1390+
* Sets the maximum size (in bytes) for the cache directory.
1391+
* On startup, cached entries are removed from oldest to newest until the
1392+
* directory size is within the max size limit.
1393+
*/
1394+
SENTRY_API void sentry_options_set_cache_max_size(
1395+
sentry_options_t *opts, size_t bytes);
1396+
1397+
/**
1398+
* Sets the maximum age (in seconds) for cache entries in the cache directory.
1399+
* On startup, cached entries exceeding the max age limit are removed.
1400+
*/
1401+
SENTRY_API void sentry_options_set_cache_max_age(
1402+
sentry_options_t *opts, uint64_t seconds);
1403+
1404+
/**
1405+
* Gets the caching mode for crash reports.
1406+
*/
1407+
SENTRY_API int sentry_options_get_cache_keep(const sentry_options_t *opts);
1408+
13781409
/**
13791410
* Adds a new attachment to be sent along.
13801411
*

src/backends/sentry_backend_crashpad.cpp

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -749,11 +749,16 @@ crashpad_backend_prune_database(sentry_backend_t *backend)
749749
// complete database to a maximum of 8M. That might still be a lot for
750750
// an embedded use-case, but minidumps on desktop can sometimes be quite
751751
// large.
752-
data->db->CleanDatabase(60 * 60 * 24 * 2);
753-
crashpad::BinaryPruneCondition condition(crashpad::BinaryPruneCondition::OR,
754-
new crashpad::DatabaseSizePruneCondition(1024 * 8),
755-
new crashpad::AgePruneCondition(2));
756-
crashpad::PruneCrashReportDatabase(data->db, &condition);
752+
SENTRY_WITH_OPTIONS (options) {
753+
data->db->CleanDatabase(options->cache_max_age);
754+
crashpad::BinaryPruneCondition condition(
755+
crashpad::BinaryPruneCondition::OR,
756+
new crashpad::DatabaseSizePruneCondition(
757+
options->cache_max_size / 1024),
758+
new crashpad::AgePruneCondition(
759+
options->cache_max_age / (24 * 60 * 60)));
760+
crashpad::PruneCrashReportDatabase(data->db, &condition);
761+
}
757762
}
758763

759764
#if defined(SENTRY_PLATFORM_WINDOWS) || defined(SENTRY_PLATFORM_LINUX)

src/path/sentry_path_unix.c

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,14 @@ sentry__path_remove(const sentry_path_t *path)
324324
return 1;
325325
}
326326

327+
int
328+
sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst)
329+
{
330+
int status;
331+
EINTR_RETRY(rename(src->path, dst->path), &status);
332+
return status == 0 ? 0 : 1;
333+
}
334+
327335
int
328336
sentry__path_create_dir_all(const sentry_path_t *path)
329337
{

src/path/sentry_path_windows.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,18 @@ sentry__path_remove(const sentry_path_t *path)
511511
return removal_success ? 0 : !is_last_error_path_not_found();
512512
}
513513

514+
int
515+
sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst)
516+
{
517+
wchar_t *src_w = src->path_w;
518+
wchar_t *dst_w = dst->path_w;
519+
if (!src_w || !dst_w) {
520+
return 1;
521+
}
522+
// MOVEFILE_REPLACE_EXISTING allows overwriting the destination if it exists
523+
return MoveFileExW(src_w, dst_w, MOVEFILE_REPLACE_EXISTING) ? 0 : 1;
524+
}
525+
514526
int
515527
sentry__path_create_dir_all(const sentry_path_t *path)
516528
{

src/sentry_core.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ sentry_init(sentry_options_t *options)
288288
backend->prune_database_func(backend);
289289
}
290290

291+
if (options->cache_keep) {
292+
sentry__cleanup_cache(options);
293+
}
294+
291295
if (options->auto_session_tracking) {
292296
sentry_start_session();
293297
}

src/sentry_database.c

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "sentry_session.h"
77
#include "sentry_uuid.h"
88
#include <errno.h>
9+
#include <stdlib.h>
910
#include <string.h>
1011

1112
sentry_run_t *
@@ -237,6 +238,13 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
237238
if (strcmp(options->run->run_path->path, run_dir->path) == 0) {
238239
continue;
239240
}
241+
242+
sentry_path_t *cache_dir = NULL;
243+
if (options->cache_keep) {
244+
cache_dir = sentry__path_join_str(options->database_path, "cache");
245+
sentry__path_create_dir_all(cache_dir);
246+
}
247+
240248
sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir);
241249
const sentry_path_t *file;
242250
while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) {
@@ -281,12 +289,24 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
281289
} else if (sentry__path_ends_with(file, ".envelope")) {
282290
sentry_envelope_t *envelope = sentry__envelope_from_path(file);
283291
sentry__capture_envelope(options->transport, envelope);
292+
293+
if (options->cache_keep) {
294+
sentry_path_t *cached_file = sentry__path_join_str(
295+
cache_dir, sentry__path_filename(file));
296+
sentry__path_rename(file, cached_file);
297+
sentry__path_free(cached_file);
298+
continue;
299+
}
284300
}
285301

286302
sentry__path_remove(file);
287303
}
288304
sentry__pathiter_free(run_iter);
289305

306+
if (options->cache_keep) {
307+
sentry__path_free(cache_dir);
308+
}
309+
290310
sentry__path_remove_all(run_dir);
291311
sentry__filelock_free(lock);
292312
}
@@ -295,6 +315,123 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash)
295315
sentry__capture_envelope(options->transport, session_envelope);
296316
}
297317

318+
// Cache Pruning below is based on prune_crash_reports.cc from Crashpad
319+
320+
/**
321+
* A cache entry with its metadata for sorting and pruning decisions.
322+
*/
323+
typedef struct {
324+
sentry_path_t *path;
325+
time_t mtime;
326+
size_t size;
327+
} cache_entry_t;
328+
329+
/**
330+
* Comparison function to sort cache entries by mtime, newest first.
331+
*/
332+
static int
333+
compare_cache_entries_newest_first(const void *a, const void *b)
334+
{
335+
const cache_entry_t *entry_a = (const cache_entry_t *)a;
336+
const cache_entry_t *entry_b = (const cache_entry_t *)b;
337+
// Newest first: if b is newer, return positive (b comes before a)
338+
if (entry_b->mtime > entry_a->mtime) {
339+
return 1;
340+
}
341+
if (entry_b->mtime < entry_a->mtime) {
342+
return -1;
343+
}
344+
return 0;
345+
}
346+
347+
void
348+
sentry__cleanup_cache(const sentry_options_t *options)
349+
{
350+
if (!options->database_path) {
351+
return;
352+
}
353+
354+
sentry_path_t *cache_dir
355+
= sentry__path_join_str(options->database_path, "cache");
356+
if (!sentry__path_is_dir(cache_dir)) {
357+
sentry__path_free(cache_dir);
358+
return;
359+
}
360+
361+
// First pass: collect all cache entries with their metadata
362+
size_t entries_capacity = 16;
363+
size_t entries_count = 0;
364+
cache_entry_t *entries
365+
= sentry_malloc(sizeof(cache_entry_t) * entries_capacity);
366+
if (!entries) {
367+
sentry__path_free(cache_dir);
368+
return;
369+
}
370+
371+
sentry_pathiter_t *iter = sentry__path_iter_directory(cache_dir);
372+
const sentry_path_t *entry;
373+
while (iter && (entry = sentry__pathiter_next(iter)) != NULL) {
374+
if (sentry__path_is_dir(entry)) {
375+
continue;
376+
}
377+
378+
// Grow array if needed
379+
if (entries_count >= entries_capacity) {
380+
entries_capacity *= 2;
381+
cache_entry_t *new_entries
382+
= sentry_malloc(sizeof(cache_entry_t) * entries_capacity);
383+
if (!new_entries) {
384+
break;
385+
}
386+
memcpy(new_entries, entries, sizeof(cache_entry_t) * entries_count);
387+
sentry_free(entries);
388+
entries = new_entries;
389+
}
390+
391+
entries[entries_count].path = sentry__path_clone(entry);
392+
entries[entries_count].mtime = sentry__path_get_mtime(entry);
393+
entries[entries_count].size = sentry__path_get_size(entry);
394+
entries_count++;
395+
}
396+
sentry__pathiter_free(iter);
397+
398+
// Sort by mtime, newest first (like crashpad)
399+
// This ensures we keep the newest entries when pruning by size
400+
qsort(entries, entries_count, sizeof(cache_entry_t),
401+
compare_cache_entries_newest_first);
402+
403+
// Calculate the age threshold
404+
time_t now = time(NULL);
405+
time_t oldest_allowed = now - options->cache_max_age;
406+
407+
// Prune entries: iterate newest-to-oldest, accumulating size
408+
// Remove if: too old OR accumulated size exceeds limit
409+
size_t accumulated_size = 0;
410+
for (size_t i = 0; i < entries_count; i++) {
411+
bool should_prune = false;
412+
413+
// Age-based pruning
414+
if (options->cache_max_age > 0 && entries[i].mtime < oldest_allowed) {
415+
should_prune = true;
416+
}
417+
418+
// Size-based pruning (accumulate size as we go, like crashpad)
419+
accumulated_size += entries[i].size;
420+
if (options->cache_max_size > 0
421+
&& accumulated_size > options->cache_max_size) {
422+
should_prune = true;
423+
}
424+
425+
if (should_prune) {
426+
sentry__path_remove_all(entries[i].path);
427+
}
428+
sentry__path_free(entries[i].path);
429+
}
430+
431+
sentry_free(entries);
432+
sentry__path_free(cache_dir);
433+
}
434+
298435
static const char *g_last_crash_filename = "last_crash";
299436

300437
bool

src/sentry_database.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ bool sentry__run_clear_session(const sentry_run_t *run);
7878
void sentry__process_old_runs(
7979
const sentry_options_t *options, uint64_t last_crash);
8080

81+
/**
82+
* Cleans up the cache based on options.max_cache_size and
83+
* options.max_cache_age.
84+
*/
85+
void sentry__cleanup_cache(const sentry_options_t *options);
86+
8187
/**
8288
* This will write the current ISO8601 formatted timestamp into the
8389
* `<database>/last_crash` file.

src/sentry_options.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ sentry_options_new(void)
5353
opts->enable_logging_when_crashed = true;
5454
opts->propagate_traceparent = false;
5555
opts->crashpad_limit_stack_capture_to_sp = false;
56+
opts->cache_keep = false;
57+
opts->cache_max_age = 2 * 24 * 60 * 60;
58+
opts->cache_max_size = 8 * 1024 * 1024;
5659
opts->symbolize_stacktraces =
5760
// AIX doesn't have reliable debug IDs for server-side symbolication,
5861
// and the diversity of Android makes it infeasible to have access to debug
@@ -475,6 +478,30 @@ sentry_options_get_symbolize_stacktraces(const sentry_options_t *opts)
475478
return opts->symbolize_stacktraces;
476479
}
477480

481+
void
482+
sentry_options_set_cache_keep(sentry_options_t *opts, int enabled)
483+
{
484+
opts->cache_keep = !!enabled;
485+
}
486+
487+
void
488+
sentry_options_set_cache_max_size(sentry_options_t *opts, size_t bytes)
489+
{
490+
opts->cache_max_size = bytes;
491+
}
492+
493+
void
494+
sentry_options_set_cache_max_age(sentry_options_t *opts, uint64_t seconds)
495+
{
496+
opts->cache_max_age = seconds;
497+
}
498+
499+
int
500+
sentry_options_get_cache_keep(const sentry_options_t *opts)
501+
{
502+
return opts->cache_keep;
503+
}
504+
478505
void
479506
sentry_options_set_system_crash_reporter_enabled(
480507
sentry_options_t *opts, int enabled)

src/sentry_options.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ struct sentry_options_s {
4545
bool enable_logging_when_crashed;
4646
bool propagate_traceparent;
4747
bool crashpad_limit_stack_capture_to_sp;
48+
bool cache_keep;
49+
50+
uint64_t cache_max_age;
51+
size_t cache_max_size;
4852

4953
sentry_attachment_t *attachments;
5054
sentry_run_t *run;

0 commit comments

Comments
 (0)