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

Add support for transaction rollbacks #178

Merged
merged 2 commits into from
Nov 11, 2024

Conversation

jacek-marchwicki
Copy link
Contributor

@jacek-marchwicki jacek-marchwicki commented Nov 5, 2024

Hi, first of all, thanks for the great library! It made my life much easier! 🎉

Previously, when an exception was thrown during a transaction, the changes were still applied to the database, even if the transaction was intended to be aborted.

For example, consider this code:

database.writeTransaction("users") {
    objectStore("users").add(
        jso {
            id = "7740f7c4-f889-498a-bc6d-f88dabdcfb9a"
            username = "Username"
        },
    )

    // Abort transaction
    throw Exception("An exception is returned, transaction should be aborted")
}

Even though an exception was thrown, the object would still be added to the database.

With this change, if an exception is thrown during a transaction, all writes within that transaction are rolled back.

Here's an example of how this works:

assertFailsWith<SomeException> {
    database.writeTransaction("users") {
        objectStore("users").add(
            jso {
                id = "7740f7c4-f889-498a-bc6d-f88dabdcfb9a"
                username = "Username"
            },
        )

        // Abort transaction
        throw SomeException()
    }
}

// Because the transaction is aborted, no new values should be stored
val users = database.transaction("users") {
    objectStore("users").getAll().toList()
}

assertEquals(listOf(), users)

}

internal fun commit() {
// Check if function exists before calling it.
Copy link
Contributor Author

@jacek-marchwicki jacek-marchwicki Nov 5, 2024

Choose a reason for hiding this comment

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

note: commit was added later to browser, so it might not be always available

https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction#browser_compatibility

note: Also from the commit documentation we can read:

For an active transaction, commits the transaction. Note that this doesn't normally have to be called — a transaction will automatically commit when all outstanding requests have been satisfied and no new requests have been made. commit() can be used to start the commit process without waiting for events from outstanding requests to be dispatched.

So calling it is optional.

@cedrickcooke cedrickcooke linked an issue Nov 5, 2024 that may be closed by this pull request
@Phoenix7351 Phoenix7351 requested a review from twyatt November 6, 2024 17:56
@twyatt twyatt removed the request for review from Phoenix7351 November 6, 2024 17:58
@jacek-marchwicki jacek-marchwicki force-pushed the add-rollback-support branch 2 times, most recently from cef824a to edce0c6 Compare November 6, 2024 21:36
@@ -141,7 +163,7 @@ public open class Transaction internal constructor(
} else if (cursor != null) {
val result = trySend(wrap(cursor, channel))
when {
result.isSuccess -> if (autoContinue) cursor.`continue`()
result.isSuccess -> if (autoContinue && !finished) cursor.`continue`()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

info: I've added this check, so continue() is not called after flow was closed.

Otherwise in the following code:

val returnedValue = database.transaction("customers") {
            objectStore("customers")
                .index("name")
                .openCursor(autoContinue = true)
                .first()
        }

Cursor might try to return new value after first() and after transaction is finished. This will cause TransactionInactiveError.

@jacek-marchwicki
Copy link
Contributor Author

@Phoenix7351, @cedrickcooke , @twyatt, sorry for the initial fail with the PR. Now it's fixed.

I don't know how to fix the following check:
Screenshot 2024-11-06 at 23 28 38

I guess I should add a label to PR, but this is not allowed by the external contributors.

@twyatt
Copy link
Member

twyatt commented Nov 6, 2024

I guess I should add a label to PR, but this is not allowed by the external contributors.

Ya, we'll add a semver label when we review it (it helps us later decide what version to release under).

@twyatt
Copy link
Member

twyatt commented Nov 6, 2024

Also, thanks for being so thorough with this PR. Great work!

@cedrickcooke cedrickcooke added the major Changes that should bump the MAJOR version number label Nov 8, 2024
Copy link
Collaborator

@cedrickcooke cedrickcooke left a comment

Choose a reason for hiding this comment

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

Awesome work on this, thank you


internal fun commit() {
// Check if function exists before calling it.
if (transaction.unsafeCast<Json>()["commit"] != undefined) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure what the standard approach is for this (as JavaScript is outside my wheelhouse) but I've often seen these kind of checks written as:

Suggested change
if (transaction.unsafeCast<Json>()["commit"] != undefined) {
if (typeof transaction.commit === 'function') {

..if the above approach is functionally the same, I think I'd prefer it from a readability perspective. 🤷

Let me know your thoughts / rationale behind the casting to Json.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! The prosed solution looks much better. I'll check if it works as expected, then prepare update.

Copy link
Contributor Author

@jacek-marchwicki jacek-marchwicki Nov 8, 2024

Choose a reason for hiding this comment

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

@twyatt I've tried with jsTypeOf(transaction::commit) === "function" but this returns true, no matter if the commit function is implement in the web browser or if it's not. Even tho this looks the best, it doesn't work :/

I can use the following code:

val transaction = transaction
if (js("typeof transaction.commit === 'function'"))

but this code is as ugly as the code with unsafeCast ;)

I don't have much preference, so I'm open for your opinion. Maybe you see another solution for this problem?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is also:

jsTypeOf(transaction.unsafeCast<Json>()["commit"]) === "function"

but this is similar to

transaction.unsafeCast<Json>()["commit"] != undefined

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yet another solution that I found, but still a little bit hacky:

private inline fun <T> functionExists(obj: T, function: T.() -> KFunction<*>): Boolean = 
    jsTypeOf(obj.unsafeCast<Json>()[obj.function().name]) === "function"

Then:

if (functionExists(transaction) { ::commit }) {
    transaction.commit()
}

Copy link
Member

Choose a reason for hiding this comment

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

I've tried with jsTypeOf(transaction::commit) === "function" but this returns true, no matter if the commit function is implement in the web browser or if it's not. Even tho this looks the best, it doesn't work :/

Oh bummer. Thanks for giving it a try. Let me experiment a little over the weekend and get back to you.

Copy link
Member

@twyatt twyatt Nov 10, 2024

Choose a reason for hiding this comment

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

@jacek-marchwicki how are you testing when commit isn't supported? It looks like all modern browsers support it. Are you downloading an older version of one of the browsers?

Screenshot 2024-11-09 at 11 51 15 PM

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure how to test this (with commit specifically), but if you have a moment, maybe one last thing to try:

Note

This is the way that the coroutines library is checking for feature support.

if (jsTypeOf(transaction.asDynamic().commit) != "undefined") {
    transaction.commit()
}

or...

if (jsTypeOf(transaction.asDynamic().commit) === "function") {
    transaction.commit()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm testing just by not using "commit" name, but temporarily renaming the method to something else that doesn't exist. I.e. "commit2".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Btw, I decided to do the check for "commit" method, because even tho it is supported in the newest web browsers, it was introduced later than other features:

Screenshot 2024-11-10 at 22 59 33

@twyatt twyatt self-requested a review November 9, 2024 02:51
@twyatt twyatt merged commit e29b9cc into JuulLabs:main Nov 11, 2024
2 checks passed
@twyatt
Copy link
Member

twyatt commented Nov 11, 2024

@jacek-marchwicki thanks for the contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
major Changes that should bump the MAJOR version number
Projects
None yet
Development

Successfully merging this pull request may close these issues.

writeTransaction should rollback on exception
3 participants