-
Notifications
You must be signed in to change notification settings - Fork 624
Retrying transactions with backoff #698
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly LGTM but I'm not psyched about removeTransactionBackoffs(). I think we can make skipDelaysForTimerId() work.
Also, I think this code is portable to JS, and it would be worthwhile to change JS to run transaction code on the AsyncQueue anyway (other than the user's updateFunction) in order to make the platforms more consistent. As-is, it's not clear what is an intentional deviation and what is accidental.
If the recursive transaction update code is getting too gnarly, I think we could probably simplify things by introducing a TransactionRunner
class that encapsulates the logic of running / retrying with backoff. Right now, SyncEngine.transaction()
is getting a bit unwieldy with 5 arguments, when outside callers (i.e. FirestoreClient.transaction()
) really only need to provide asyncQueue
and updateFunction
. The rest are just there to enable the method to call itself recursively, I think.
firebase-firestore/src/main/java/com/google/firebase/firestore/util/ExponentialBackoff.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java
Outdated
Show resolved
Hide resolved
ee1e768
to
a90317c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review! Quick question: from a code health and maintainability perspective, why is it bad to introduce new test hooks, like I did with removeTransactionBackoffs
? I can see how skipDelaysForTimerId
is a more elegant solution, but when is it appropriate/preferred to add new test hooks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for reworking! I have some minor feedback, but overall I'm liking this approach better.
firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
/** Returns the result of the transaction after it has been run. */ | ||
public Task<TResult> getTask() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason for this to be a separate function? I think I'd just make runTransaction() return the Task<TResult>
.
This will also probably port better to iOS (runTransaction()
will just accept a completion callback instead of returning a Task).
firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java
Outdated
Show resolved
Hide resolved
Oh! By the way, I realized I forgot to answer your question:
Uh, this is pretty subjective. I guess my general philosophy is that test hooks are bad for a variety of reasons, including:
And so in general, it's good to have as few test hooks as possible, keep the scope as limited as possible, and make them reusable so you don't have to invent more test hooks later. In our case, we've already chosen the AsyncQueue as a useful place to allow tests to hook in to deal with time-related issues in tests (i.e. fast-forwarlding time via runDelayedOperationsEarly()). So adding new test hooks to the AsyncQueue is preferable to a new transaction-specific test hook. It's more consistent and is more likely to be reusable in the future. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few remaining nits, but this basically LGTM.
firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java
Outdated
Show resolved
Hide resolved
firebase-firestore/src/main/java/com/google/firebase/firestore/core/TransactionRunner.java
Outdated
Show resolved
Hide resolved
/test new-smoke-tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM except for typo.
@@ -27,8 +26,7 @@ | |||
import com.google.firebase.firestore.util.ExponentialBackoff; | |||
|
|||
/** | |||
* TransactionRunner encapsulates the logic needed to run and retry transactions so that the caller | |||
* does not have to manage the backoff and retry count through recursive calls. | |||
* TransactionRunner encapsulates the logic needed to run and retry transactions without backoff. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
without => with ? 😄
Good call on recommending that I try and implement Android first before merging web. This has a been a painful, but educational experience.
A couple things about Android:
TaskCompletionSource
strategy used by Android for recursion over to web.skipDelaysForTimerIds
approach of web doesn't port over to Android b/c it relies onrunDelayedTasksUntil
, which blocks until the delay is completed. This means that we must call delays from outside the AsyncQueue, or else it deadlocks. Overriding the backoff is the only other approach that works aside from calling arunDelayedTasksUntil
on a 10ms interval on a separate thread. Thoughts on this approach?