Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Crash in copyToRealm. #1409

Closed
darkan23 opened this issue Jun 6, 2023 · 9 comments
Closed

Crash in copyToRealm. #1409

darkan23 opened this issue Jun 6, 2023 · 9 comments

Comments

@darkan23
Copy link

darkan23 commented Jun 6, 2023

How frequently does the bug occur?

Sometimes

Description

I've got a weird crash when copyToRealm.

open class PendingPhone : RealmObject {
      @PrimaryKey
      var pendingId: String = ""
      var client: Client? = null
      var dealtId: Long = 0
      var clientId: Long = 0
      var phoneType: PhoneType? = null
      var phoneColorType: PhoneColorType? = null
      var phoneOwnerType: PhoneOwnerType? = null
      var phoneNumber: String = ""
      var description: String? = null
      var creatingDate: Long = 0
      var syncDate: Long? = null
}

I am trying to save the given object in Realm:

  val creatingDate = Instant.now().toEpochMilli()
  val newPhone = PendingPhone().apply {
     this.pendingId = pendingId
     this.debtor = debtor
     this.debtId = debtId
     this.debtorId = debtor.debtorId
     this.phoneType = phoneType
     this.phoneColorType = null
     this.phoneOwnerType = phoneOwnerType
     this.phoneNumber = phoneNumber
     this.description = description
     this.creatingDate = creatingDate
     this.syncDate = null
  }
  realmTemplate.executeInRealm { realm ->
     realm.copyToRealm(newPhone)
  }
 suspend fun <T> executeInRealm(code: (MutableRealm) -> T): = realm.writeBlocking {
    code.invoke(this)
}

In this case, if you call findLatest before saving, as the error says, then after the error it will say that the object has not yet been created, use copyToRealm

What am I doing wrong??

Stacktrace & log output

Stacktrace below :

java.lang.IllegalArgumentException: Cannot import an outdated object. Use findLatest(object) to find an up-to-date version of the object in the given context before importing it.

Can you reproduce the bug?

  • select --

Reproduction Steps

No response

Version

1.8.0

What Atlas App Services are you using?

Local Database only

Are you using encryption?

Yes

Platform OS and version(s)

Android, v. 10

Build environment

Android Studio version: ...
Android Build Tools version: ...
Gradle version: ...

@cmelchior
Copy link
Contributor

Hi @darkan23

This happens when you try to copyToRealm an already managed Realm Object that is outdated. Most likely, you are doing something like this:

// Create unmanaged person outside the write transaction
val person = Person("Jane Doe") // Create unmanaged person
person.child = realm.query<Child>("name == 'Tim'").first().find()!! // Reference a managed object

realm.write {
  // Copying the person here might throw the error you are seeing if the Realm progressed from the time you did the query.
  copyToRealm(person) 
}

Is it something like this you are doing?

@sync-by-unito sync-by-unito bot added the Waiting-For-Reporter Waiting for more information from the reporter before we can proceed label Jun 6, 2023
@darkan23
Copy link
Author

darkan23 commented Jun 6, 2023

@cmelchior

Yes, before I save the phone, I'm looking for a client.

override fun createPendingPhone(
    dealId: Long,
    pendingId: String,
    phoneType: PhoneType?,
    phoneOwnerType: PhoneOwnerType?,
    phoneNumber: String,
    description: String?,
) {
    appScope.launch {
        val client = realmTemplate.findFirstInRealm { realm ->
            realm.query<Client>("deals.dealId = $dealId")
        }
        if (client == null || !client.isValid()) {
            log.warn("No client for deal with id $dealId! Can't save pending phone.")
        } else {
            val creatingDate = Instant.now().toEpochMilli()
            val newPhone = PendingPhone().apply {
                this.pendingId = pendingId
                this.client = client
                this.dealId = dealId
                this.clientId = client.clientId
                this.phoneType = phoneType
                this.phoneColorType = null
                this.phoneOwnerType = phoneOwnerType
                this.phoneNumber = phoneNumber
                this.description = description
                this.creatingDate = creatingDate
                this.syncDate = null
            }
            realmTemplate.executeInRealm { realm ->
                realm.copyToRealm(newPhone)
            }
        }
    }
}

