Skip to content

awaitPendingWrites initial revision #689

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
7 commits merged into from
Aug 8, 2019
Merged

awaitPendingWrites initial revision #689

7 commits merged into from
Aug 8, 2019

Conversation

ghost
Copy link

@ghost ghost commented Aug 6, 2019

Added 'waitForPendingWrites' to firestore.

@ghost ghost added the api: firestore label Aug 6, 2019
@googlebot googlebot added the cla: yes Override cla label Aug 6, 2019
@ghost ghost requested a review from schmidt-sebastian August 6, 2019 15:20
@schmidt-sebastian schmidt-sebastian self-assigned this Aug 6, 2019
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

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

I think this should work as is - I just have a bunch of comments on styling and comment. The concept is sound though! Thank you.

assertTrue(!pendingWrite.isComplete());
assertTrue(!awaitsPendingWrites2.isComplete());

waitFor(firestore.enableNetwork());
Copy link
Contributor

Choose a reason for hiding this comment

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

Technically, you don't need to wait for enableNetwork() or the pending write.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

*/
public Task<Void> awaitPendingWrites() {
this.verifyNotShutdown();
if (!remoteStore.canUseNetwork()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should run on the async queue:

  • It accesses internal state.
  • enableNetwork() and disableNetwork() run on the async queue, so for this warning to show up these operations would need to resolve before the user calls awaitPendingWrites.

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for the catch.

if (!remoteStore.canUseNetwork()) {
Logger.warn(
LOG_TAG,
"Network is disabled, the Task created to wait for all writes getting"
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can simplify this:

"The network is disabled. The task returned by awaitPendingWrites() will not complete until the network is enabled."

Copy link
Author

Choose a reason for hiding this comment

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

Done.

public Task<Void> awaitPendingWrites() {
this.verifyNotShutdown();
if (!remoteStore.canUseNetwork()) {
Logger.warn(
Copy link
Contributor

Choose a reason for hiding this comment

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

I am a little torn on whether this is a "warning" - it is certainly not a problem in the SDK itself, and technically not even for users of the SDK. It prevents unexpected behavior - so I would advocate to lower the log level here.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@@ -187,6 +187,11 @@ public MutationBatch getNextMutationBatchAfterBatchId(int batchId) {
return queue.size() > index ? queue.get(index) : null;
}

@Override
public int getLargestUnacknowledgedBatchId() {
return queue.size() == 0 ? 0 : nextBatchId - 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

Google style guide generally recommends using isEmpty(), since it always runs in O(1).

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@@ -249,6 +249,16 @@ public MutationBatch getNextMutationBatchAfterBatchId(int batchId) {
.firstValue(row -> decodeInlineMutationBatch(row.getInt(0), row.getBlob(1)));
}

@Override
public int getLargestUnacknowledgedBatchId() {
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.

Nit: In case the mutation queue is not empty, you are running a very similar query twice.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed.

if (isEmpty()) {
return 0;
}
return db.query("SELECT MAX(batch_id) FROM mutations " + "WHERE uid = ?")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: s/" + "//

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@@ -1143,4 +1143,21 @@ public void testHandlesPatchMutationWithTransformThenRemoteEvent() {
assertChanged(doc("foo/bar", 1, map("sum", 1), Document.DocumentState.LOCAL_MUTATIONS));
assertContains(doc("foo/bar", 1, map("sum", 1), Document.DocumentState.LOCAL_MUTATIONS));
}

@Test
public void testGetHighestUnacknowledgedBatchIdReturnsExpectedResult() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Maybe remove ReturnsExpectedResult, since it doesn't add too much to the descriptiveness.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

*
* <p>Both acceptance and rejection count as server acknowledgement.
*
* @return A {@link Task} which resolves when all pending writes are acknowledged by the server.
Copy link
Contributor

Choose a reason for hiding this comment

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

We likely want to update the comment a bit:

  • We should use concepts that are more widely known ("server acknowledgement for all pending writes" -> "when all writes have been sent to the backend").
  • We should mention how this behaves during a user change.
  • We should mention that this works even after client restart.

If you want to, we can tweak this comment in the chatroom.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah. I made some change, but not enough. Especially i don't understand your comment about client restart, and how this still works with a restart. Let's chat tmr.

Copy link
Contributor

Choose a reason for hiding this comment

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

One of the reasons that this is a heavily requested feature is that it offers users the ability to monitor the state of writes that were issued during an earlier app session.If a user issues a write and then closes the app before the write is committed, it is not very straightforward to figure out whether the write made it to the backend once the app is opened again. drain/awaitPendingWrites solves this problem nicely.

Just for reference: This can be done today by either issuing a dummy write and waiting for its completion (one would have to know that we commit writes sequentially) or by issuing gets for all documents that were written and checking the hasPendingWrites flags.

@schmidt-sebastian schmidt-sebastian assigned ghost and unassigned schmidt-sebastian Aug 6, 2019
Task<Void> pendingWrite = documentReference.set(data);
Task<Void> awaitsPendingWrites2 = firestore.waitForPendingWrites();

// `awaitsPendingWrites1` is complete immediately because there is no pending writes at
Copy link
Contributor

Choose a reason for hiding this comment

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

s/is complete/completes/
s/there is/there are/

Copy link
Author

Choose a reason for hiding this comment

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

Done.

if (!remoteStore.canUseNetwork()) {
Logger.debug(
TAG,
"The network is disabled. The task returned by 'awaitPendingWrites()' will not complete until the network is enabled.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Please manually wrap to 100 characters.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

}

if (pendingWritesCallbacks.containsKey(largestPendingBatchId)) {
pendingWritesCallbacks.get(largestPendingBatchId).add(userTask);
Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine to stick with the multi-map.

@@ -282,6 +282,14 @@ public LocalWriteResult writeLocally(List<Mutation> mutations) {
});
}

/**
* Returns the largest (latest) batch id in mutation queue that is pending server response.
* Returns 0 if the queue is empty.
Copy link
Contributor

Choose a reason for hiding this comment

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

You are checking for this constant in registerPendingWritesTask().

If you use -1 (or rather the existing concept of MutationBatch.UNKNOWN) it is apparent that this is a special sentinel value. -1 indicates right away that it is not a valid batch ID.

0, on the other hand, could very well be a valid batch ID. It is not obvious that we decided (or rather that Proto3 decided for us) that the first valid batch ID should be 1.

@@ -565,6 +626,7 @@ public void handleCredentialChange(User user) {
// Notify local store and emit any resulting events from swapping out the mutation queue.
ImmutableSortedMap<DocumentKey, MaybeDocument> changes = localStore.handleUserChange(user);
emitNewSnapsAndNotifyLocalStore(changes, /*remoteEvent=*/ null);
failOutstandingPendingWritesAwaitingTasks();
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally, we would want to fail these Tasks before we raise the new snapshots for the newly signed in user. That way, we handle the callbacks for the old user before invoking any callbacks that affect the new user.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

*
* <p>Both acceptance and rejection count as server acknowledgement.
*
* @return A {@link Task} which resolves when all pending writes are acknowledged by the server.
Copy link
Contributor

Choose a reason for hiding this comment

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

One of the reasons that this is a heavily requested feature is that it offers users the ability to monitor the state of writes that were issued during an earlier app session.If a user issues a write and then closes the app before the write is committed, it is not very straightforward to figure out whether the write made it to the backend once the app is opened again. drain/awaitPendingWrites solves this problem nicely.

Just for reference: This can be done today by either issuing a dummy write and waiting for its completion (one would have to know that we commit writes sequentially) or by issuing gets for all documents that were written and checking the hasPendingWrites flags.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

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

One comment about the order of callbacks and one final nudge to use MutationBatch.UNKNOWN to indicate that there is nothing to wait for.

@ghost ghost assigned schmidt-sebastian and unassigned ghost Aug 7, 2019
Copy link
Contributor

@wilhuff wilhuff left a comment

Choose a reason for hiding this comment

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

This isn't a full review just the public comments.

@@ -362,6 +362,16 @@ public WriteBatch batch() {
return shutdownInternal();
}

/**
* Wait until all pending writes existed at the time of calling are sent to the backend.
Copy link
Contributor

Choose a reason for hiding this comment

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

A bunch of comments on this:

  • Phrase comments with an implicit "this method" at the beginning
  • missing "that" between "writes existed"
  • This doesn't just wait until writes are sent, but also waits for them to complete

[This method ...] Waits until all pending writes that existed at the time of calling are have been successfully written to the server. The task will will not resolve while the client is offline.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

/**
* Wait until all pending writes existed at the time of calling are sent to the backend.
*
* @return A {@link Task} which resolves when all pending writes are sent to the backend. If there
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't @link items that are already in the signature of the method. Use @link for anything related that's not part of the signature.

Use {@code Task} to highlight that it's a type without the extra visual noise that comes from an actual link.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

* there is a Firebase user change, the returned {@code Task} will resolve to an exception. If
* the client is offline, the returned {@code Task} will not resolve.
*/
Task<Void> waitForPendingWrites() {
Copy link
Contributor

Choose a reason for hiding this comment

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

I still think we should move this comment to a doc (it could be the API proposal doc) and do some collaborative editing:

  • pending writes that existed at the time of calling could be clearer.
  • written to the server is an unusual way of phrasing the right thing.
  • I think we should move the comment on the user change to a separate paragraph and move it out of the @return section
  • Tasks on Android don't resolve, they complete.

@mikelehen usually has a ton of good feedback on these comments, and his feedback tends to be much more valuable than what I can provide.

Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

The API proposal doc would be a good place to collaborate on this.

Note that the "written to the server" language is used in the iOS documentation already which is why I suggested it: https://github.com/firebase/firebase-ios-sdk/blob/master/Firestore/Source/Public/FIRDocumentReference.h#L117. I'm definitely open to finding a better way to succinctly describe this.

Copy link
Author

Choose a reason for hiding this comment

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

Using Sebastian's version now, looks great.

@ghost
Copy link
Author

ghost commented Aug 7, 2019

Question: if there are no pending writes, will the task resolve while offline?

It will resolve in that case. That sounds right to me.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian left a comment

Choose a reason for hiding this comment

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

Looks great! Thank you.

}

private MockCredentialsProvider() {
super();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: This super() call is not needed, is it?

Copy link
Author

Choose a reason for hiding this comment

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

It's not.

listener.onValue(user);
}

private Listener<User> listener;
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Move to top of class.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

* <p>The returned Task completes immediately if there are no outstanding writes. Otherwise, the
* Task waits for all previously issued writes (including those written in a previous app
* session), but it does not wait for writes that were added after the method is called. If you
* wish to wait for additional writes, you have to call `waitForPendingWrites()` again.
Copy link
Contributor

Choose a reason for hiding this comment

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

Use {@code} for all mentions of waitForPendingWrites.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

if (!remoteStore.canUseNetwork()) {
Logger.debug(
TAG,
"The network is disabled. The task returned by 'awaitPendingWrites()' will not"
Copy link
Contributor

Choose a reason for hiding this comment

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

Super nit: We usually leave the space at the end of the line. This helps to align comments with more than two lines.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

}
}

/** Resolves tasks waiting for this batch id to get acknowledged by server, if there is any. */
Copy link
Contributor

Choose a reason for hiding this comment

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

s/is/are/

Copy link
Author

Choose a reason for hiding this comment

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

Done.

assertEquals(2, localStore.getHighestUnacknowledgedBatchId());

rejectMutation();
assertEquals(-1, localStore.getHighestUnacknowledgedBatchId());
Copy link
Contributor

Choose a reason for hiding this comment

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

s/-1/MutationBatch.UNKNOWN/

@schmidt-sebastian schmidt-sebastian assigned ghost and unassigned schmidt-sebastian Aug 8, 2019
@ghost ghost merged commit f096bbe into master Aug 8, 2019
@rlazo rlazo deleted the wuandy/AwaitPendingWrites branch September 27, 2019 14:55
@firebase firebase locked and limited conversation to collaborators Oct 8, 2019
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants