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
Disk-backed surfaces that share paths across re-init
Scope cache (.scope-cache/, via PersistingScopeObserver + CacheUtils):
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 and Android PersistingOptionsCacheGenerationObserver):
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, Android AndroidEnvelopeCache):
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)
- Profiling traces dir cleanup on init vs in-flight profile finalization
- Session Replay dirs (
replay_<id>/) under cache dir (ReplayCache); cleanup walks cache dir on integration start
- ANR profile
QueueFiles under cache dir (AnrProfileManager / 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
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 [Sentry]
During SDK re-init (
Sentry.initwhile already enabled), previous and new SDK lifecycles can concurrently read/write the same on-disk paths undercacheDirPath. 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
isRestarting=true, then continues init immediately:Sentry.init→Scopes.close(true)Scopes.close(boolean)notifyOptionsObservers/movePreviousSession/finalizePreviousSessionDisk-backed surfaces that share paths across re-init
Scope cache (
.scope-cache/, viaPersistingScopeObserver+CacheUtils):breadcrumbs.json— singleQueueFile; old executor may still add/sync while new initresetCache()clears/syncs the same fileuser.json,tags.json,extras.json,contexts.json,request.json,level.json,fingerprint.json,transaction.json,trace.json,replay.json— plain overwrite/deleteof fixed names; no atomic replaceresetCache()after options observers run, which collides with any late old-lifecycle flushesOptions cache (
.options-cache/, viaPersistingOptionsObserverand AndroidPersistingOptionsCacheGenerationObserver):release.json,proguard-uuid.json,sdk-version.json,environment.json,dist.json,tags.json,replay-error-sample-rate.json,app-last-update-time.jsonFileOutputStreamoverwriteSession / envelope cache (
EnvelopeCache, AndroidAndroidEnvelopeCache):session.json/previous_session.json— move + rewrite on session start; previous-session finalize also reads/deletes these*.envelopeoffline cache — old client flush vs new client store/send against same dirlast_crash,.sentry-native/last_crash,startup_crash(outbox)Other fixed-path / shared-dir writers (same
cacheDirPath, lifecycle-overlapping on re-init):Sentry.handleAppStartProfilingConfig)replay_<id>/) under cache dir (ReplayCache); cleanup walks cache dir on integration startQueueFiles under cache dir (AnrProfileManager/ rotation helper already documents concurrentQueueFilerisk)Highest-confidence race today
Scope persistence breadcrumbs: single shared
breadcrumbs.jsonQueueFile, old executor still draining adds while new init clears it inresetCache(), with only executor ordering (not cross-lifecycle isolation) as the safety net.Related
Unknowns
Raised by Markus Hintersteiner during SDK re-init / disk-persistence discussion.
Requested by markus.hintersteiner.
--
View Junior Session [Sentry]