@github-actions github-actions bot added Needs-Attention Reporter has responded. Review comment. and removed Waiting-For-Reporter Waiting for more information from the reporter before we can proceed labels Jun 6, 2023
@cmelchior
Copy link
Contributor

Yes, that pattern is a problem. The reason it is a problem is that Realm Objects are versioned and thus might change once you enter a write transaction.

I.e. in your logic, you might read Client at version 1 and store it in your object, then, just before doing the write, another thread deletes that Client object.

Then when you enter the write, it is unclear what the correct behavior would be.

In cases like this, you basically have two options:

  1. You can use copyFromRealm() on the Client object when you assign it to PendingPhone. That would work, but you might accidentally end up overwriting data you didn't want to.

  2. Move all the logic inside the write transactions. You can only ever have active write transaction, so that will ensure we can guarantee data integrity. So something like this:

override fun createPendingPhone(
    dealId: Long,
    pendingId: String,
    phoneType: PhoneType?,
    phoneOwnerType: PhoneOwnerType?,
    phoneNumber: String,
    description: String?,
) {
    appScope.launch {
        // Always start transaction
        realmTemplate.executeInRealm { realm ->
            val client = realm.query<Client>("deals.dealId = $dealId")
            if (client == null || !client.isValid()) {
                log.warn("No client for deal with id $dealId! Can't save pending phone.")
                realm.cancelTransaction()
            } else {
                val creatingDate = Instant.now().toEpochMilli()
                val newPhone = PendingPhone().apply {
                    this.pendingId = pendingId
                    this.client = client
                    this.dealId = dealId
                    this.clientId = client.clientId
                    this.phoneType = phoneType
                    this.phoneColorType = null
                    this.phoneOwnerType = phoneOwnerType
                    this.phoneNumber = phoneNumber
                    this.description = description
                    this.creatingDate = creatingDate
                    this.syncDate = null
                }
                realm.copyToRealm(newPhone)
            }
        }
}

I would strongly recommend option 2.

@sync-by-unito sync-by-unito bot added Waiting-For-Reporter Waiting for more information from the reporter before we can proceed and removed Needs-Attention Reporter has responded. Review comment. labels Jun 6, 2023
@darkan23
Copy link
Author

darkan23 commented Jun 6, 2023

@cmelchior

Thanks for the help!

@darkan23 darkan23 closed this as completed Jun 6, 2023
@github-actions github-actions bot added Needs-Attention Reporter has responded. Review comment. and removed Waiting-For-Reporter Waiting for more information from the reporter before we can proceed labels Jun 6, 2023
@Igro182
Copy link

Igro182 commented Jun 8, 2023

Hi!
@cmelchior thank you for the explanation. it solved my problem.
I wanted to ask another question here. i think it's the right topic.

Is this a good example of using:


            //Sample with find latest
            realmTemplate.write { realm ->
                val latestPerson = realm.findLatest(person)

                //do something with latestPerson, like add phone or anything

                realm.copyToRealm(latestPerson)
            }

Thank you!

@cmelchior
Copy link
Contributor

@Igro182

You don't need the last copyToRealm. Once you used findLatest you have access to the mutable Realm data and can just modify that object directly, so:

            //Sample with find latest
            realmTemplate.write { realm ->
                realm.findLatest(person)?.let {
                   name = "Jane Doe"
                }
            } ?: TODO("Handle the user not existing")

@Igro182
Copy link

Igro182 commented Jun 9, 2023

@cmelchior Thank you! does it actually mean, this is another option to resolve the issue described by @darkan23 ? any of these options is preferable from performance or any other point of view?

@cmelchior
Copy link
Contributor

Using findLatest is much better for performance, since you are only modifying the fields that changed.

@sync-by-unito sync-by-unito bot removed the Needs-Attention Reporter has responded. Review comment. label Jun 12, 2023
@Igro182
Copy link

Igro182 commented Jun 12, 2023

Using findLatest is much better for performance, since you are only modifying the fields that changed.

Thank you @cmelchior !

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 18, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants