Skip to content

Commit 856e99f

Browse files
tsushanthromtsn
authored andcommitted
refactor(replay): move persistingExecutor ownership to ReplayIntegration
Move persistingExecutor out of BaseCaptureStrategy and into ReplayIntegration, passing it as a constructor argument to CaptureStrategy subclasses. Shut it down in ReplayIntegration.close() alongside replayExecutor so executor lifecycle is managed in one place.
1 parent 8bef6ab commit 856e99f

6 files changed

Lines changed: 49 additions & 38 deletions

File tree

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ public class ReplayIntegration(
111111
val delegate = Executors.newSingleThreadScheduledExecutor(ReplayExecutorServiceThreadFactory())
112112
ReplayExecutorService(delegate, options)
113113
}
114+
private val persistingExecutor by lazy {
115+
val delegate =
116+
Executors.newSingleThreadScheduledExecutor(ReplayPersistingExecutorServiceThreadFactory())
117+
ReplayExecutorService(delegate, options)
118+
}
114119

115120
internal val isEnabled = AtomicBoolean(false)
116121
internal val isManualPause = AtomicBoolean(false)
@@ -192,6 +197,7 @@ public class ReplayIntegration(
192197
scopes,
193198
dateProvider,
194199
replayExecutor,
200+
persistingExecutor,
195201
replayCacheProvider,
196202
)
197203
} else {
@@ -201,6 +207,7 @@ public class ReplayIntegration(
201207
dateProvider,
202208
random,
203209
replayExecutor,
210+
persistingExecutor,
204211
replayCacheProvider,
205212
)
206213
}
@@ -374,6 +381,7 @@ public class ReplayIntegration(
374381
recorder = null
375382
rootViewsSpy.close()
376383
replayExecutor.shutdown()
384+
persistingExecutor.shutdown()
377385
lifecycle.currentState = CLOSED
378386
}
379387
}
@@ -554,4 +562,14 @@ public class ReplayIntegration(
554562
return ret
555563
}
556564
}
565+
566+
private class ReplayPersistingExecutorServiceThreadFactory : ThreadFactory {
567+
private var cnt = 0
568+
569+
override fun newThread(r: Runnable): Thread {
570+
val ret = Thread(r, "SentryReplayPersister-" + cnt++)
571+
ret.setDaemon(true)
572+
return ret
573+
}
574+
}
557575
}

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BaseCaptureStrategy.kt

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import io.sentry.android.replay.ScreenshotRecorderConfig
2525
import io.sentry.android.replay.capture.CaptureStrategy.Companion.createSegment
2626
import io.sentry.android.replay.capture.CaptureStrategy.ReplaySegment
2727
import io.sentry.android.replay.gestures.ReplayGestureConverter
28-
import io.sentry.android.replay.util.ReplayExecutorService
2928
import io.sentry.android.replay.util.ReplayRunnable
3029
import io.sentry.protocol.SentryId
3130
import io.sentry.rrweb.RRWebEvent
@@ -34,9 +33,7 @@ import java.io.File
3433
import java.util.Date
3534
import java.util.Deque
3635
import java.util.concurrent.ConcurrentLinkedDeque
37-
import java.util.concurrent.Executors
3836
import java.util.concurrent.ScheduledExecutorService
39-
import java.util.concurrent.ThreadFactory
4037
import java.util.concurrent.atomic.AtomicBoolean
4138
import java.util.concurrent.atomic.AtomicLong
4239
import java.util.concurrent.atomic.AtomicReference
@@ -50,6 +47,7 @@ internal abstract class BaseCaptureStrategy(
5047
private val scopes: IScopes?,
5148
private val dateProvider: ICurrentDateProvider,
5249
protected val replayExecutor: ScheduledExecutorService,
50+
protected val persistingExecutor: ScheduledExecutorService,
5351
private val replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null,
5452
) : CaptureStrategy {
5553
internal companion object {
@@ -58,21 +56,6 @@ internal abstract class BaseCaptureStrategy(
5856
private const val MAX_TRACE_IDS = 100
5957
}
6058

61-
// Explicit holder so we can detect whether the executor was ever initialised and shut it down
62-
// without initialising it as a side-effect (which is what a plain `lazy` delegate would do).
63-
private var persistingExecutorHolder: ScheduledExecutorService? = null
64-
private val persistingExecutor: ScheduledExecutorService
65-
get() {
66-
return persistingExecutorHolder
67-
?: run {
68-
val delegate =
69-
Executors.newSingleThreadScheduledExecutor(
70-
ReplayPersistingExecutorServiceThreadFactory()
71-
)
72-
ReplayExecutorService(delegate, options).also { persistingExecutorHolder = it }
73-
}
74-
}
75-
7659
private val gestureConverter = ReplayGestureConverter(dateProvider)
7760

7861
protected val isTerminating = AtomicBoolean(false)
@@ -133,13 +116,6 @@ internal abstract class BaseCaptureStrategy(
133116
replayStartTimestamp.set(0)
134117
segmentTimestamp = null
135118
currentReplayId = SentryId.EMPTY_ID
136-
// Shut down the persisting executor only if it was actually initialised. We must not call the
137-
// blocking ReplayExecutorService.shutdown() here because stop() may run on the main thread,
138-
// and blocking there risks an ANR. shutdownNow() is non-blocking: it cancels queued tasks and
139-
// interrupts any running one, which is acceptable because the tasks are best-effort persistence
140-
// writes and the cache has already been closed above.
141-
persistingExecutorHolder?.shutdownNow()
142-
persistingExecutorHolder = null
143119
}
144120

145121
protected fun createSegmentInternal(
@@ -209,16 +185,6 @@ internal abstract class BaseCaptureStrategy(
209185
}
210186
}
211187

212-
private class ReplayPersistingExecutorServiceThreadFactory : ThreadFactory {
213-
private var cnt = 0
214-
215-
override fun newThread(r: Runnable): Thread {
216-
val ret = Thread(r, "SentryReplayPersister-" + cnt++)
217-
ret.setDaemon(true)
218-
return ret
219-
}
220-
}
221-
222188
private inline fun <T> persistableAtomicNullable(
223189
initialValue: T? = null,
224190
propertyName: String,

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/BufferCaptureStrategy.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ internal class BufferCaptureStrategy(
3333
private val dateProvider: ICurrentDateProvider,
3434
private val random: Random,
3535
executor: ScheduledExecutorService,
36+
persistingExecutor: ScheduledExecutorService,
3637
replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null,
3738
) :
3839
BaseCaptureStrategy(
3940
options,
4041
scopes,
4142
dateProvider,
4243
executor,
44+
persistingExecutor,
4345
replayCacheProvider = replayCacheProvider,
4446
) {
4547
// TODO: capture envelopes for buffered segments instead, but don't send them until buffer is
@@ -150,8 +152,10 @@ internal class BufferCaptureStrategy(
150152
)
151153
return this
152154
}
153-
// we hand over replayExecutor to the new strategy to preserve order of execution
154-
val captureStrategy = SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor)
155+
// we hand over replayExecutor and persistingExecutor to the new strategy to preserve order of
156+
// execution
157+
val captureStrategy =
158+
SessionCaptureStrategy(options, scopes, dateProvider, replayExecutor, persistingExecutor)
155159
captureStrategy.recorderConfig = recorderConfig
156160
captureStrategy.start(
157161
segmentId = currentSegment,

sentry-android-replay/src/main/java/io/sentry/android/replay/capture/SessionCaptureStrategy.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ internal class SessionCaptureStrategy(
2121
private val scopes: IScopes?,
2222
private val dateProvider: ICurrentDateProvider,
2323
executor: ScheduledExecutorService,
24+
persistingExecutor: ScheduledExecutorService,
2425
replayCacheProvider: ((replayId: SentryId) -> ReplayCache)? = null,
25-
) : BaseCaptureStrategy(options, scopes, dateProvider, executor, replayCacheProvider) {
26+
) :
27+
BaseCaptureStrategy(
28+
options,
29+
scopes,
30+
dateProvider,
31+
executor,
32+
persistingExecutor,
33+
replayCacheProvider,
34+
) {
2635
internal companion object {
2736
private const val TAG = "SessionCaptureStrategy"
2837
}

sentry-android-replay/src/test/java/io/sentry/android/replay/capture/BufferCaptureStrategyTest.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ class BufferCaptureStrategyTest {
111111
null
112112
}
113113
},
114+
mock {
115+
whenever(it.submit(any<Runnable>())).doAnswer { invocation ->
116+
(invocation.arguments[0] as Runnable).run()
117+
null
118+
}
119+
},
114120
) { _ ->
115121
replayCache
116122
}

sentry-android-replay/src/test/java/io/sentry/android/replay/capture/SessionCaptureStrategyTest.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ class SessionCaptureStrategyTest {
123123
.whenever(it)
124124
.submit(any<Runnable>())
125125
},
126+
mock {
127+
doAnswer { invocation ->
128+
(invocation.arguments[0] as Runnable).run()
129+
null
130+
}
131+
.whenever(it)
132+
.submit(any<Runnable>())
133+
},
126134
) { _ ->
127135
replayCache
128136
}

0 commit comments

Comments
 (0)