getsentry / getsentry/sentry-java

Disk-persisted data can race during SDK re-init

Open
#5,959 1 comment 0 reactions 0 assignees View on GitHub
Android Improvement Platform: Java Type: Bug
Dominant language
Kotlin
Stars
1.4k
Forks
478
Avg merge
2d 23h
Merged PRs (30d)
67

Description

During SDK re-init (`Sentry.init` while already enabled), previous and new SDK lifecycles can concurrently read/write the same on-disk paths under `cacheDirPath`. On restart, `Scopes.close(true)` shuts the old executor down asynchronously rather than blocking, so old flush/session/envelope work can still touch files while the new init writes the same locations.

Impact: corrupted or lost scope/options/session state used for ANR/exit enrichment and crash recovery; possible lost breadcrumbs.

## Why this races

- Re-init path closes previous scopes with `isRestarting=true`, then continues init immediately: [`Sentry.init`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Sentry.java) → [`Scopes.close(true)`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Scopes.java)
- On restart the old executor is closed via a submitted task (non-blocking), so pending disk work can still run briefly: [`Scopes.close(boolean)`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Scopes.java)
- New init then enqueues session move/finalize, options observers, and scope-cache reset on the **new** executor against the same cache paths: [`notifyOptionsObservers` / `movePreviousSession` / `finalizePreviousSession`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Sentry.java)
- Fixed filenames are not isolated per SDK lifecycle / run id

## Disk-backed surfaces that share paths across re-init

**Scope cache** (`.scope-cache/`, via [`PersistingScopeObserver`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/cache/PersistingScopeObserver.java) + [`CacheUtils`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/cache/CacheUtils.java)):
- `breadcrumbs.json` — single `QueueFile`; old executor may still add/sync while new init `resetCache()` clears/syncs the same file
- `user.json`, `tags.json`, `extras.json`, `contexts.json`, `request.json`, `level.json`, `fingerprint.json`, `transaction.json`, `trace.json`, `replay.json` — plain overwrite/`delete` of fixed names; no atomic replace
- New init explicitly `resetCache()` after options observers run, which collides with any late old-lifecycle flushes

**Options cache** (`.options-cache/`, via [`PersistingOptionsObserver`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/cache/PersistingOptionsObserver.java) and Android [`PersistingOptionsCacheGenerationObserver`](https://github.com/getsentry/sentry-java/blob/main/sentry-android-core/src/main/java/io/sentry/android/core/PersistingOptionsCacheGenerationObserver.java)):
- `release.json`, `proguard-uuid.json`, `sdk-version.json`, `environment.json`, `dist.json`, `tags.json`, `replay-error-sample-rate.json`, `app-last-update-time.json`
- Written on every init via options observers; same fixed paths, non-atomic `FileOutputStream` overwrite

**Session / envelope cache** ([`EnvelopeCache`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/cache/EnvelopeCache.java), Android [`AndroidEnvelopeCache`](https://github.com/getsentry/sentry-java/blob/main/sentry-android-core/src/main/java/io/sentry/android/core/cache/AndroidEnvelopeCache.java)):
- `session.json` / `previous_session.json` — move + rewrite on session start; previous-session finalize also reads/deletes these
- `*.envelope` offline cache — old client flush vs new client store/send against same dir
- crash markers: `last_crash`, `.sentry-native/last_crash`, `startup_crash` (outbox)
- Android ANR last-reported markers under cache dir

**Other fixed-path / shared-dir writers** (same `cacheDirPath`, lifecycle-overlapping on re-init):
- App-start profiling config file rewritten on init ([`Sentry.handleAppStartProfilingConfig`](https://github.com/getsentry/sentry-java/blob/main/sentry/src/main/java/io/sentry/Sentry.java))
- Profiling traces dir cleanup on init vs in-flight profile finalization
- Session Replay dirs (`replay_/`) under cache dir ([`ReplayCache`](https://github.com/getsentry/sentry-java/blob/main/sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayCache.kt)); cleanup walks cache dir on integration start
- ANR profile `QueueFile`s under cache dir ([`AnrProfileManager`](https://github.com/getsentry/sentry-java/blob/main/sentry-android-core/src/main/java/io/sentry/android/core/anr/AnrProfileManager.java) / rotation helper already documents concurrent `QueueFile` risk)

## Highest-confidence race today

Scope persistence breadcrumbs: single shared `breadcrumbs.json` `QueueFile`, old executor still draining adds while new init clears it in `resetCache()`, with only executor ordering (not cross-lifecycle isolation) as the safety net.

## Related

- Closed perf work on scope persistence writes: https://github.com/getsentry/sentry-java/issues/5714
- Closed re-init executor timeout work: https://github.com/getsentry/sentry-java/issues/5715
- Closed “don’t wait when SDK is reinitialized”: https://github.com/getsentry/sentry-java/issues/3162

## Unknowns

- Whether this has been observed in production beyond code inspection / reasoning about re-init
- How often customer/integration re-init hits the overlapping-executor window in practice

Raised by Markus Hintersteiner during SDK re-init / disk-persistence discussion.

Requested by **markus.hintersteiner**.

--

[View Junior Session](https://junior-prod.sentry.dev/conversations/slack%3ACUCRT2B70%3A1786606836.172319) [[Sentry]](https://sentry.sentry.io/explore/conversations/slack%3ACUCRT2B70%3A1786606836.172319/?project=4510944073809921)

Contributor guide

Open the contributing guide

Research direction

Trace re-initialization from Sentry.init through Scopes.close(boolean), then inspect PersistingScopeObserver, CacheUtils, PersistingOptionsObserver, and EnvelopeCache. Focus first on the shared breadcrumbs.json QueueFile and the old executor's shutdown behavior; done should demonstrate that re-init cannot overlap disk operations or lose persisted scope, options, session, or envelope data.

Written by the indexing model from the issue text.

Assessment

Tech stack
java, kotlin
Domain
mobile, tooling
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.