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

Ensure clearWith lambda is deleting all the list item #6392

Merged
merged 4 commits into from
Jun 30, 2022

Conversation

bmarty
Copy link
Member

@bmarty bmarty commented Jun 28, 2022

Else we will get an infinite loop. This specific error will help to figure out what is happening.

Tested locally with a Realm schema upgrade:

internal class MigrateSessionTo032(realm: DynamicRealm) : RealmMigrator(realm, 32) {

    override fun doMigrate(realm: DynamicRealm) {
        val chunks = realm.where("ChunkEntity")
                .findAll()

        chunks.forEach { chunk ->
            chunk.getList(ChunkEntityFields.TIMELINE_EVENTS.`$`).clearWith {
                // NO OP, let the app crash!
            }
        }
    }
}

And got this crash with stacktrace in log:

java.lang.RuntimeException: Unable to create application im.vector.app.VectorApplication: java.lang.IllegalStateException: `clearWith` MUST delete all elements of the RealmList
        at android.app.ActivityThread.handleBindApplication(ActivityThread.java:6465)
        at android.app.ActivityThread.access$1300(ActivityThread.java:219)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1859)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: java.lang.IllegalStateException: `clearWith` MUST delete all elements of the RealmList
        at org.matrix.android.sdk.internal.extensions.RealmExtensionsKt.clearWith(RealmExtensions.kt:36)
        at org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032.doMigrate(MigrateSessionTo032.kt:31)
        ...

With this code:

            chunk.getList(ChunkEntityFields.TIMELINE_EVENTS.`$`).clearWith {
                it.deleteFromRealm()
            }

No crash is observed and the DB is properly migrated.

…et an infinite loop. This specific error will help to figure out what is happening.
@bmarty bmarty requested review from a team, mnaturel and fedrunov and removed request for a team June 28, 2022 10:02
first()?.let { delete.invoke(it) }
if (previousSize != size + 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be previousSize == size? (to highlight no deletion has occurred)

Copy link
Member Author

@bmarty bmarty Jun 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point, but I expect previousSize == size + 1 to be the only valid condition, so I think it's better like that.

We may have lambda which add object(s) to the list, or delete multiple objects, we live in a crazy world!

This will be possible to delete another item than the provide one in the lambda, for instance the last one, but at the end this will have the same result.

Another solution to avoid that would have to change the Extension to:

internal fun <T : RealmObject /* <- Needed contract to be able to use `isValid` */> RealmList<T>.clearWith(delete: (T) -> Unit) {
    while (!isEmpty()) {
        first()?.let {
            delete.invoke(it)
            if (it.isValid) {
                // Object has not been deleted
                error("`clearWith` MUST delete all elements of the RealmList")
            }
        }
    }
}

or even just delete it with a warning:

internal fun <T : RealmObject /* <- Needed contract to be able to use `isValid` */> RealmList<T>.clearWith(delete: (T) -> Unit) {
    while (!isEmpty()) {
        first()?.let {
            delete.invoke(it)
            if (it.isValid) {
                // Object has not been deleted
                Timber.w("`clearWith` MUST delete all elements of the RealmList")
                it.deleteFromRealm()
            }
        }
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for explaining 👍 if possible I'd prefer for us to avoid the while entirely as that seems to be the root of our problem

fun RealmList<T>.clearWith(delete: (T) -> Unit) {
  map { item ->
    { delete(item) }
  }.forEach { it.invoke() }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add that just with the signature of the method, we are not sure the provided lambda will be a deleting action, we could pass any action on a type T that just do nothing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we can use map, because the RealmList is getting empty while we call deleteFromRealm() on its items. This is not really Kotlin logic.
I will add some Kdoc on the fun.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh i understand your code with map better now, have read too fast. It may work. Let me give it a try.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ouchadam @mnaturel , I have updated the PR, and tested again dummy migration, it behaves at expected.

/**
* throw in debug, only log in production. As this method does not always throw, next statement should be a return
*/
internal fun fatalError(message: String) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bmarty bmarty force-pushed the feature/bma/safe_clearWith branch from e4376bd to a0025bc Compare June 29, 2022 13:12
lambda.invoke()
}

if (!isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use isNotEmpty() to improve readability?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

Copy link
Contributor

@ouchadam ouchadam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the update!

@sonarqubecloud
Copy link

SonarCloud Quality Gate failed.    Quality Gate failed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

0.0% 0.0% Coverage
0.0% 0.0% Duplication

@bmarty bmarty merged commit 90e851a into develop Jun 30, 2022
@bmarty bmarty deleted the feature/bma/safe_clearWith branch June 30, 2022 13:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants