Fix deadlock with nested calls to runBlocking.

Fixes #14460
This commit is contained in:
Greyson Parrelli
2025-11-28 13:39:07 -05:00
parent fee062e727
commit 2bf3ec60eb
3 changed files with 77 additions and 7 deletions

View File

@@ -5,8 +5,7 @@
package org.thoughtcrime.securesms.backup.v2
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import androidx.annotation.VisibleForTesting
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.ThrottledDebouncer
import java.util.concurrent.ExecutionException
@@ -19,7 +18,10 @@ import kotlin.time.Duration.Companion.seconds
*/
object ArchiveDatabaseExecutor {
val executor = Executors.newSingleThreadExecutor(SignalExecutors.NumberedThreadFactory("archive-db", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD))
@VisibleForTesting
const val THREAD_NAME = "archive-db"
private val executor = Executors.newSingleThreadExecutor { Thread(it, THREAD_NAME) }
/**
* By default, downloading/uploading an attachment wants to notify a bunch of database observation listeners. This slams the observer so hard that other
@@ -39,6 +41,10 @@ object ArchiveDatabaseExecutor {
}
fun <T> runBlocking(block: () -> T): T {
if (Thread.currentThread().name.equals(THREAD_NAME)) {
return block()
}
return try {
executor.submit(block).get()
} catch (e: ExecutionException) {

View File

@@ -360,11 +360,9 @@ class UploadAttachmentToArchiveJob private constructor(
}
private fun setArchiveTransferStateWithDelayedNotification(attachmentId: AttachmentId, transferState: AttachmentTable.ArchiveTransferState) {
ArchiveDatabaseExecutor.runBlocking {
SignalDatabase.attachments.setArchiveTransferState(attachmentId, transferState, notify = false)
ArchiveDatabaseExecutor.throttledNotifyAttachmentObservers()
}
}
class Factory : Job.Factory<UploadAttachmentToArchiveJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): UploadAttachmentToArchiveJob {

View File

@@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.backup.v2
import android.app.Application
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
@RunWith(RobolectricTestRunner::class)
@Config(manifest = Config.NONE, application = Application::class)
class ArchiveDatabaseExecutorTest {
@Test
fun `runBlocking returns the result of the block`() {
val result = ArchiveDatabaseExecutor.runBlocking {
42
}
assertThat(result).isEqualTo(42)
}
@Test
fun `nested runBlocking calls do not deadlock`() {
val completed = AtomicBoolean(false)
val result = AtomicReference<String>()
val latch = CountDownLatch(1)
Thread {
try {
val r = ArchiveDatabaseExecutor.runBlocking {
ArchiveDatabaseExecutor.runBlocking {
ArchiveDatabaseExecutor.runBlocking {
"nested-result"
}
}
}
result.set(r)
completed.set(true)
} finally {
latch.countDown()
}
}.start()
val finishedInTime = latch.await(5, TimeUnit.SECONDS)
assertThat(finishedInTime).isTrue()
assertThat(completed.get()).isTrue()
assertThat(result.get()).isEqualTo("nested-result")
}
@Test
fun `runBlocking executes on archive-db thread`() {
val threadName = ArchiveDatabaseExecutor.runBlocking {
Thread.currentThread().name
}
assertThat(threadName).isEqualTo(ArchiveDatabaseExecutor.THREAD_NAME)
}
}