Skip to content

Fix handling of sqlite transactions #136

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

Merged
merged 5 commits into from
Nov 22, 2018
Merged

Fix handling of sqlite transactions #136

merged 5 commits into from
Nov 22, 2018

Conversation

rsgowman
Copy link
Member

  • Move as much work as possible out of the sqlite txn.

    In particular, don't do anything that could throw in the finally block, as this will mask any exception thrown within the transaction itself.

  • Use sqlite transaction listeners (rather than doing so manually)

    Using sqlite's version allows us to not worry about getting the transaction semantics correct in the face of exceptions.

In particular, don't do anything that could throw in the finally block,
as this will mask any exception thrown within the transaction itself.
Using sqlite's version allows us to not worry about getting the
transaction semantics correct in the face of exceptions.
@googlebot googlebot added the cla: yes Override cla label Nov 21, 2018
@rsgowman rsgowman requested a review from mikelehen November 21, 2018 18:07
Copy link
Contributor

@mikelehen mikelehen left a comment

Choose a reason for hiding this comment

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

Oh! I didn't know SQLiteTransactionListener existed. Very nice.

I have a slight preference to not have SQLiteLruReferenceDelegate implement SQLiteTransactionListener, but this is okay as-is too.

@@ -143,35 +143,32 @@ RemoteDocumentCache getRemoteDocumentCache() {

@Override
void runTransaction(String action, Runnable operation) {
Logger.debug(TAG, "Starting transaction: %s", action);
db.beginTransactionWithListener(referenceDelegate);
Copy link
Contributor

Choose a reason for hiding this comment

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

Since ReferenceDelegate has public onTransactionStarted() / onTransactionCommitted() methods as part of its interface contract, I think it might be cleaner to leave SQLiteLruReferenceDelegate alone, and have an anonymous implementation of SQLiteTransactionListener here that calls the existing methods on the referenceDelegate.

I know this will result in some duplication here since we start transactions in two places, but I think it keeps the contract cleaner. WDYT?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, though by moving it in here, it clutters the transaction logic, making it more difficult to audit. See 496e614

However, we can create an adapter to (a) reduce the duplication and (b) move the clutter out from the transaction methods. See 2931f24

wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

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

This probably isn't worth churning on too much, but having an "Adapter" class introduces some conceptual complexity. I'd actually just have the listener be a private class member initialized via an anonymous class:

// Create a SQLiteTransactionListener that delegates to our referenceDelegate.
private final SQLiteTransactionListener transactionListener = 
  new SQLiteTransactionListener() {
          @Override
          public void onBegin() {
            referenceDelegate.onTransactionStarted();
          }
           @Override
          public void onCommit() {
            referenceDelegate.onTransactionCommitted();
          }
           @Override
          public void onRollback() {}
        });

...

db.beginTransactionWithListener(transactionListener);

Copy link
Contributor

Choose a reason for hiding this comment

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

WDYT? I'm fine with you just submitting as-is too. :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, that makes more sense; done.

@rsgowman rsgowman assigned mikelehen and unassigned rsgowman Nov 21, 2018
@rsgowman
Copy link
Member Author

/retest

@mikelehen mikelehen assigned rsgowman and unassigned mikelehen Nov 21, 2018
... instantiated as an anon implementation of the interface.
@google-oss-bot
Copy link
Contributor

@rsgowman: The following test failed, say /retest to rerun them all:

Test name Commit Details Rerun command
smoke-tests-debug b2146b9 link /test smoke-tests-debug

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. I understand the commands that are listed here.

Copy link
Contributor

@mikelehen mikelehen left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks!

@rsgowman rsgowman merged commit 0b672e8 into master Nov 22, 2018
@rsgowman rsgowman deleted the rsgowman/txn branch November 22, 2018 14:37
@wilhuff
Copy link
Contributor

wilhuff commented Nov 27, 2018

Have we confirmed that an exception thrown in one of these callbacks gets properly reported? The documentation on the listener itself is extremely sparse and I'm worried that while this is definitely conceptually cleaner than what we've done before the lack of direct control over sequencing may not actually help uncover the root cause in issues like #115.

@rsgowman
Copy link
Member Author

"sparse" is a nicer way to put it than I'd use. :)

I looked at the source instead (incl for those older versions of android) to make sure it's doing reasonable things in the presence of exceptions.

Summary:

For the onBegin() callback, sqlite:
a) starts the transaction
b) calls the onBegin() callback in a try block.
c) catches RuntimeExceptions from onBegin, rollback's the txn, rethrows.

For the onCommit/onRollback, sqlite:
a) calls the onBegin/onRollback callback (as appropriate) in a try block.
b) catches RuntimeException, and marks the txn as a failure (if it wasn't already)
c) calls commit/rollback as appropriate
d) if an exception happened at (b), then rethrow the exception.

You can loose the original root cause from the notification callbacks if sqlite suffers a serious issue such as failing to execute the 'rollback' statement. (Perhaps a disk error or similar could cause that.)

Feel free to take a look yourself. For instance, here's the lollipop version: https://android.googlesource.com/platform/frameworks/base/+/lollipop-release/core/java/android/database/sqlite/SQLiteSession.java (just search for onBegin/onCommit)

@firebase firebase locked and limited conversation to collaborators Oct 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
cla: yes Override cla size/M
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants