📌

AndroidのRealm周りでのクラッシュ調査(Realm access from incorrect thread)

2024/12/05に公開

原因究明までの記録

間違っていたらご指摘いただけると嬉しいです!Coroutineにもkotlinにも知見があるわけではないので、とても助かります!

crashの内容

crash

Realm access from incorrect thread. Realm objects can only be accessed on the thread they were created.

crashlyticsのログ

io.realm.BaseRealm.checkIfValid (BaseRealm.java:530)
io.realm.BaseRealm.refresh (BaseRealm.java:199)
io.realm.Realm.refresh (Realm.java:138)
io.realm.kotlin.RealmExtensionsKt.executeTransactionAwait (RealmExtensions.kt:153)
io.realm.kotlin.RealmExtensionsKt$executeTransactionAwait$1.invokeSuspend (Unknown Source:13)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely (CoroutineScheduler.kt:570)
kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask (CoroutineScheduler.kt:750)
kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker (CoroutineScheduler.kt:677)
kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run (CoroutineScheduler.kt:664)

基本的にRealmのインスタンスやRealmResultは生成したthread以外からアクセスできない。(freezeするとか例外はあるが)

調査過程

エラー文より、以下のcheckIfValidでrealmのインスタンスのthreadIdとcurrentThreadのidが違うことによってIllegalStateExceptionが発生している。GitHubのlink

    protected void checkIfValid() {
        if (sharedRealm == null || sharedRealm.isClosed()) {
            throw new IllegalStateException(BaseRealm.CLOSED_REALM_MESSAGE);
        }

        // Checks if we are in the right thread.
        if (!frozen && threadId != Thread.currentThread().getId()) {
            throw new IllegalStateException(BaseRealm.INCORRECT_THREAD_MESSAGE);
        }
    }

これがどこで呼ばれているかっていうと、crashlyticsのログにある通り、executeTransactionAwaitのrefreshから GitHubのlink

suspend fun Realm.executeTransactionAwait(
        context: CoroutineContext = Realm.WRITE_EXECUTOR.asCoroutineDispatcher(),
        transaction: (realm: Realm) -> Unit
) {
    // Default to our own thread pool executor (as dispatcher)
    withContext(context) {
        // Get a new coroutine-confined Realm instance from the original Realm's configuration
        Realm.getInstance(configuration).use { coroutineRealm ->
            // Ensure cooperation and prevent execution if the scope is not active.
            if (isActive) {
                coroutineRealm.executeTransaction(transaction)
            }
        }
    }

    // force refresh because we risk fetching stale data from other realms
    refresh()
}

refreshのスレッドとsuspendの呼出元のスレッドが一致することが担保されていない。
それによってrealmインスタンスが持つthreadIdとthreadが一致しなくなりcrash。つまりrealm側の実装ミスという結論。

結論

executeTransactionAwaitのせい

補足

refreshのスレッドとsuspendの呼出元のスレッドが一致することが担保されていない。

という部分を補足すると
withContext

Calls to withContext whose context argument provides a CoroutineDispatcher that is different from the current one, by necessity, perform additional dispatches: the block can not be executed immediately and needs to be dispatched for execution on the passed CoroutineDispatcher, and then when the block completes, the execution has to shift back to the original dispatcher.

ドキュメントより、異なるCoroutineDispatcherを指定しているとき、blockが完了すると元のdispatcherに戻るが、threadまでは担保されない。
同じCoroutineDispatcherを指定したときにblockが完了した時にどうなるのか。その挙動についての情報が得られてない。動作確認上は、途中でsuspendしてスレッド変えない限りthreadは変わらなかった。もしその挙動が担保されているなら、Realmインスタンスを取得した時と同じCoroutineDispatcherを指定すれば直る。根拠不足なので結論としなかった。

Discussion