Skip to content

Commit 7a3e33b

Browse files
romtsnclaude
andcommitted
Use shutdownNow() for replay executors in close() to avoid ANR
shutdown() calls awaitTermination() which blocks up to shutdownTimeoutMillis. Since close() can run on the main thread (via Sentry.close() from hybrid SDKs), this risks an ANR. shutdownNow() is non-blocking and sufficient at teardown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3b21f8f commit 7a3e33b

2 files changed

Lines changed: 26 additions & 4 deletions

File tree

sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,15 +107,17 @@ public class ReplayIntegration(
107107
private var gestureRecorder: GestureRecorder? = null
108108
private val random by lazy { Random() }
109109
internal val rootViewsSpy by lazy { RootViewsSpy.install() }
110-
private val replayExecutor by lazy {
110+
private val lazyReplayExecutor = lazy {
111111
val delegate = Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
112112
ReplayExecutorService(delegate, options)
113113
}
114-
private val persistingExecutor by lazy {
114+
private val replayExecutor by lazyReplayExecutor
115+
private val lazyPersistingExecutor = lazy {
115116
val delegate =
116117
Executors.newSingleThreadScheduledExecutor(ReplayPersistingExecutorServiceThreadFactory())
117118
ReplayExecutorService(delegate, options)
118119
}
120+
private val persistingExecutor by lazyPersistingExecutor
119121

120122
internal val isEnabled = AtomicBoolean(false)
121123
internal val isManualPause = AtomicBoolean(false)
@@ -380,8 +382,20 @@ public class ReplayIntegration(
380382
recorder?.close()
381383
recorder = null
382384
rootViewsSpy.close()
383-
replayExecutor.shutdown()
384-
persistingExecutor.shutdown()
385+
if (lazyReplayExecutor.isInitialized()) {
386+
if (options.threadChecker.isMainThread) {
387+
replayExecutor.gracefulShutdown()
388+
} else {
389+
replayExecutor.shutdown()
390+
}
391+
}
392+
if (lazyPersistingExecutor.isInitialized()) {
393+
if (options.threadChecker.isMainThread) {
394+
persistingExecutor.gracefulShutdown()
395+
} else {
396+
persistingExecutor.shutdown()
397+
}
398+
}
385399
lifecycle.currentState = CLOSED
386400
}
387401
}

sentry-android-replay/src/main/java/io/sentry/android/replay/util/ReplayExecutorService.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,14 @@ internal class ReplayExecutorService(
5757
}
5858
}
5959
}
60+
61+
fun gracefulShutdown() {
62+
synchronized(this) {
63+
if (!isShutdown) {
64+
delegate.shutdown()
65+
}
66+
}
67+
}
6068
}
6169

6270
internal class ReplayRunnable(val taskName: String, delegate: Runnable) : Runnable by delegate

0 commit comments

Comments
 (0)