Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Fixes

- Fix potential ANR/deadlock in Session Replay when `checkCanRecord` runs on the replay executor thread ([#5837](https://github.com/getsentry/sentry-java/pull/5837))
- Prevent concurrent PixelCopy access during Session Replay masking and bitmap cleanup ([#5808](https://github.com/getsentry/sentry-java/pull/5808))
- Release `MediaMuxer` when the replay video encoder fails to start to avoid a resource leak ([#5607](https://github.com/getsentry/sentry-java/pull/5607))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Looper
import android.view.MotionEvent
import io.sentry.Breadcrumb
import io.sentry.DataCategory.All
Expand Down Expand Up @@ -352,7 +353,7 @@ public class ReplayIntegration(
}
addFrame(bitmap, frameTimeStamp, screen)
}
checkCanRecord()
postOnMainThread { checkCanRecord() }
Comment thread
romtsn marked this conversation as resolved.
}

override fun onScreenshotRecorded(screenshot: File, frameTimestamp: Long) {
Expand All @@ -375,7 +376,7 @@ public class ReplayIntegration(
}
addFrame(screenshot, frameTimestamp, screen)
}
checkCanRecord()
postOnMainThread { checkCanRecord() }
}

override fun close() {
Expand All @@ -390,21 +391,23 @@ public class ReplayIntegration(
recorder?.close()
recorder = null
rootViewsSpy.close()
if (lazyReplayExecutor.isInitialized()) {
if (options.threadChecker.isMainThread) {
replayExecutor.gracefulShutdown()
} else {
replayExecutor.shutdown()
}
lifecycle.currentState = CLOSED
}
// shutdown outside lock — awaiting termination while holding lifecycleLock deadlocks
// if any executor task tries to acquire the same lock
if (lazyReplayExecutor.isInitialized()) {
if (options.threadChecker.isMainThread) {
replayExecutor.gracefulShutdown()
} else {
replayExecutor.shutdown()
}
if (lazyPersistingExecutor.isInitialized()) {
if (options.threadChecker.isMainThread) {
persistingExecutor.gracefulShutdown()
} else {
persistingExecutor.shutdown()
}
}
if (lazyPersistingExecutor.isInitialized()) {
if (options.threadChecker.isMainThread) {
persistingExecutor.gracefulShutdown()
} else {
persistingExecutor.shutdown()
}
lifecycle.currentState = CLOSED
}
}

Expand Down Expand Up @@ -444,6 +447,17 @@ public class ReplayIntegration(
captureStrategy?.onTouchEvent(event)
}

// Runs [block] on the main thread. If already there, executes inline; otherwise posts via
// the main looper handler. Prevents deadlocks when lifecycle-lock-acquiring code (e.g.
// checkCanRecord -> pauseInternal) is called from the replay executor thread.
Comment thread
romtsn marked this conversation as resolved.
private inline fun postOnMainThread(crossinline block: () -> Unit) {
if (Looper.myLooper() == Looper.getMainLooper()) {
block()
} else {
mainLooperHandler.post { block() }
}
}

/**
* Check if we're offline or rate-limited and pause for session mode to not overflow the envelope
* cache.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import io.sentry.rrweb.RRWebMetaEvent
import io.sentry.rrweb.RRWebVideoEvent
import io.sentry.transport.CurrentDateProvider
import io.sentry.transport.ICurrentDateProvider
import io.sentry.transport.RateLimiter
import java.time.Duration
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
Expand All @@ -41,6 +42,7 @@ import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.check
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
Expand All @@ -61,11 +63,17 @@ class ReplaySmokeTest {
internal class Fixture {
val options = SentryOptions()
val scope = Scope(options)
val rateLimiter =
mock<RateLimiter> {
on { isActiveForCategory(any()) }.thenReturn(false)
}
val scopes =
mock<IScopes> {
doAnswer { (it.arguments[0] as ScopeCallback).run(scope) }
.whenever(it)
.configureScope(any())

on { rateLimiter }.doReturn(rateLimiter)
}

private class ImmediateHandler :
Expand All @@ -91,7 +99,10 @@ class ReplaySmokeTest {
mainLooperHandler =
mock {
whenever(mock.handler).thenReturn(ImmediateHandler())
whenever(mock.post(any())).then { (it.arguments[0] as Runnable).run() }
whenever(mock.post(any())).then {
(it.arguments[0] as Runnable).run()
true
}
whenever(mock.postDelayed(any(), anyLong())).then {
// have to use another thread here otherwise it will block the test thread
recordingThread.schedule(
Expand Down
Loading