From 2be46164a7f1bc484d4132b632a14d69663054c7 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 15 May 2024 10:36:29 -0400 Subject: [PATCH 01/30] Clear Cache WIP --- .../firestore/remote/RemoteStoreTest.java | 3 ++ .../core/MemoryComponentProvider.java | 5 +++ .../firebase/firestore/core/SyncEngine.java | 8 ++++ .../firestore/local/DocumentOverlayCache.java | 5 +++ .../firestore/local/IndexManager.java | 5 +++ .../firebase/firestore/local/LocalStore.java | 14 +++++++ .../firestore/local/MemoryIndexManager.java | 14 +++++++ .../local/MemoryRemoteDocumentCache.java | 7 ++++ .../firestore/local/MutationQueue.java | 5 +++ .../firestore/local/RemoteDocumentCache.java | 3 ++ .../local/SQLiteDocumentOverlayCache.java | 5 +++ .../firestore/local/SQLiteIndexManager.java | 34 +++++++++++++++++ .../firestore/local/SQLiteMutationQueue.java | 9 +++++ .../local/SQLiteRemoteDocumentCache.java | 6 +++ .../firestore/remote/RemoteSerializer.java | 6 ++- .../firestore/remote/RemoteStore.java | 37 +++++++++++++++++-- .../remote/WatchChangeAggregator.java | 26 ++++++++++++- 17 files changed, 185 insertions(+), 7 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java index bf2b97219f6..ee8620d5aa7 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java @@ -68,6 +68,9 @@ public void handleOnlineStateChange(OnlineState onlineState) { networkChangeSemaphore.release(); } + @Override + public void handleClearCache() {} + @Override public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { return null; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java index fc8180b6937..c7d3ce8952d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java @@ -145,6 +145,11 @@ public void handleOnlineStateChange(OnlineState onlineState) { getSyncEngine().handleOnlineStateChange(onlineState); } + @Override + public void handleClearCache() { + getSyncEngine().handleClearCache(); + } + @Override public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { return getSyncEngine().getRemoteKeysForTarget(targetId); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index e113cc06c8d..793cf9df035 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -515,6 +515,14 @@ public void handleRejectedWrite(int batchId, Status status) { emitNewSnapsAndNotifyLocalStore(changes, /*remoteEvent=*/ null); } + @Override + public void handleClearCache() { + assertCallback("handleClearCache"); + + localStore.clearCacheData(); + remoteStore.clearAllTargets(); + } + /** * Takes a snapshot of current mutation queue, and register a user task which will resolve when * all those mutations are either accepted or rejected by the server. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java index 6d7f999587c..f3d1b19cd9f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java @@ -77,4 +77,9 @@ public interface DocumentOverlayCache { * @return Mapping of each document key in the collection group to its overlay. */ Map getOverlays(String collectionGroup, int sinceBatchId, int count); + + /** + * Clear overlays. This should only be done when mutation queue is cleared. + */ + void clear(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/IndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/IndexManager.java index 4751ae6e208..f66598cfcd2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/IndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/IndexManager.java @@ -32,6 +32,7 @@ * Collection Group queries. */ public interface IndexManager { + /** Represents the index state as it relates to a particular target. */ enum IndexType { /** Indicates that no index could be found for serving the target. */ @@ -51,6 +52,8 @@ enum IndexType { /** Initializes the IndexManager. */ void start(); + void clearParents(); + /** * Creates an index entry mapping the collectionId (last segment of the path) to the parent path * (either the containing document location or the empty path for root-level collections). Index @@ -128,4 +131,6 @@ enum IndexType { /** Updates the index entries for the provided documents. */ void updateIndexEntries(ImmutableSortedMap documents); + + void clearIndexData(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java index 89282977822..4774d7c3efe 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java @@ -230,6 +230,20 @@ public ImmutableSortedMap handleUserChange(User user) { return localDocuments.getDocuments(changedKeys); } + public void clearCacheData() { + mutationQueue.clear(); + + // Clearing the mutation queue requires also clearing document overlays. + documentOverlayCache.clear(); + + remoteDocuments.clear(); + + // Clearing parents is only possible when both mutations and document cache are cleared. + indexManager.clearParents(); + + indexManager.clearIndexData(); + } + /** Accepts locally generated Mutations and commits them to storage. */ public LocalDocumentsResult writeLocally(List mutations) { Timestamp localWriteTime = Timestamp.now(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryIndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryIndexManager.java index a55734b3859..e1c0a815d32 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryIndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryIndexManager.java @@ -40,6 +40,11 @@ public MemoryIndexManager() {} @Override public void start() {} + @Override + public void clearParents() { + collectionParentsIndex.clear(); + } + @Override public void addToCollectionParentIndex(ResourcePath collectionPath) { collectionParentsIndex.add(collectionPath); @@ -119,6 +124,11 @@ public void updateIndexEntries(ImmutableSortedMap documen // Field indices are not supported with memory persistence. } + @Override + public void clearIndexData() { + // Field indices are not supported with memory persistence. + } + /** * Internal implementation of the collection-parent index. Also used for in-memory caching by * SQLiteIndexManager and initial index population in SQLiteSchema. @@ -144,5 +154,9 @@ List getEntries(String collectionId) { HashSet existingParents = index.get(collectionId); return existingParents != null ? new ArrayList<>(existingParents) : Collections.emptyList(); } + + void clear() { + index.clear(); + } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java index 3b4fa1048b2..26d96e7658d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java @@ -74,6 +74,13 @@ public void removeAll(Collection keys) { indexManager.updateIndexEntries(deletedDocs); } + @Override + public void clear() { + hardAssert(indexManager != null, "setIndexManager() not called"); + docs = emptyDocumentMap(); + indexManager.clearIndexData(); + } + @Override public MutableDocument get(DocumentKey key) { Document doc = docs.get(key); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MutationQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MutationQueue.java index 02a7f7ea359..89e14f51eb4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MutationQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MutationQueue.java @@ -133,4 +133,9 @@ List getAllMutationBatchesAffectingDocumentKeys( /** Performs a consistency check, examining the mutation queue for any leaks, if possible. */ void performConsistencyCheck(); + + /** + * Removes all mutations. + */ + void clear(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java index 8ff90864342..3382ba59668 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java @@ -51,6 +51,9 @@ interface RemoteDocumentCache { /** Removes the cached entries for the given keys (no-op if no entry exists). */ void removeAll(Collection keys); + /** Removes all cached entries. */ + void clear(); + /** * Looks up an entry in the cache. * diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java index 84628edd4a2..370ee15ad94 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java @@ -199,6 +199,11 @@ public Map getOverlays( return result; } + @Override + public void clear() { + db.execute("DELETE FROM document_overlays"); + } + private void processOverlaysInBackground( BackgroundQueue backgroundQueue, Map results, Cursor row) { byte[] rawMutation = row.getBlob(0); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java index 4a060d2fe02..e124733e648 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java @@ -169,6 +169,12 @@ public void start() { started = true; } + @Override + public void clearParents() { + db.execute("DELETE FROM collection_parents"); + collectionParentsCache.clear(); + } + @Override public void addToCollectionParentIndex(ResourcePath collectionPath) { hardAssert(started, "IndexManager not started"); @@ -241,6 +247,8 @@ public void deleteAllFieldIndexes() { nextIndexToUpdate.clear(); memoizedIndexes.clear(); + memoizedMaxIndexId = -1; + memoizedMaxSequenceNumber = -1; } @Override @@ -282,6 +290,32 @@ public void updateIndexEntries(ImmutableSortedMap documen } } + @Override + public void clearIndexData() { + db.execute("DELETE FROM index_entries"); + db.execute("DELETE FROM index_state"); + nextIndexToUpdate.clear(); + memoizedIndexes.clear(); + memoizedMaxIndexId = -1; + memoizedMaxSequenceNumber = -1; + + db.query("SELECT index_id, collection_group, index_proto FROM index_configuration") + .forEach( + row -> { + try { + int indexId = row.getInt(0); + String collectionGroup = row.getString(1); + List segments = + serializer.decodeFieldIndexSegments(Index.parseFrom(row.getBlob(2))); + + // Store the index and update `memoizedMaxIndexId` and `memoizedMaxSequenceNumber`. + memoizeIndex(FieldIndex.create(indexId, collectionGroup, segments, FieldIndex.INITIAL_STATE)); + } catch (InvalidProtocolBufferException e) { + throw fail("Failed to decode index: " + e); + } + }); + } + /** * Updates the index entries for the provided document by deleting entries that are no longer * referenced in {@code newEntries} and adding all newly added entries. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java index dd70a58d02b..4a763eaf988 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java @@ -437,6 +437,15 @@ public void performConsistencyCheck() { danglingMutationReferences); } + @Override + public void clear() { + db.execute("DELETE FROM mutations"); + db.execute("DELETE FROM document_mutations"); + db.execute("DELETE FROM mutation_queues"); + nextBatchId = 1; + lastStreamToken = WriteStream.EMPTY_STREAM_TOKEN; + } + /** * Decodes mutation batch bytes obtained via substring. If the blob is smaller than * BLOB_MAX_INLINE_LENGTH, executes additional queries to load the rest of the blob. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java index b26f9601a81..4ba6be42057 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java @@ -111,6 +111,12 @@ public void removeAll(Collection keys) { indexManager.updateIndexEntries(deletedDocs); } + @Override + public void clear() { + db.execute("DELETE FROM remote_documents"); + indexManager.clearIndexData(); + } + @Override public MutableDocument get(DocumentKey documentKey) { return getAll(Collections.singletonList(documentKey)).get(documentKey); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java index bb7d291c165..cb980eb39af 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java @@ -75,6 +75,7 @@ import com.google.firestore.v1.Target; import com.google.firestore.v1.Target.DocumentsTarget; import com.google.firestore.v1.Target.QueryTarget; +import com.google.firestore.v1.TargetChange; import com.google.firestore.v1.Value; import com.google.protobuf.Int32Value; import io.grpc.Status; @@ -1016,10 +1017,11 @@ public SnapshotVersion decodeVersionFromListenResponse(ListenResponse watchChang if (watchChange.getResponseTypeCase() != ResponseTypeCase.TARGET_CHANGE) { return SnapshotVersion.NONE; } - if (watchChange.getTargetChange().getTargetIdsCount() != 0) { + TargetChange targetChange = watchChange.getTargetChange(); + if (targetChange.getTargetIdsCount() != 0) { return SnapshotVersion.NONE; } - return decodeVersion(watchChange.getTargetChange().getReadTime()); + return decodeVersion(targetChange.getReadTime()); } private Status fromStatus(com.google.rpc.Status status) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index eda25fa2792..f1a6b312471 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -14,6 +14,7 @@ package com.google.firebase.firestore.remote; +import static com.google.firebase.firestore.util.Assert.fail; import static com.google.firebase.firestore.util.Assert.hardAssert; import androidx.annotation.Nullable; @@ -47,6 +48,7 @@ import com.google.protobuf.ByteString; import io.grpc.Status; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.List; @@ -104,6 +106,11 @@ public interface RemoteStoreCallback { */ void handleOnlineStateChange(OnlineState onlineState); + /** + * Synchronization event that requires cache be cleared. + */ + void handleClearCache(); + /** * Returns the set of remote document keys for the given target ID. This list includes the * documents that were assigned to the target when we received the last snapshot. @@ -468,8 +475,7 @@ private void handleWatchChange(SnapshotVersion snapshotVersion, WatchChange watc watchChange instanceof WatchTargetChange ? (WatchTargetChange) watchChange : null; if (watchTargetChange != null - && watchTargetChange.getChangeType().equals(WatchTargetChangeType.Removed) - && watchTargetChange.getCause() != null) { + && watchTargetChange.getChangeType().equals(WatchTargetChangeType.Removed)) { // There was an error on a target, don't wait for a consistent snapshot to raise events processTargetError(watchTargetChange); } else { @@ -582,15 +588,38 @@ private void raiseWatchSnapshot(SnapshotVersion snapshotVersion) { } private void processTargetError(WatchTargetChange targetChange) { - hardAssert(targetChange.getCause() != null, "Processing target error without a cause"); + Status cause = targetChange.getCause(); + hardAssert(cause != null, "Processing target error without a cause"); for (Integer targetId : targetChange.getTargetIds()) { // Ignore targets that have been removed already. if (listenTargets.containsKey(targetId)) { listenTargets.remove(targetId); watchChangeAggregator.removeTarget(targetId); - remoteStoreCallback.handleRejectedListen(targetId, targetChange.getCause()); + remoteStoreCallback.handleRejectedListen(targetId, cause); + } + } + } + + public void clearAllTargets() { + List targetIds = new ArrayList<>(); + for (Entry entry : listenTargets.entrySet()) { + switch (entry.getValue().getPurpose()) { + case LIMBO_RESOLUTION: + // Limbo resolutions are cleared when original listen is cleared. + continue; + case LISTEN: + case EXISTENCE_FILTER_MISMATCH: + case EXISTENCE_FILTER_MISMATCH_BLOOM: + targetIds.add(entry.getKey()); } } + WatchTargetChange targetChange = new WatchTargetChange( + WatchTargetChangeType.Removed, + targetIds, + WatchStream.EMPTY_RESUME_TOKEN, + Status.ABORTED + ); + processTargetError(targetChange); } // Write Stream diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java index d5064fa3d8e..9e82c6e65e6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java @@ -109,7 +109,7 @@ public void handleDocumentChange(DocumentChange documentChange) { } for (int targetId : documentChange.getRemovedTargetIds()) { - removeDocumentFromTarget(targetId, documentKey, documentChange.getNewDocument()); + removeDocumentFromTarget(targetId, documentKey, document); } } @@ -529,6 +529,30 @@ private void resetTarget(int targetId) { } } + private void resetAllTargets() { + for (Map.Entry entry : targetStates.entrySet()) { + Integer targetId = entry.getKey(); + TargetState targetState = new TargetState(); + entry.setValue(targetState); + + if (!isActiveTarget(targetId)) { + continue; + } + + ImmutableSortedSet existingKeys = + targetMetadataProvider.getRemoteKeysForTarget(targetId); + for (DocumentKey key : existingKeys) { + if (targetContainsDocument(targetId, key)) { + targetState.addDocumentChange(key, DocumentViewChange.Type.REMOVED); + } else { + // The document may have entered and left the target before we raised a snapshot, so we can + // just ignore the change. + targetState.removeDocumentChange(key); + } + } + } + } + /** Returns whether the LocalStore considers the document to be part of the specified target. */ private boolean targetContainsDocument(int targetId, DocumentKey key) { ImmutableSortedSet existingKeys = From cbf5987a6da718a1be4ab57018d4a9034f284354 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 15 May 2024 21:33:46 -0400 Subject: [PATCH 02/30] Clear Cache WIP --- .../firebase/firestore/core/SyncEngine.java | 11 ++++++++- .../firebase/firestore/local/LocalStore.java | 1 + .../firestore/local/ReferenceSet.java | 8 ++++--- .../firestore/remote/RemoteStore.java | 19 ++++++++------- .../remote/WatchChangeAggregator.java | 24 ------------------- 5 files changed, 27 insertions(+), 36 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index 793cf9df035..5fc5cf82ebb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -518,9 +518,18 @@ public void handleRejectedWrite(int batchId, Status status) { @Override public void handleClearCache() { assertCallback("handleClearCache"); + boolean canUseNetwork = remoteStore.canUseNetwork(); + if (canUseNetwork) { + remoteStore.disableNetwork(); + } + + remoteStore.abortAllTargets(); localStore.clearCacheData(); - remoteStore.clearAllTargets(); + + if (canUseNetwork) { + remoteStore.enableNetwork(); + } } /** diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java index 4774d7c3efe..fe287c3a19b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java @@ -241,6 +241,7 @@ public void clearCacheData() { // Clearing parents is only possible when both mutations and document cache are cleared. indexManager.clearParents(); + // Note that index configuration is preserved. indexManager.clearIndexData(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/ReferenceSet.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/ReferenceSet.java index b46174a201e..83cf40f19b1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/ReferenceSet.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/ReferenceSet.java @@ -42,6 +42,10 @@ public class ReferenceSet { private ImmutableSortedSet referencesByTarget; public ReferenceSet() { + init(); + } + + private void init() { referencesByKey = new ImmutableSortedSet<>(emptyList(), DocumentReference.BY_KEY); referencesByTarget = new ImmutableSortedSet<>(emptyList(), DocumentReference.BY_TARGET); } @@ -102,9 +106,7 @@ public ImmutableSortedSet removeReferencesForId(int targetId) { /** Clears all references for all IDs. */ public void removeAllReferences() { - for (DocumentReference reference : referencesByKey) { - removeReference(reference); - } + init(); } private void removeReference(DocumentReference ref) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index f1a6b312471..6925b9834dc 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -139,6 +139,7 @@ public interface RemoteStoreCallback { private final OnlineStateTracker onlineStateTracker; private boolean networkEnabled = false; + private final WatchStream watchStream; private final WriteStream writeStream; @Nullable private WatchChangeAggregator watchChangeAggregator; @@ -600,7 +601,12 @@ private void processTargetError(WatchTargetChange targetChange) { } } - public void clearAllTargets() { + public void abortAllTargets() { + // To prevent Limbo Resolution from sending new listen request during abort of all targets, the + // network must be disabled. Not doing so will cause `handleRejectedListen` to start watch + // stream. + hardAssert(!canUseNetwork(), "Network should be disabled during abort of all targets."); + List targetIds = new ArrayList<>(); for (Entry entry : listenTargets.entrySet()) { switch (entry.getValue().getPurpose()) { @@ -613,13 +619,10 @@ public void clearAllTargets() { targetIds.add(entry.getKey()); } } - WatchTargetChange targetChange = new WatchTargetChange( - WatchTargetChangeType.Removed, - targetIds, - WatchStream.EMPTY_RESUME_TOKEN, - Status.ABORTED - ); - processTargetError(targetChange); + for (Integer targetId : targetIds) { + listenTargets.remove(targetId); + remoteStoreCallback.handleRejectedListen(targetId, Status.ABORTED); + } } // Write Stream diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java index 9e82c6e65e6..d20a5be1eeb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java @@ -529,30 +529,6 @@ private void resetTarget(int targetId) { } } - private void resetAllTargets() { - for (Map.Entry entry : targetStates.entrySet()) { - Integer targetId = entry.getKey(); - TargetState targetState = new TargetState(); - entry.setValue(targetState); - - if (!isActiveTarget(targetId)) { - continue; - } - - ImmutableSortedSet existingKeys = - targetMetadataProvider.getRemoteKeysForTarget(targetId); - for (DocumentKey key : existingKeys) { - if (targetContainsDocument(targetId, key)) { - targetState.addDocumentChange(key, DocumentViewChange.Type.REMOVED); - } else { - // The document may have entered and left the target before we raised a snapshot, so we can - // just ignore the change. - targetState.removeDocumentChange(key); - } - } - } - } - /** Returns whether the LocalStore considers the document to be part of the specified target. */ private boolean targetContainsDocument(int targetId, DocumentKey key) { ImmutableSortedSet existingKeys = From 9f8cffb36fe2e512aec41a228f3150f1e2e44e81 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 15 May 2024 22:11:10 -0400 Subject: [PATCH 03/30] Clear Cache WIP --- .../com/google/firebase/firestore/local/BundleCache.java | 2 ++ .../com/google/firebase/firestore/local/LocalStore.java | 1 + .../google/firebase/firestore/local/MemoryBundleCache.java | 6 ++++++ .../google/firebase/firestore/local/SQLiteBundleCache.java | 6 ++++++ 4 files changed, 15 insertions(+) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java index 872ada71760..642077264f8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java @@ -39,4 +39,6 @@ public interface BundleCache { /** Saves a NamedQuery from a bundle, using its name as the persistent key. */ void saveNamedQuery(NamedQuery query); + + void clear(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java index fe287c3a19b..7924e3eaeb2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java @@ -237,6 +237,7 @@ public void clearCacheData() { documentOverlayCache.clear(); remoteDocuments.clear(); + bundleCache.clear(); // Clearing parents is only possible when both mutations and document cache are cleared. indexManager.clearParents(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java index 147af879453..f6203bcca37 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java @@ -46,4 +46,10 @@ public NamedQuery getNamedQuery(String queryName) { public void saveNamedQuery(NamedQuery query) { namedQueries.put(query.getName(), query); } + + @Override + public void clear() { + bundles.clear(); + namedQueries.clear(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java index 444f03a03e4..8c385aa309f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java @@ -104,4 +104,10 @@ public void saveNamedQuery(NamedQuery query) { query.getReadTime().getTimestamp().getNanoseconds(), bundledQuery.toByteArray()); } + + @Override + public void clear() { + db.execute("DELETE FROM bundles"); + db.execute("DELETE FROM named_queries"); + } } From 40a182bdeb0cdcc4012f4bc4ded0fecef1eb367f Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 16 May 2024 14:06:16 -0400 Subject: [PATCH 04/30] Clear Cache WIP --- .../firestore/local/MemoryDocumentOverlayCache.java | 6 ++++++ .../firestore/local/MemoryMutationQueue.java | 12 +++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java index 0731591c5d2..997d2a14b8d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java @@ -146,4 +146,10 @@ public Map getOverlays( return result; } + + @Override + public void clear() { + overlays.clear(); + overlayByBatchId.clear(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java index 7a238dd20c1..e7eee857f40 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java @@ -18,6 +18,7 @@ import static com.google.firebase.firestore.util.Preconditions.checkNotNull; import static java.util.Collections.emptyList; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.Timestamp; import com.google.firebase.database.collection.ImmutableSortedSet; @@ -75,11 +76,15 @@ final class MemoryMutationQueue implements MutationQueue { MemoryMutationQueue(MemoryPersistence persistence, User user) { this.persistence = persistence; queue = new ArrayList<>(); + indexManager = persistence.getIndexManager(user); + init(); + } + private void init() { + queue.clear(); batchesByDocumentKey = new ImmutableSortedSet<>(emptyList(), DocumentReference.BY_KEY); nextBatchId = 1; lastStreamToken = WriteStream.EMPTY_STREAM_TOKEN; - indexManager = persistence.getIndexManager(user); } // MutationQueue implementation @@ -320,6 +325,11 @@ public void performConsistencyCheck() { } } + @Override + public void clear() { + init(); + } + boolean containsKey(DocumentKey key) { // Create a reference with a zero ID as the start position to find any document reference with // this key. From 28aaaad7a13468c9e222b64c3e99476c17f2c520 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 16 May 2024 21:28:14 -0400 Subject: [PATCH 05/30] Clear Cache WIP Test --- .../firestore/local/CountingQueryEngine.java | 14 +++- .../firestore/local/LocalStoreTestCase.java | 68 +++++++++++++++++++ .../firebase/firestore/spec/SpecTestCase.java | 5 ++ 3 files changed, 86 insertions(+), 1 deletion(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java index d93231ad215..7b2d5ca5983 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java @@ -159,6 +159,11 @@ public void removeAll(Collection keys) { subject.removeAll(keys); } + @Override + public void clear() { + subject.clear(); + } + @Override public MutableDocument get(DocumentKey documentKey) { MutableDocument result = subject.get(documentKey); @@ -256,8 +261,15 @@ public Map getOverlays( return result; } + @Override + public void clear() { + subject.clear(); + } + private OverlayType getOverlayType(Overlay overlay) { - if (overlay.getMutation() instanceof SetMutation) { + if (overlay == null) { + return null; + } else if (overlay.getMutation() instanceof SetMutation) { return OverlayType.Set; } else if (overlay.getMutation() instanceof PatchMutation) { return OverlayType.Patch; diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java index 2b2fef7159f..f7716430a01 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java @@ -958,6 +958,25 @@ public void testCanExecuteDocumentQueries() { .containsExactly(doc("foo/bar", 0, map("foo", "bar")).setHasLocalMutations()); } + @Test + public void testCanExecuteDocumentQueriesAfterClearingCacheData() { + localStore.writeLocally( + asList( + setMutation("foo/bar", map("foo", "bar")), + setMutation("foo/baz", map("foo", "baz")), + setMutation("foo/bar/Foo/Bar", map("Foo", "Bar")))); + Query query = Query.atPath(ResourcePath.fromSegments(asList("foo", "bar"))); + QueryResult resultBefore = localStore.executeQuery(query, /* usePreviousResults= */ true); + + localStore.clearCacheData(); + + assertThat(values(resultBefore.getDocuments())) + .containsExactly(doc("foo/bar", 0, map("foo", "bar")).setHasLocalMutations()); + + QueryResult resultAfter = localStore.executeQuery(query, /* usePreviousResults= */ true); + assertThat(values(resultAfter.getDocuments())).isEmpty(); + } + @Test public void testCanExecuteCollectionQueries() { localStore.writeLocally( @@ -975,6 +994,30 @@ public void testCanExecuteCollectionQueries() { doc("foo/baz", 0, map("foo", "baz")).setHasLocalMutations()); } + @Test + public void testCanExecuteCollectionQueriesAfterClearingCacheData() { + localStore.writeLocally( + asList( + setMutation("fo/bar", map("fo", "bar")), + setMutation("foo/bar", map("foo", "bar")), + setMutation("foo/baz", map("foo", "baz")), + setMutation("foo/bar/Foo/Bar", map("Foo", "Bar")), + setMutation("fooo/blah", map("fooo", "blah")))); + Query query = query("foo"); + QueryResult resultBefore = localStore.executeQuery(query, /* usePreviousResults= */ true); + + localStore.clearCacheData(); + + assertThat(values(resultBefore.getDocuments())) + .containsExactly( + doc("foo/bar", 0, map("foo", "bar")).setHasLocalMutations(), + doc("foo/baz", 0, map("foo", "baz")).setHasLocalMutations()); + + QueryResult resultAfter = localStore.executeQuery(query, /* usePreviousResults= */ true); + + assertThat(values(resultAfter.getDocuments())).isEmpty(); + } + @Test public void testCanExecuteMixedCollectionQueries() { Query query = query("foo"); @@ -993,6 +1036,31 @@ public void testCanExecuteMixedCollectionQueries() { doc("foo/bonk", 0, map("a", "b")).setHasLocalMutations()); } + @Test + public void testCanExecuteMixedCollectionQueriesAfterClearingCacheData() { + Query query = query("foo"); + allocateQuery(query); + assertTargetId(2); + + applyRemoteEvent(updateRemoteEvent(doc("foo/baz", 10, map("a", "b")), asList(2), emptyList())); + applyRemoteEvent(updateRemoteEvent(doc("foo/bar", 20, map("a", "b")), asList(2), emptyList())); + writeMutation(setMutation("foo/bonk", map("a", "b"))); + + QueryResult resultBefore = localStore.executeQuery(query, /* usePreviousResults= */ true); + + localStore.clearCacheData(); + + assertThat(values(resultBefore.getDocuments())) + .containsExactly( + doc("foo/bar", 20, map("a", "b")), + doc("foo/baz", 10, map("a", "b")), + doc("foo/bonk", 0, map("a", "b")).setHasLocalMutations()); + + QueryResult resultAfter = localStore.executeQuery(query, /* usePreviousResults= */ true); + + assertThat(values(resultAfter.getDocuments())).isEmpty(); + } + @Test public void testReadsAllDocumentsForInitialCollectionQueries() { Query query = query("foo"); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index a2a57e9cc48..997e496d668 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -344,6 +344,11 @@ public void handleOnlineStateChange(OnlineState onlineState) { syncEngine.handleOnlineStateChange(onlineState); } + @Override + public void handleClearCache() { + syncEngine.handleClearCache(); + } + private List>> getCurrentOutstandingWrites() { List>> writes = outstandingWrites.get(currentUser); if (writes == null) { From 25809f9d16b042502e84cc859e82a4fb7b08e0fb Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 16 May 2024 21:46:40 -0400 Subject: [PATCH 06/30] Fix --- .../com/google/firebase/firestore/remote/RemoteStore.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index 6925b9834dc..7984056db82 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -139,7 +139,6 @@ public interface RemoteStoreCallback { private final OnlineStateTracker onlineStateTracker; private boolean networkEnabled = false; - private final WatchStream watchStream; private final WriteStream writeStream; @Nullable private WatchChangeAggregator watchChangeAggregator; @@ -476,7 +475,8 @@ private void handleWatchChange(SnapshotVersion snapshotVersion, WatchChange watc watchChange instanceof WatchTargetChange ? (WatchTargetChange) watchChange : null; if (watchTargetChange != null - && watchTargetChange.getChangeType().equals(WatchTargetChangeType.Removed)) { + && watchTargetChange.getChangeType().equals(WatchTargetChangeType.Removed) + && watchTargetChange.getCause() != null) { // There was an error on a target, don't wait for a consistent snapshot to raise events processTargetError(watchTargetChange); } else { From f982bbf284c14b7074823eee63a3f81e5a07c7c2 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 22 May 2024 10:52:52 -0400 Subject: [PATCH 07/30] WIP --- .../firebase/firestore/remote/StreamTest.java | 12 ++-- .../firebase/firestore/local/LocalStore.java | 8 +++ .../firestore/remote/RemoteStore.java | 17 ++++-- .../firestore/remote/WatchStream.java | 58 +++++++++++++++++-- .../firestore/remote/WriteStream.java | 19 ++++-- .../proto/google/firestore/v1/firestore.proto | 41 +++++++++++++ .../firestore/remote/MockDatastore.java | 6 +- 7 files changed, 141 insertions(+), 20 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java index c1fd0728933..a1dc9d9b771 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java @@ -37,6 +37,8 @@ import com.google.firebase.firestore.testutil.IntegrationTestUtil; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.AsyncQueue.TimerId; +import com.google.protobuf.ByteString; + import io.grpc.Status; import java.util.ArrayList; import java.util.Collections; @@ -95,7 +97,7 @@ public void onClose(Status status) { } @Override - public void onHandshakeComplete() { + public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { handshakeSemaphore.release(); } @@ -127,7 +129,7 @@ private void waitForWriteStreamOpen( AsyncQueue testQueue, WriteStream writeStream, StreamStatusCallback callback) { testQueue.enqueueAndForget(writeStream::start); waitFor(callback.openSemaphore); - testQueue.enqueueAndForget(writeStream::writeHandshake); + testQueue.enqueueAndForget(() -> writeStream.sendHandshake(ByteString.EMPTY)); waitFor(callback.handshakeSemaphore); } @@ -170,9 +172,9 @@ public void testWriteStreamStopAfterHandshake() throws Exception { StreamStatusCallback streamCallback = new StreamStatusCallback() { @Override - public void onHandshakeComplete() { + public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { assertThat(writeStreamWrapper[0].getLastStreamToken()).isNotEmpty(); - super.onHandshakeComplete(); + super.onHandshakeComplete(dbToken, clearCache); } @Override @@ -192,7 +194,7 @@ public void onWriteResponse( () -> assertThrows(Throwable.class, () -> writeStream.writeMutations(mutations))); // Handshake should always be called - testQueue.enqueueAndForget(writeStream::writeHandshake); + testQueue.enqueueAndForget(() -> writeStream.sendHandshake(ByteString.EMPTY)); waitFor(streamCallback.handshakeSemaphore); // Now writes should succeed diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java index 89282977822..f29b7b167e2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java @@ -396,6 +396,14 @@ public SnapshotVersion getLastRemoteSnapshotVersion() { return targetCache.getLastRemoteSnapshotVersion(); } + public ByteString getDbToken() { + return globalsCache.getDbToken(); + } + + public void setDbToken(ByteString dbToken) { + globalsCache.setDbToken(dbToken); + } + /** * Updates the "ground-state" (remote) documents. We assume that the remote event reflects any * write batches that have been acknowledged or rejected (specifically, we do not re-apply local diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index eda25fa2792..03016827d80 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -175,7 +175,13 @@ public RemoteStore( new WatchStream.Callback() { @Override public void onOpen() { - handleWatchStreamOpen(); + watchStream.sendHandshake(localStore.getDbToken()); + } + + @Override + public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { + localStore.setDbToken(dbToken); + handleWatchStreamHandshakeComplete(); } @Override @@ -194,11 +200,12 @@ public void onClose(Status status) { new WriteStream.Callback() { @Override public void onOpen() { - writeStream.writeHandshake(); + writeStream.sendHandshake(localStore.getDbToken()); } @Override - public void onHandshakeComplete() { + public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { + localStore.setDbToken(dbToken); handleWriteStreamHandshakeComplete(); } @@ -365,7 +372,7 @@ public void listen(TargetData targetData) { if (shouldStartWatchStream()) { startWatchStream(); - } else if (watchStream.isOpen()) { + } else if (watchStream.isOpen() && watchStream.isHandshakeComplete()) { sendWatchRequest(targetData); } } @@ -449,7 +456,7 @@ private void startWatchStream() { onlineStateTracker.handleWatchStreamStart(); } - private void handleWatchStreamOpen() { + private void handleWatchStreamHandshakeComplete() { // Restore any existing watches. for (TargetData targetData : listenTargets.values()) { sendWatchRequest(targetData); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java index e9d6830c7fd..f323ee74b1a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java @@ -21,8 +21,11 @@ import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.AsyncQueue.TimerId; import com.google.firestore.v1.FirestoreGrpc; +import com.google.firestore.v1.InitRequest; +import com.google.firestore.v1.InitResponse; import com.google.firestore.v1.ListenRequest; import com.google.firestore.v1.ListenResponse; +import com.google.firestore.v1.WriteRequest; import com.google.protobuf.ByteString; import java.util.Map; @@ -45,11 +48,16 @@ public class WatchStream /** A callback interface for the set of events that can be emitted by the WatchStream */ interface Callback extends AbstractStream.StreamCallback { + + /** The handshake for this write stream has completed */ + void onHandshakeComplete(ByteString dbToken, boolean clearCache); + /** A new change from the watch stream. Snapshot version will ne non-null if it was set */ void onWatchChange(SnapshotVersion snapshotVersion, WatchChange watchChange); } private final RemoteSerializer serializer; + protected boolean handshakeComplete = false; WatchStream( FirestoreChannel channel, @@ -67,6 +75,37 @@ interface Callback extends AbstractStream.StreamCallback { this.serializer = serializer; } + @Override + public void start() { + this.handshakeComplete = false; + super.start(); + } + + /** + * Sends an InitRequest to the server. + */ + void sendHandshake(ByteString dbToken) { + hardAssert(isOpen(), "Writing handshake requires an opened stream"); + hardAssert(!handshakeComplete, "Handshake already completed"); + + InitRequest.Builder initRequest = InitRequest.newBuilder() + .setDbToken(dbToken); + + ListenRequest.Builder request = ListenRequest.newBuilder() + .setDatabase(serializer.databaseName()) + .setInitRequest(initRequest); + + writeRequest(request.build()); + } + + /** + * Tracks whether or not a handshake has been successfully exchanged and the stream is ready to + * accept watch queries. + */ + boolean isHandshakeComplete() { + return handshakeComplete; + } + /** * Registers interest in the results of the given query. If the query includes a resumeToken it * will be included in the request. Results that affect the query will be streamed back as @@ -74,6 +113,7 @@ interface Callback extends AbstractStream.StreamCallback { */ public void watchQuery(TargetData targetData) { hardAssert(isOpen(), "Watching queries requires an open stream"); + hardAssert(handshakeComplete, "Handshake must be complete before watching queries"); ListenRequest.Builder request = ListenRequest.newBuilder() .setDatabase(serializer.databaseName()) @@ -100,12 +140,22 @@ public void unwatchTarget(int targetId) { } @Override - public void onNext(com.google.firestore.v1.ListenResponse listenResponse) { + public void onNext(com.google.firestore.v1.ListenResponse response) { // A successful response means the stream is healthy backoff.reset(); - WatchChange watchChange = serializer.decodeWatchChange(listenResponse); - SnapshotVersion snapshotVersion = serializer.decodeVersionFromListenResponse(listenResponse); - listener.onWatchChange(snapshotVersion, watchChange); + if (!handshakeComplete) { + hardAssert(response.hasInitResponse(), "InitResponse expected as part of Handshake response"); + + // The first response is the handshake response + handshakeComplete = true; + + InitResponse initResponse = response.getInitResponse(); + listener.onHandshakeComplete(initResponse.getDbToken(), initResponse.getClearCache()); + } else { + WatchChange watchChange = serializer.decodeWatchChange(response); + SnapshotVersion snapshotVersion = serializer.decodeVersionFromListenResponse(response); + listener.onWatchChange(snapshotVersion, watchChange); + } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java index f35b15a5151..20c2b41ffe7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java @@ -23,6 +23,8 @@ import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.AsyncQueue.TimerId; import com.google.firestore.v1.FirestoreGrpc; +import com.google.firestore.v1.InitRequest; +import com.google.firestore.v1.InitResponse; import com.google.firestore.v1.WriteRequest; import com.google.firestore.v1.WriteResponse; import com.google.protobuf.ByteString; @@ -54,7 +56,7 @@ public class WriteStream extends AbstractStream mutationResults); @@ -131,12 +133,18 @@ void setLastStreamToken(ByteString streamToken) { * StreamingWrite RPC work. Subsequent {@link #writeMutations} calls should wait until a response * has been delivered to {@link WriteStream.Callback#onHandshakeComplete}. */ - void writeHandshake() { + void sendHandshake(ByteString dbToken) { hardAssert(isOpen(), "Writing handshake requires an opened stream"); hardAssert(!handshakeComplete, "Handshake already completed"); // TODO: Support stream resumption. We intentionally do not set the stream token on the // handshake, ignoring any stream token we might have. - WriteRequest.Builder request = WriteRequest.newBuilder().setDatabase(serializer.databaseName()); + + InitRequest.Builder initRequest = InitRequest.newBuilder() + .setDbToken(dbToken); + + WriteRequest.Builder request = WriteRequest.newBuilder() + .setDatabase(serializer.databaseName()) + .setInitRequest(initRequest); writeRequest(request.build()); } @@ -164,10 +172,13 @@ public void onNext(WriteResponse response) { lastStreamToken = response.getStreamToken(); if (!handshakeComplete) { + hardAssert(response.hasInitResponse(),"InitResponse expected as part of Handshake response"); + // The first response is the handshake response handshakeComplete = true; - listener.onHandshakeComplete(); + InitResponse initResponse = response.getInitResponse(); + listener.onHandshakeComplete(initResponse.getDbToken(), initResponse.getClearCache()); } else { // A successful first write response means the stream is healthy, // Note, that we could consider a successful handshake healthy, however, diff --git a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto index 1bf75ea3c15..b29c3359c79 100644 --- a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto +++ b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto @@ -566,6 +566,39 @@ message RunAggregationQueryResponse { google.protobuf.Timestamp read_time = 3; } +// New message +message InitRequest { + // Token for synchronization. + // + // The `db_token`, that was received previously as part of InitResponse, should be + // passed back in the next `InitRequest`. + // + // If this is the first time SDK connects, then the `db_token` should be empty. + // + // The token contains database information used to determine whether SDK is out of + // sync. Contents are opaque and can change in the future. + // + // The `db_token` on the ListenStream has the same contents as the WriteStream. + // Whichever stream was last to receive a `db_token`, is the `db_token` that should + // be used as part of the InitRequest, regardless of whether it was from the other + // stream. + // + // The InitResponse will signal whether to `clear_cache`. + bytes db_token = 1; +} + +// New message +message InitResponse { + // Token for synchronization + // + // The `db_token` should be returned as part of the next InitRequest. + bytes db_token = 1; + + // Depending on `db_token`, changes may have occurred that require SDK to clear + // cache. + bool clear_cache = 2; +} + // The request for [Firestore.Write][google.firestore.v1.Firestore.Write]. // // The first request creates a stream, or resumes an existing one from a token. @@ -613,6 +646,8 @@ message WriteRequest { // Labels associated with this write request. map labels = 5; + + InitRequest init_request = 6; } // The response for [Firestore.Write][google.firestore.v1.Firestore.Write]. @@ -635,6 +670,8 @@ message WriteResponse { // The time at which the commit occurred. google.protobuf.Timestamp commit_time = 4; + + InitResponse init_response = 5; } // A request for [Firestore.Listen][google.firestore.v1.Firestore.Listen] @@ -650,6 +687,8 @@ message ListenRequest { // The ID of a target to remove from this stream. int32 remove_target = 3; + + InitRequest init_request = 5; } // Labels associated with this target change. @@ -679,6 +718,8 @@ message ListenResponse { // Returned when documents may have been removed from the given target, but // the exact documents are unknown. ExistenceFilter filter = 5; + + InitResponse init_response = 7; } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java index 91077548585..4766c8247ad 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java @@ -31,6 +31,8 @@ import com.google.firebase.firestore.testutil.EmptyCredentialsProvider; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Util; +import com.google.protobuf.ByteString; + import io.grpc.Status; import java.util.ArrayList; import java.util.HashMap; @@ -178,11 +180,11 @@ public boolean isOpen() { } @Override - public void writeHandshake() { + public void sendHandshake(ByteString dbToken) { hardAssert(!handshakeComplete, "Handshake already completed"); writeStreamRequestCount += 1; handshakeComplete = true; - listener.onHandshakeComplete(); + listener.onHandshakeComplete(dbToken, false); } @Override From dbafb0e55b91033b8e300f1a7d84cd50c5d3c29e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 16 May 2024 22:22:20 -0400 Subject: [PATCH 08/30] Fix --- .../com/google/firebase/firestore/local/SQLiteGlobalsCache.java | 1 - .../com/google/firebase/firestore/local/GlobalsCacheTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteGlobalsCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteGlobalsCache.java index 51e8467313c..a9c7233b608 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteGlobalsCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteGlobalsCache.java @@ -14,7 +14,6 @@ package com.google.firebase.firestore.local; -import com.google.firebase.firestore.auth.User; import com.google.protobuf.ByteString; public class SQLiteGlobalsCache implements GlobalsCache{ diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/GlobalsCacheTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/GlobalsCacheTest.java index d5274211e83..2693277ed95 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/GlobalsCacheTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/GlobalsCacheTest.java @@ -16,7 +16,6 @@ import static org.junit.Assert.assertEquals; -import com.google.firebase.firestore.auth.User; import com.google.protobuf.ByteString; import org.junit.After; From b4c144a6781d7ce4d5c696ef5a044471ebd97292 Mon Sep 17 00:00:00 2001 From: Daymon <17409137+daymxn@users.noreply.github.com> Date: Thu, 16 May 2024 23:54:01 -0500 Subject: [PATCH 09/30] Fix health metrics failures (#5969) Per [b/338407806](https://b.corp.google.com/issues/338407806), This fixes the issues with the health metrics action failing. It seemed to stem from a bunch of outdated dependencies in the health metrics sub projects. --- health-metrics/apk-size/apk-size.gradle | 4 ++-- health-metrics/apk-size/app/build.gradle | 2 +- health-metrics/apk-size/app/default.gradle | 3 ++- .../gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 61574 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 ++- .../benchmark/template/build.gradle | 2 +- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/health-metrics/apk-size/apk-size.gradle b/health-metrics/apk-size/apk-size.gradle index 5c0cd6967ac..620e2c7e9be 100644 --- a/health-metrics/apk-size/apk-size.gradle +++ b/health-metrics/apk-size/apk-size.gradle @@ -20,12 +20,12 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.2.0' + classpath 'com.android.tools.build:gradle:8.2.2' } } plugins { - id "com.dorongold.task-tree" version "1.5" + id "com.dorongold.task-tree" version "3.0.0" } apply from: '../../sdkProperties.gradle' diff --git a/health-metrics/apk-size/app/build.gradle b/health-metrics/apk-size/app/build.gradle index 2a4bedfdeb0..16ad24c9116 100644 --- a/health-metrics/apk-size/app/build.gradle +++ b/health-metrics/apk-size/app/build.gradle @@ -16,8 +16,8 @@ apply plugin: "com.android.application" apply from: "default.gradle" android { flavorDimensions "firebase", "abi" - // https://developer.android.com/ndk/guides/abis + namespace "com.google.apksize" productFlavors { "universal" { dimension "abi" diff --git a/health-metrics/apk-size/app/default.gradle b/health-metrics/apk-size/app/default.gradle index 83932707d3a..a2bd7151e0d 100644 --- a/health-metrics/apk-size/app/default.gradle +++ b/health-metrics/apk-size/app/default.gradle @@ -25,7 +25,7 @@ android { abortOnError false checkReleaseBuilds false } - compileSdk 33 + compileSdk 34 defaultConfig { applicationId 'com.google.apksize' @@ -61,4 +61,5 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22")) } diff --git a/health-metrics/apk-size/gradle/wrapper/gradle-wrapper.jar b/health-metrics/apk-size/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..943f0cbfa754578e88a3dae77fce6e3dea56edbf 100644 GIT binary patch delta 39316 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 delta 36846 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 CcabXq diff --git a/health-metrics/apk-size/gradle/wrapper/gradle-wrapper.properties b/health-metrics/apk-size/gradle/wrapper/gradle-wrapper.properties index 4d9ca164914..744c64d1277 100644 --- a/health-metrics/apk-size/gradle/wrapper/gradle-wrapper.properties +++ b/health-metrics/apk-size/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/health-metrics/benchmark/template/build.gradle b/health-metrics/benchmark/template/build.gradle index 071bb3b84f6..f0f8a1cbcac 100644 --- a/health-metrics/benchmark/template/build.gradle +++ b/health-metrics/benchmark/template/build.gradle @@ -18,7 +18,7 @@ plugins { id 'com.android.test' version '7.2.2' apply false id 'org.jetbrains.kotlin.android' version '1.8.22' apply false - id 'com.google.gms.google-services' version '4.3.13' apply false + id 'com.google.gms.google-services' version '4.3.15' apply false id 'com.google.firebase.crashlytics' version '2.9.1' apply false id 'com.google.firebase.firebase-perf' version '1.4.1' apply false } From d6bc28efd6cf6e53e873b6deba5cffada9114296 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 22 May 2024 10:52:52 -0400 Subject: [PATCH 10/30] WIP --- .../firebase/firestore/remote/StreamTest.java | 12 ++-- .../firebase/firestore/local/LocalStore.java | 8 +++ .../firestore/remote/RemoteStore.java | 17 ++++-- .../firestore/remote/WatchStream.java | 58 +++++++++++++++++-- .../firestore/remote/WriteStream.java | 19 ++++-- .../proto/google/firestore/v1/firestore.proto | 41 +++++++++++++ .../firestore/remote/MockDatastore.java | 6 +- 7 files changed, 141 insertions(+), 20 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java index c1fd0728933..a1dc9d9b771 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java @@ -37,6 +37,8 @@ import com.google.firebase.firestore.testutil.IntegrationTestUtil; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.AsyncQueue.TimerId; +import com.google.protobuf.ByteString; + import io.grpc.Status; import java.util.ArrayList; import java.util.Collections; @@ -95,7 +97,7 @@ public void onClose(Status status) { } @Override - public void onHandshakeComplete() { + public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { handshakeSemaphore.release(); } @@ -127,7 +129,7 @@ private void waitForWriteStreamOpen( AsyncQueue testQueue, WriteStream writeStream, StreamStatusCallback callback) { testQueue.enqueueAndForget(writeStream::start); waitFor(callback.openSemaphore); - testQueue.enqueueAndForget(writeStream::writeHandshake); + testQueue.enqueueAndForget(() -> writeStream.sendHandshake(ByteString.EMPTY)); waitFor(callback.handshakeSemaphore); } @@ -170,9 +172,9 @@ public void testWriteStreamStopAfterHandshake() throws Exception { StreamStatusCallback streamCallback = new StreamStatusCallback() { @Override - public void onHandshakeComplete() { + public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { assertThat(writeStreamWrapper[0].getLastStreamToken()).isNotEmpty(); - super.onHandshakeComplete(); + super.onHandshakeComplete(dbToken, clearCache); } @Override @@ -192,7 +194,7 @@ public void onWriteResponse( () -> assertThrows(Throwable.class, () -> writeStream.writeMutations(mutations))); // Handshake should always be called - testQueue.enqueueAndForget(writeStream::writeHandshake); + testQueue.enqueueAndForget(() -> writeStream.sendHandshake(ByteString.EMPTY)); waitFor(streamCallback.handshakeSemaphore); // Now writes should succeed diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java index 7924e3eaeb2..b97ebc475af 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java @@ -412,6 +412,14 @@ public SnapshotVersion getLastRemoteSnapshotVersion() { return targetCache.getLastRemoteSnapshotVersion(); } + public ByteString getDbToken() { + return globalsCache.getDbToken(); + } + + public void setDbToken(ByteString dbToken) { + globalsCache.setDbToken(dbToken); + } + /** * Updates the "ground-state" (remote) documents. We assume that the remote event reflects any * write batches that have been acknowledged or rejected (specifically, we do not re-apply local diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index 7984056db82..925da25ec68 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -182,7 +182,13 @@ public RemoteStore( new WatchStream.Callback() { @Override public void onOpen() { - handleWatchStreamOpen(); + watchStream.sendHandshake(localStore.getDbToken()); + } + + @Override + public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { + localStore.setDbToken(dbToken); + handleWatchStreamHandshakeComplete(); } @Override @@ -201,11 +207,12 @@ public void onClose(Status status) { new WriteStream.Callback() { @Override public void onOpen() { - writeStream.writeHandshake(); + writeStream.sendHandshake(localStore.getDbToken()); } @Override - public void onHandshakeComplete() { + public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { + localStore.setDbToken(dbToken); handleWriteStreamHandshakeComplete(); } @@ -372,7 +379,7 @@ public void listen(TargetData targetData) { if (shouldStartWatchStream()) { startWatchStream(); - } else if (watchStream.isOpen()) { + } else if (watchStream.isOpen() && watchStream.isHandshakeComplete()) { sendWatchRequest(targetData); } } @@ -456,7 +463,7 @@ private void startWatchStream() { onlineStateTracker.handleWatchStreamStart(); } - private void handleWatchStreamOpen() { + private void handleWatchStreamHandshakeComplete() { // Restore any existing watches. for (TargetData targetData : listenTargets.values()) { sendWatchRequest(targetData); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java index e9d6830c7fd..f323ee74b1a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java @@ -21,8 +21,11 @@ import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.AsyncQueue.TimerId; import com.google.firestore.v1.FirestoreGrpc; +import com.google.firestore.v1.InitRequest; +import com.google.firestore.v1.InitResponse; import com.google.firestore.v1.ListenRequest; import com.google.firestore.v1.ListenResponse; +import com.google.firestore.v1.WriteRequest; import com.google.protobuf.ByteString; import java.util.Map; @@ -45,11 +48,16 @@ public class WatchStream /** A callback interface for the set of events that can be emitted by the WatchStream */ interface Callback extends AbstractStream.StreamCallback { + + /** The handshake for this write stream has completed */ + void onHandshakeComplete(ByteString dbToken, boolean clearCache); + /** A new change from the watch stream. Snapshot version will ne non-null if it was set */ void onWatchChange(SnapshotVersion snapshotVersion, WatchChange watchChange); } private final RemoteSerializer serializer; + protected boolean handshakeComplete = false; WatchStream( FirestoreChannel channel, @@ -67,6 +75,37 @@ interface Callback extends AbstractStream.StreamCallback { this.serializer = serializer; } + @Override + public void start() { + this.handshakeComplete = false; + super.start(); + } + + /** + * Sends an InitRequest to the server. + */ + void sendHandshake(ByteString dbToken) { + hardAssert(isOpen(), "Writing handshake requires an opened stream"); + hardAssert(!handshakeComplete, "Handshake already completed"); + + InitRequest.Builder initRequest = InitRequest.newBuilder() + .setDbToken(dbToken); + + ListenRequest.Builder request = ListenRequest.newBuilder() + .setDatabase(serializer.databaseName()) + .setInitRequest(initRequest); + + writeRequest(request.build()); + } + + /** + * Tracks whether or not a handshake has been successfully exchanged and the stream is ready to + * accept watch queries. + */ + boolean isHandshakeComplete() { + return handshakeComplete; + } + /** * Registers interest in the results of the given query. If the query includes a resumeToken it * will be included in the request. Results that affect the query will be streamed back as @@ -74,6 +113,7 @@ interface Callback extends AbstractStream.StreamCallback { */ public void watchQuery(TargetData targetData) { hardAssert(isOpen(), "Watching queries requires an open stream"); + hardAssert(handshakeComplete, "Handshake must be complete before watching queries"); ListenRequest.Builder request = ListenRequest.newBuilder() .setDatabase(serializer.databaseName()) @@ -100,12 +140,22 @@ public void unwatchTarget(int targetId) { } @Override - public void onNext(com.google.firestore.v1.ListenResponse listenResponse) { + public void onNext(com.google.firestore.v1.ListenResponse response) { // A successful response means the stream is healthy backoff.reset(); - WatchChange watchChange = serializer.decodeWatchChange(listenResponse); - SnapshotVersion snapshotVersion = serializer.decodeVersionFromListenResponse(listenResponse); - listener.onWatchChange(snapshotVersion, watchChange); + if (!handshakeComplete) { + hardAssert(response.hasInitResponse(), "InitResponse expected as part of Handshake response"); + + // The first response is the handshake response + handshakeComplete = true; + + InitResponse initResponse = response.getInitResponse(); + listener.onHandshakeComplete(initResponse.getDbToken(), initResponse.getClearCache()); + } else { + WatchChange watchChange = serializer.decodeWatchChange(response); + SnapshotVersion snapshotVersion = serializer.decodeVersionFromListenResponse(response); + listener.onWatchChange(snapshotVersion, watchChange); + } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java index f35b15a5151..20c2b41ffe7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java @@ -23,6 +23,8 @@ import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.AsyncQueue.TimerId; import com.google.firestore.v1.FirestoreGrpc; +import com.google.firestore.v1.InitRequest; +import com.google.firestore.v1.InitResponse; import com.google.firestore.v1.WriteRequest; import com.google.firestore.v1.WriteResponse; import com.google.protobuf.ByteString; @@ -54,7 +56,7 @@ public class WriteStream extends AbstractStream mutationResults); @@ -131,12 +133,18 @@ void setLastStreamToken(ByteString streamToken) { * StreamingWrite RPC work. Subsequent {@link #writeMutations} calls should wait until a response * has been delivered to {@link WriteStream.Callback#onHandshakeComplete}. */ - void writeHandshake() { + void sendHandshake(ByteString dbToken) { hardAssert(isOpen(), "Writing handshake requires an opened stream"); hardAssert(!handshakeComplete, "Handshake already completed"); // TODO: Support stream resumption. We intentionally do not set the stream token on the // handshake, ignoring any stream token we might have. - WriteRequest.Builder request = WriteRequest.newBuilder().setDatabase(serializer.databaseName()); + + InitRequest.Builder initRequest = InitRequest.newBuilder() + .setDbToken(dbToken); + + WriteRequest.Builder request = WriteRequest.newBuilder() + .setDatabase(serializer.databaseName()) + .setInitRequest(initRequest); writeRequest(request.build()); } @@ -164,10 +172,13 @@ public void onNext(WriteResponse response) { lastStreamToken = response.getStreamToken(); if (!handshakeComplete) { + hardAssert(response.hasInitResponse(),"InitResponse expected as part of Handshake response"); + // The first response is the handshake response handshakeComplete = true; - listener.onHandshakeComplete(); + InitResponse initResponse = response.getInitResponse(); + listener.onHandshakeComplete(initResponse.getDbToken(), initResponse.getClearCache()); } else { // A successful first write response means the stream is healthy, // Note, that we could consider a successful handshake healthy, however, diff --git a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto index 1bf75ea3c15..b29c3359c79 100644 --- a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto +++ b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto @@ -566,6 +566,39 @@ message RunAggregationQueryResponse { google.protobuf.Timestamp read_time = 3; } +// New message +message InitRequest { + // Token for synchronization. + // + // The `db_token`, that was received previously as part of InitResponse, should be + // passed back in the next `InitRequest`. + // + // If this is the first time SDK connects, then the `db_token` should be empty. + // + // The token contains database information used to determine whether SDK is out of + // sync. Contents are opaque and can change in the future. + // + // The `db_token` on the ListenStream has the same contents as the WriteStream. + // Whichever stream was last to receive a `db_token`, is the `db_token` that should + // be used as part of the InitRequest, regardless of whether it was from the other + // stream. + // + // The InitResponse will signal whether to `clear_cache`. + bytes db_token = 1; +} + +// New message +message InitResponse { + // Token for synchronization + // + // The `db_token` should be returned as part of the next InitRequest. + bytes db_token = 1; + + // Depending on `db_token`, changes may have occurred that require SDK to clear + // cache. + bool clear_cache = 2; +} + // The request for [Firestore.Write][google.firestore.v1.Firestore.Write]. // // The first request creates a stream, or resumes an existing one from a token. @@ -613,6 +646,8 @@ message WriteRequest { // Labels associated with this write request. map labels = 5; + + InitRequest init_request = 6; } // The response for [Firestore.Write][google.firestore.v1.Firestore.Write]. @@ -635,6 +670,8 @@ message WriteResponse { // The time at which the commit occurred. google.protobuf.Timestamp commit_time = 4; + + InitResponse init_response = 5; } // A request for [Firestore.Listen][google.firestore.v1.Firestore.Listen] @@ -650,6 +687,8 @@ message ListenRequest { // The ID of a target to remove from this stream. int32 remove_target = 3; + + InitRequest init_request = 5; } // Labels associated with this target change. @@ -679,6 +718,8 @@ message ListenResponse { // Returned when documents may have been removed from the given target, but // the exact documents are unknown. ExistenceFilter filter = 5; + + InitResponse init_response = 7; } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java index 91077548585..4766c8247ad 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java @@ -31,6 +31,8 @@ import com.google.firebase.firestore.testutil.EmptyCredentialsProvider; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Util; +import com.google.protobuf.ByteString; + import io.grpc.Status; import java.util.ArrayList; import java.util.HashMap; @@ -178,11 +180,11 @@ public boolean isOpen() { } @Override - public void writeHandshake() { + public void sendHandshake(ByteString dbToken) { hardAssert(!handshakeComplete, "Handshake already completed"); writeStreamRequestCount += 1; handshakeComplete = true; - listener.onHandshakeComplete(); + listener.onHandshakeComplete(dbToken, false); } @Override From 0d0235c65b35227953706e61298a34aeb4da5bc3 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 22 May 2024 16:04:57 -0400 Subject: [PATCH 11/30] Theoretical Implementation --- .../firestore/remote/RemoteStoreTest.java | 2 +- .../core/MemoryComponentProvider.java | 4 +- .../firebase/firestore/core/SyncEngine.java | 5 +- .../firestore/remote/RemoteStore.java | 57 +++++++++++++++---- .../firebase/firestore/spec/SpecTestCase.java | 4 +- 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java index ee8620d5aa7..cb52365683f 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java @@ -69,7 +69,7 @@ public void handleOnlineStateChange(OnlineState onlineState) { } @Override - public void handleClearCache() {} + public void clearCacheData() {} @Override public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java index c7d3ce8952d..af95fca3e69 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java @@ -146,8 +146,8 @@ public void handleOnlineStateChange(OnlineState onlineState) { } @Override - public void handleClearCache() { - getSyncEngine().handleClearCache(); + public void clearCacheData() { + getSyncEngine().clearCacheData(); } @Override diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index 5fc5cf82ebb..86faf4ebce4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -516,8 +516,9 @@ public void handleRejectedWrite(int batchId, Status status) { } @Override - public void handleClearCache() { - assertCallback("handleClearCache"); + public void clearCacheData() { + assertCallback("clearCacheData"); + boolean canUseNetwork = remoteStore.canUseNetwork(); if (canUseNetwork) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index 925da25ec68..7ef84a5a011 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -66,6 +66,8 @@ public final class RemoteStore implements WatchChangeAggregator.TargetMetadataPr /** The log tag to use for this class. */ private static final String LOG_TAG = "RemoteStore"; + private boolean writeStreamHandshakeInProgress; + private boolean watchStreamHandshakeInProgress; /** A callback interface for events from RemoteStore. */ public interface RemoteStoreCallback { @@ -109,7 +111,7 @@ public interface RemoteStoreCallback { /** * Synchronization event that requires cache be cleared. */ - void handleClearCache(); + void clearCacheData(); /** * Returns the set of remote document keys for the given target ID. This list includes the @@ -176,19 +178,25 @@ public RemoteStore( onlineStateTracker = new OnlineStateTracker(workerQueue, remoteStoreCallback::handleOnlineStateChange); + watchStreamHandshakeInProgress = false; + writeStreamHandshakeInProgress = false; + // Create new streams (but note they're not started yet). watchStream = datastore.createWatchStream( new WatchStream.Callback() { @Override public void onOpen() { - watchStream.sendHandshake(localStore.getDbToken()); + hardAssert(!watchStreamHandshakeInProgress, "Watch handshake already in progress."); + if (!writeStreamHandshakeInProgress) { + watchStream.sendHandshake(localStore.getDbToken()); + } + watchStreamHandshakeInProgress = true; } @Override public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { - localStore.setDbToken(dbToken); - handleWatchStreamHandshakeComplete(); + handleWatchStreamHandshakeComplete(dbToken, clearCache); } @Override @@ -198,6 +206,7 @@ public void onWatchChange(SnapshotVersion snapshotVersion, WatchChange watchChan @Override public void onClose(Status status) { + watchStreamHandshakeInProgress = false; handleWatchStreamClose(status); } }); @@ -207,13 +216,16 @@ public void onClose(Status status) { new WriteStream.Callback() { @Override public void onOpen() { - writeStream.sendHandshake(localStore.getDbToken()); + hardAssert(!writeStreamHandshakeInProgress, "Watch handshake already in progress."); + if (!watchStreamHandshakeInProgress) { + writeStream.sendHandshake(localStore.getDbToken()); + } + writeStreamHandshakeInProgress = true; } @Override public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { - localStore.setDbToken(dbToken); - handleWriteStreamHandshakeComplete(); + handleWriteStreamHandshakeComplete(dbToken, clearCache); } @Override @@ -224,6 +236,7 @@ public void onWriteResponse( @Override public void onClose(Status status) { + writeStreamHandshakeInProgress = false; handleWriteStreamClose(status); } }); @@ -463,7 +476,19 @@ private void startWatchStream() { onlineStateTracker.handleWatchStreamStart(); } - private void handleWatchStreamHandshakeComplete() { + private void handleWatchStreamHandshakeComplete(ByteString dbToken, boolean clearCache) { + if (clearCache) { + remoteStoreCallback.clearCacheData(); + } + localStore.setDbToken(dbToken); + watchStreamHandshakeInProgress = false; + + // If write stream started handshake, but was waiting for listen handshake to complete, we + // can continue write handshake now. + if (writeStreamHandshakeInProgress) { + writeStream.sendHandshake(dbToken); + } + // Restore any existing watches. for (TargetData targetData : listenTargets.values()) { sendWatchRequest(targetData); @@ -612,7 +637,7 @@ public void abortAllTargets() { // To prevent Limbo Resolution from sending new listen request during abort of all targets, the // network must be disabled. Not doing so will cause `handleRejectedListen` to start watch // stream. - hardAssert(!canUseNetwork(), "Network should be disabled during abort of all targets."); + hardAssert(!canUseNetwork(), "Network must be disabled during abort of all targets."); List targetIds = new ArrayList<>(); for (Entry entry : listenTargets.entrySet()) { @@ -695,7 +720,19 @@ private void startWriteStream() { * Handles a successful handshake response from the server, which is our cue to send any pending * writes. */ - private void handleWriteStreamHandshakeComplete() { + private void handleWriteStreamHandshakeComplete(ByteString dbToken, boolean clearCache) { + if (clearCache) { + remoteStoreCallback.clearCacheData(); + } + localStore.setDbToken(dbToken); + writeStreamHandshakeInProgress = false; + + // If listen stream started handshake, but was waiting for write handshake to complete, we + // can continue listen handshake now. + if (watchStreamHandshakeInProgress) { + watchStream.sendHandshake(dbToken); + } + // Record the stream token. localStore.setLastStreamToken(writeStream.getLastStreamToken()); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index 997e496d668..266b565d4b8 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -345,8 +345,8 @@ public void handleOnlineStateChange(OnlineState onlineState) { } @Override - public void handleClearCache() { - syncEngine.handleClearCache(); + public void clearCacheData() { + syncEngine.clearCacheData(); } private List>> getCurrentOutstandingWrites() { From 49f7d702ec3149e54a8724359492153e4ea6abec Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Fri, 24 May 2024 00:58:18 -0400 Subject: [PATCH 12/30] Fix spec tests --- .../google/firebase/firestore/remote/WatchStream.java | 4 ++-- .../google/firebase/firestore/util/AsyncQueue.java | 4 ++++ .../firebase/firestore/remote/MockDatastore.java | 11 ++++++++++- .../google/firebase/firestore/spec/SpecTestCase.java | 4 ++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java index f323ee74b1a..910e553181f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java @@ -88,8 +88,8 @@ void sendHandshake(ByteString dbToken) { hardAssert(isOpen(), "Writing handshake requires an opened stream"); hardAssert(!handshakeComplete, "Handshake already completed"); - InitRequest.Builder initRequest = InitRequest.newBuilder() - .setDbToken(dbToken); + InitRequest.Builder initRequest = InitRequest.newBuilder(); + if (dbToken != null) initRequest.setDbToken(dbToken); ListenRequest.Builder request = ListenRequest.newBuilder() .setDatabase(serializer.databaseName()) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java index 0333a04adf1..34d4ef0d8de 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java @@ -580,6 +580,10 @@ public boolean containsDelayedTask(TimerId timerId) { return false; } + public boolean isIdle() { + return executor.internalExecutor.getActiveCount() == 0; + } + /** * Runs some or all delayed tasks early, blocking until completion. * diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java index 4766c8247ad..998201b497c 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java @@ -61,6 +61,7 @@ private class MockWatchStream extends WatchStream { @Override public void start() { hardAssert(!open, "Trying to start already started watch stream"); + handshakeComplete = false; open = true; listener.onOpen(); } @@ -70,6 +71,7 @@ public void stop() { super.stop(); activeTargets.clear(); open = false; + handshakeComplete = false; } @Override @@ -82,6 +84,13 @@ public boolean isOpen() { return open; } + @Override + void sendHandshake(ByteString dbToken) { + hardAssert(!handshakeComplete, "Handshake already completed"); + handshakeComplete = true; + getWorkerQueue().enqueue(() -> listener.onHandshakeComplete(dbToken == null ? ByteString.EMPTY :dbToken, false)); + } + @Override public void watchQuery(TargetData targetData) { String resumeToken = Util.toDebugString(targetData.getResumeToken()); @@ -184,7 +193,7 @@ public void sendHandshake(ByteString dbToken) { hardAssert(!handshakeComplete, "Handshake already completed"); writeStreamRequestCount += 1; handshakeComplete = true; - listener.onHandshakeComplete(dbToken, false); + getWorkerQueue().enqueue(() -> listener.onHandshakeComplete(dbToken == null ? ByteString.EMPTY :dbToken, false)); } @Override diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index 266b565d4b8..a390948dd4e 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -1295,6 +1295,10 @@ private void runSteps(JSONArray steps, JSONObject config) throws Exception { backgroundExecutor.execute(() -> drainBackgroundQueue.setResult(null)); waitFor(drainBackgroundQueue.getTask()); + while (!queue.isIdle()) { + Thread.sleep(1); + } + if (expectedSnapshotEvents != null) { log(" Validating expected snapshot events " + expectedSnapshotEvents); } From 685a29142ca2a3cba4fd83f61d7397b7eb148445 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 27 May 2024 11:47:22 -0400 Subject: [PATCH 13/30] WIP --- .../firebase/firestore/core/EventManager.java | 8 ++ .../firebase/firestore/core/SyncEngine.java | 26 ++++- .../firebase/firestore/local/BundleCache.java | 2 + .../firestore/local/DocumentOverlayCache.java | 2 + .../firebase/firestore/local/LocalStore.java | 15 ++- .../firestore/local/MemoryBundleCache.java | 5 + .../local/MemoryDocumentOverlayCache.java | 5 + .../local/MemoryRemoteDocumentCache.java | 5 + .../firestore/local/MemoryTargetCache.java | 27 +++++- .../firestore/local/RemoteDocumentCache.java | 2 + .../firestore/local/SQLiteBundleCache.java | 7 ++ .../local/SQLiteDocumentOverlayCache.java | 7 ++ .../local/SQLiteRemoteDocumentCache.java | 7 ++ .../firestore/local/SQLiteTargetCache.java | 18 ++++ .../firebase/firestore/local/TargetCache.java | 4 + .../firestore/remote/RemoteStore.java | 26 +---- .../firestore/core/EventManagerTest.java | 97 +++++++++++++++++++ .../firestore/local/CountingQueryEngine.java | 10 ++ 18 files changed, 244 insertions(+), 29 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java index afb8c66278a..8eb99124475 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java @@ -16,6 +16,8 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; +import androidx.annotation.VisibleForTesting; + import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.ListenSource; import com.google.firebase.firestore.core.SyncEngine.SyncEngineCallback; @@ -265,4 +267,10 @@ public void handleOnlineStateChange(OnlineState onlineState) { raiseSnapshotsInSyncEvent(); } } + + @VisibleForTesting + public boolean isEmpty() { + return queries.isEmpty() + && snapshotsInSyncListeners.isEmpty(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index 5fc5cf82ebb..6218cbb1d4d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -524,7 +524,7 @@ public void handleClearCache() { remoteStore.disableNetwork(); } - remoteStore.abortAllTargets(); + abortAllTargets(); localStore.clearCacheData(); if (canUseNetwork) { @@ -837,4 +837,28 @@ private boolean errorIsInteresting(Status error) { return false; } + @VisibleForTesting + public boolean isEmpty() { + return queryViewsByQuery.isEmpty() + && queriesByTarget.isEmpty() + && enqueuedLimboResolutions.isEmpty() + && activeLimboTargetsByKey.isEmpty() + && activeLimboResolutionsByTarget.isEmpty() + && limboDocumentRefs.isEmpty() + && mutationUserCallbacks.isEmpty() + && pendingWritesCallbacks.isEmpty(); + } + + public void abortAllTargets() { + hardAssert(!remoteStore.canUseNetwork(), "Network should be disabled during abort of all targets."); + + List targetIds = new ArrayList<>(); + for (QueryView queryView : queryViewsByQuery.values()) { + targetIds.add(queryView.getTargetId()); + } + for (Integer targetId : targetIds) { + handleRejectedListen(targetId, Status.ABORTED); + } + } + } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java index 642077264f8..c88e1744112 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java @@ -41,4 +41,6 @@ public interface BundleCache { void saveNamedQuery(NamedQuery query); void clear(); + + boolean isEmpty(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java index f3d1b19cd9f..e9a7fe2e0f7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java @@ -82,4 +82,6 @@ public interface DocumentOverlayCache { * Clear overlays. This should only be done when mutation queue is cleared. */ void clear(); + + boolean isEmpty(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java index 7924e3eaeb2..6f5d6c38698 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java @@ -99,7 +99,7 @@ *

The LocalStore must be able to efficiently execute queries against its local cache of the * documents, to provide the initial set of results before any remote changes have been received. */ -public final class LocalStore implements BundleCallback { +public class LocalStore implements BundleCallback { /** * The maximum time to leave a resume token buffered without writing it out. This value is * arbitrary: it's long enough to avoid several writes (possibly indefinitely if updates come more @@ -237,6 +237,7 @@ public void clearCacheData() { documentOverlayCache.clear(); remoteDocuments.clear(); + targetCache.clear(); bundleCache.clear(); // Clearing parents is only possible when both mutations and document cache are cleared. @@ -932,4 +933,16 @@ private static Target newUmbrellaTarget(String bundleName) { // queried. return Query.atPath(ResourcePath.fromString("__bundle__/docs/" + bundleName)).toTarget(); } + + @VisibleForTesting + public boolean isEmpty() { + return localViewReferences.isEmpty() + && queryDataByTarget.size() == 0 + && targetIdByTarget.isEmpty() + && mutationQueue.isEmpty() + && documentOverlayCache.isEmpty() + && remoteDocuments.isEmpty() + && bundleCache.isEmpty() + && targetCache.isEmpty(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java index f6203bcca37..7ad3dc4cbf7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java @@ -52,4 +52,9 @@ public void clear() { bundles.clear(); namedQueries.clear(); } + + @Override + public boolean isEmpty() { + return bundles.isEmpty() && namedQueries.isEmpty(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java index 997d2a14b8d..925bb7085f6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java @@ -152,4 +152,9 @@ public void clear() { overlays.clear(); overlayByBatchId.clear(); } + + @Override + public boolean isEmpty() { + return overlays.isEmpty() && overlayByBatchId.isEmpty(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java index 26d96e7658d..47bd23b996d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java @@ -146,6 +146,11 @@ public Map getDocumentsMatchingQuery( return result; } + @Override + public boolean isEmpty() { + return docs.isEmpty(); + } + @Override public Map getDocumentsMatchingQuery( Query query, IndexOffset offset, @Nonnull Set mutatedKeys) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java index 41881f16f04..937604fa3ac 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java @@ -16,6 +16,7 @@ import android.util.SparseArray; import androidx.annotation.Nullable; + import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.core.Target; import com.google.firebase.firestore.model.DocumentKey; @@ -41,14 +42,21 @@ final class MemoryTargetCache implements TargetCache { private int highestTargetId; /** The last received snapshot version. */ - private SnapshotVersion lastRemoteSnapshotVersion = SnapshotVersion.NONE; + private SnapshotVersion lastRemoteSnapshotVersion; - private long highestSequenceNumber = 0; + private long highestSequenceNumber; private final MemoryPersistence persistence; MemoryTargetCache(MemoryPersistence persistence) { this.persistence = persistence; + init(); + } + + private void init() { + highestTargetId = 0; + highestSequenceNumber = 0; + lastRemoteSnapshotVersion = SnapshotVersion.NONE; } @Override @@ -172,6 +180,21 @@ public boolean containsKey(DocumentKey key) { return references.containsKey(key); } + @Override + public void clear() { + targets.clear(); + init(); + } + + @Override + public boolean isEmpty() { + return targets.isEmpty() + && references.isEmpty() + && highestTargetId == 0 + && highestSequenceNumber == 0 + && lastRemoteSnapshotVersion == SnapshotVersion.NONE; + } + long getByteSize(LocalSerializer serializer) { long count = 0; for (Map.Entry entry : targets.entrySet()) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java index 3382ba59668..a41285fc853 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java @@ -110,4 +110,6 @@ Map getDocumentsMatchingQuery( IndexOffset offset, @Nonnull Set mutatedKeys, @Nullable QueryContext context); + + boolean isEmpty(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java index 8c385aa309f..348aa4e076d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java @@ -24,6 +24,8 @@ import com.google.firestore.bundle.BundledQuery; import com.google.protobuf.InvalidProtocolBufferException; +import kotlin.NotImplementedError; + class SQLiteBundleCache implements BundleCache { private final SQLitePersistence db; private final LocalSerializer serializer; @@ -110,4 +112,9 @@ public void clear() { db.execute("DELETE FROM bundles"); db.execute("DELETE FROM named_queries"); } + + @Override + public boolean isEmpty() { + throw new NotImplementedError(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java index 370ee15ad94..c41a8ea5403 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java @@ -204,6 +204,13 @@ public void clear() { db.execute("DELETE FROM document_overlays"); } + @Override + public boolean isEmpty() { + return db.query("SELECT COUNT(*) FROM overlay_mutation") + .firstValue(row -> row.getInt(0)) + .intValue() == 0; + } + private void processOverlaysInBackground( BackgroundQueue backgroundQueue, Map results, Cursor row) { byte[] rawMutation = row.getBlob(0); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java index 4ba6be42057..825da2014f2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java @@ -47,6 +47,8 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; +import kotlin.NotImplementedError; + final class SQLiteRemoteDocumentCache implements RemoteDocumentCache { /** The number of bind args per collection group in {@link #getAll(String, IndexOffset, int)} */ @VisibleForTesting static final int BINDS_PER_STATEMENT = 9; @@ -288,6 +290,11 @@ public Map getDocumentsMatchingQuery( context); } + @Override + public boolean isEmpty() { + throw new NotImplementedError(); + } + private MutableDocument decodeMaybeDocument( byte[] bytes, int readTimeSeconds, int readTimeNanos) { try { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java index 12105419fd6..ab1e4e5f678 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java @@ -28,6 +28,8 @@ import com.google.firebase.firestore.util.Consumer; import com.google.protobuf.InvalidProtocolBufferException; +import kotlin.NotImplementedError; + /** Cached Queries backed by SQLite. */ final class SQLiteTargetCache implements TargetCache { @@ -310,4 +312,20 @@ public boolean containsKey(DocumentKey key) { .binding(path) .isEmpty(); } + + @Override + public void clear() { + db.execute("DELETE FROM targets"); + db.execute("DELETE FROM target_documents"); + highestTargetId = 0; + lastListenSequenceNumber = 0; + lastRemoteSnapshotVersion = SnapshotVersion.NONE; + targetCount = 0; + writeMetadata(); + } + + @Override + public boolean isEmpty() { + throw new NotImplementedError(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java index 0b39babfb5f..bbe424a32c6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java @@ -115,4 +115,8 @@ interface TargetCache { /** @return True if the document is part of any target */ boolean containsKey(DocumentKey key); + + void clear(); + + boolean isEmpty(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index 7984056db82..c74bb850f14 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -59,7 +59,7 @@ * RemoteStore handles all interaction with the backend through a simple, clean interface. This * class is not thread safe and should be only called from the worker AsyncQueue. */ -public final class RemoteStore implements WatchChangeAggregator.TargetMetadataProvider { +public class RemoteStore implements WatchChangeAggregator.TargetMetadataProvider { /** The maximum number of pending writes to allow. TODO: Negotiate this value with the backend. */ private static final int MAX_PENDING_WRITES = 10; @@ -601,30 +601,6 @@ private void processTargetError(WatchTargetChange targetChange) { } } - public void abortAllTargets() { - // To prevent Limbo Resolution from sending new listen request during abort of all targets, the - // network must be disabled. Not doing so will cause `handleRejectedListen` to start watch - // stream. - hardAssert(!canUseNetwork(), "Network should be disabled during abort of all targets."); - - List targetIds = new ArrayList<>(); - for (Entry entry : listenTargets.entrySet()) { - switch (entry.getValue().getPurpose()) { - case LIMBO_RESOLUTION: - // Limbo resolutions are cleared when original listen is cleared. - continue; - case LISTEN: - case EXISTENCE_FILTER_MISMATCH: - case EXISTENCE_FILTER_MISMATCH_BLOOM: - targetIds.add(entry.getKey()); - } - } - for (Integer targetId : targetIds) { - listenTargets.remove(targetId); - remoteStoreCallback.handleRejectedListen(targetId, Status.ABORTED); - } - } - // Write Stream /** diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java index 5531033a272..70dbbfec8e8 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java @@ -16,24 +16,51 @@ import static com.google.firebase.firestore.testutil.TestUtil.path; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; + +import com.google.firebase.firestore.EventListener; +import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.FirebaseFirestoreException.Code; +import com.google.firebase.firestore.auth.User; import com.google.firebase.firestore.core.EventManager.ListenOptions; +import com.google.firebase.firestore.local.LocalSerializer; +import com.google.firebase.firestore.local.LocalStore; +import com.google.firebase.firestore.local.LruGarbageCollector; +import com.google.firebase.firestore.local.MemoryPersistence; +import com.google.firebase.firestore.local.Persistence; +import com.google.firebase.firestore.local.QueryEngine; +import com.google.firebase.firestore.local.TargetData; +import com.google.firebase.firestore.model.DatabaseId; +import com.google.firebase.firestore.remote.RemoteSerializer; +import com.google.firebase.firestore.remote.RemoteStore; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentMatcher; import org.mockito.InOrder; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; @@ -141,4 +168,74 @@ public void testWillForwardOnOnlineStateChangedCalls() { eventManager.handleOnlineStateChange(OnlineState.ONLINE); assertEquals(Arrays.asList(OnlineState.UNKNOWN, OnlineState.ONLINE), events); } + + @Test + public void xxx() { + Query query = Query.atPath(path("foo/bar")); + + EventListener eventListener1 = mock(EventListener.class); + EventListener eventListener2 = mock(EventListener.class); + + QueryListener listener1 = new QueryListener(query, new ListenOptions(), eventListener1); + QueryListener listener2 = new QueryListener(query, new ListenOptions(), eventListener2); + + RemoteStore remoteStore = mockRemoteStore(); + LocalStore localStore = createLruGcMemoryLocalStore(); + SyncEngine syncEngine = spy(new SyncEngine(localStore, remoteStore, User.UNAUTHENTICATED, 100)); + EventManager eventManager = new EventManager(syncEngine); + + eventManager.addQueryListener(listener1); + eventManager.addQueryListener(listener2); + + syncEngine.handleClearCache(); + + verify(syncEngine, times(1)) + .listen( + query, + /** shouldListenToRemote= */ + true); + + ArgumentMatcher abortedExceptionMatcher = e -> e.getCode() == Code.ABORTED; + verify(eventListener1, times(1)) + .onEvent(isNull(), argThat(abortedExceptionMatcher)); + + verify(eventListener2, times(1)) + .onEvent(isNull(), argThat(abortedExceptionMatcher)); + + Mockito.verify(remoteStore, times(1)).listen(any(TargetData.class)); + Mockito.verify(remoteStore, atLeastOnce()).canUseNetwork(); + Mockito.verify(remoteStore, times(1)).disableNetwork(); + Mockito.verify(remoteStore, times(1)).enableNetwork(); + Mockito.verifyNoMoreInteractions(remoteStore); + + assertTrue(syncEngine.isEmpty()); + assertTrue(eventManager.isEmpty()); + assertTrue(localStore.isEmpty()); + } + + @NonNull + private static RemoteStore mockRemoteStore() { + AtomicBoolean online = new AtomicBoolean(true); + RemoteStore remoteStore = mock(RemoteStore.class); + when(remoteStore.canUseNetwork()).thenAnswer(invocation -> online.get()); + doAnswer((Answer) invocation -> { + online.set(true); + return null; + }).when(remoteStore).enableNetwork(); + doAnswer((Answer) invocation -> { + online.set(false); + return null; + }).when(remoteStore).disableNetwork(); + return remoteStore; + } + + @NonNull + private static LocalStore createLruGcMemoryLocalStore() { + DatabaseId databaseId = DatabaseId.forProject("projectId"); + LocalSerializer serializer = new LocalSerializer(new RemoteSerializer(databaseId)); + Persistence persistence = MemoryPersistence.createLruGcMemoryPersistence( + LruGarbageCollector.Params.Default(), serializer); + persistence.start(); + return new LocalStore(persistence, new QueryEngine(), User.UNAUTHENTICATED); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java index 7b2d5ca5983..dd691616127 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java @@ -205,6 +205,11 @@ public Map getDocumentsMatchingQuery( documentsReadByCollection[0] += result.size(); return result; } + + @Override + public boolean isEmpty() { + return subject.isEmpty(); + } }; } @@ -266,6 +271,11 @@ public void clear() { subject.clear(); } + @Override + public boolean isEmpty() { + return subject.isEmpty(); + } + private OverlayType getOverlayType(Overlay overlay) { if (overlay == null) { return null; From 53a39f9533c9f31ec301848b99119ce64119583b Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 4 Jun 2024 15:44:09 -0400 Subject: [PATCH 14/30] Rewrite --- .../firebase/firestore/AccessHelper.java | 23 +- .../firestore/CompositeIndexQueryTest.java | 3 +- .../FirestoreClientProviderTest.java | 12 + .../firebase/firestore/FirestoreTest.java | 3 +- .../google/firebase/firestore/QueryTest.java | 4 +- .../firestore/ServerTimestampTest.java | 8 +- .../firebase/firestore/ValidationTest.java | 5 +- .../firestore/remote/RemoteStoreTest.java | 8 +- .../firebase/firestore/remote/StreamTest.java | 21 +- .../testutil/IntegrationTestUtil.java | 7 +- .../firebase/firestore/AggregateQuery.java | 5 +- .../firebase/firestore/DocumentReference.java | 30 +- .../firebase/firestore/FirebaseFirestore.java | 179 ++--------- .../firestore/FirestoreClientProvider.java | 283 ++++++++++++++++++ .../PersistentCacheIndexManager.java | 10 +- .../com/google/firebase/firestore/Query.java | 12 +- .../google/firebase/firestore/WriteBatch.java | 4 +- .../firestore/core/ComponentProvider.java | 12 +- .../firebase/firestore/core/EventManager.java | 13 +- .../firestore/core/FirestoreClient.java | 138 ++++++--- .../core/MemoryComponentProvider.java | 13 +- .../firebase/firestore/core/SyncEngine.java | 76 +---- .../firebase/firestore/local/BundleCache.java | 4 - .../firestore/local/DocumentOverlayCache.java | 7 - .../firestore/local/GlobalsCache.java | 4 +- .../firestore/local/IndexManager.java | 5 - .../firebase/firestore/local/LocalStore.java | 39 +-- .../firestore/local/MemoryBundleCache.java | 11 - .../local/MemoryDocumentOverlayCache.java | 11 - .../firestore/local/MemoryGlobalsCache.java | 10 +- .../firestore/local/MemoryIndexManager.java | 14 - .../firestore/local/MemoryMutationQueue.java | 12 +- .../local/MemoryRemoteDocumentCache.java | 12 - .../firestore/local/MemoryTargetCache.java | 27 +- .../firestore/local/MutationQueue.java | 5 - .../firestore/local/ReferenceSet.java | 8 +- .../firestore/local/RemoteDocumentCache.java | 5 - .../firestore/local/SQLiteBundleCache.java | 13 - .../local/SQLiteDocumentOverlayCache.java | 12 - .../firestore/local/SQLiteGlobalsCache.java | 10 +- .../firestore/local/SQLiteIndexManager.java | 34 --- .../firestore/local/SQLiteMutationQueue.java | 9 - .../local/SQLiteRemoteDocumentCache.java | 13 - .../firestore/local/SQLiteTargetCache.java | 18 -- .../firebase/firestore/local/TargetCache.java | 4 - .../firestore/remote/AbstractStream.java | 31 +- .../firebase/firestore/remote/Datastore.java | 3 + .../firestore/remote/RemoteSerializer.java | 6 +- .../firestore/remote/RemoteStore.java | 116 ++++--- .../remote/WatchChangeAggregator.java | 2 +- .../firestore/remote/WatchStream.java | 41 +-- .../firestore/remote/WriteStream.java | 65 ++-- .../firebase/firestore/util/AsyncQueue.java | 271 ++--------------- .../SynchronizedShutdownAwareExecutor.java | 255 ++++++++++++++++ .../proto/google/firestore/v1/firestore.proto | 16 +- .../firestore/core/EventManagerTest.java | 32 +- .../firestore/local/CountingQueryEngine.java | 24 +- .../firestore/local/GlobalsCacheTest.java | 4 +- .../firestore/local/LocalStoreTestCase.java | 68 ----- .../firestore/remote/MockDatastore.java | 17 +- .../firebase/firestore/spec/SpecTestCase.java | 16 +- .../firestore/util/AsyncQueueTest.java | 4 +- 62 files changed, 1020 insertions(+), 1107 deletions(-) create mode 100644 firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreClientProviderTest.java create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java index b9d0bce0c55..a4fc82cef35 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java @@ -15,6 +15,9 @@ package com.google.firebase.firestore; import android.content.Context; + +import androidx.core.util.Supplier; + import com.google.firebase.FirebaseApp; import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.User; @@ -26,21 +29,19 @@ public final class AccessHelper { /** Makes the FirebaseFirestore constructor accessible. */ public static FirebaseFirestore newFirebaseFirestore( - Context context, - DatabaseId databaseId, - String persistenceKey, - CredentialsProvider authProvider, - CredentialsProvider appCheckProvider, - AsyncQueue asyncQueue, - FirebaseApp firebaseApp, - FirebaseFirestore.InstanceRegistry instanceRegistry) { + Context context, + DatabaseId databaseId, + String persistenceKey, + Supplier> authProviderFactory, + Supplier> appCheckTokenProviderFactory, + FirebaseApp firebaseApp, + FirebaseFirestore.InstanceRegistry instanceRegistry) { return new FirebaseFirestore( context, databaseId, persistenceKey, - authProvider, - appCheckProvider, - asyncQueue, + authProviderFactory, + appCheckTokenProviderFactory, firebaseApp, instanceRegistry, null); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java index f92ab7afd3d..ae7134425a6 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java @@ -38,6 +38,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.firebase.firestore.Query.Direction; +import com.google.firebase.firestore.core.FirestoreClient; import com.google.firebase.firestore.testutil.CompositeIndexTestHelper; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Map; @@ -742,7 +743,7 @@ public void testMultipleInequalityReadFromCacheWhenOffline() { assertEquals(2L, snapshot1.size()); assertFalse(snapshot1.getMetadata().isFromCache()); - waitFor(collection.firestore.getClient().disableNetwork()); + waitFor(collection.firestore.callClient(FirestoreClient::disableNetwork)); QuerySnapshot snapshot2 = waitFor(query.get()); assertEquals(2L, snapshot2.size()); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreClientProviderTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreClientProviderTest.java new file mode 100644 index 00000000000..9a55bc5931b --- /dev/null +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreClientProviderTest.java @@ -0,0 +1,12 @@ +package com.google.firebase.firestore; + +import org.junit.runner.RunWith; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +@RunWith(AndroidJUnit4.class) +public class FirestoreClientProviderTest { + + //TODO(requires backend/emulator support) + +} \ No newline at end of file diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java index 631dcec4dd8..6988ce09c31 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreTest.java @@ -49,6 +49,7 @@ import com.google.firebase.firestore.FirebaseFirestoreException.Code; import com.google.firebase.firestore.Query.Direction; import com.google.firebase.firestore.auth.User; +import com.google.firebase.firestore.core.FirestoreClient; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.testutil.EventAccumulator; import com.google.firebase.firestore.testutil.IntegrationTestUtil; @@ -1132,7 +1133,7 @@ public void testAppDeleteLeadsToFirestoreTerminate() { app.delete(); - assertTrue(instance.getClient().isTerminated()); + assertTrue(instance.callClient(FirestoreClient::isTerminated)); } @Test diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java index b6ec3d61a51..29ca658515e 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/QueryTest.java @@ -536,12 +536,12 @@ public void testQueriesFireFromCacheWhenOffline() { assertFalse(querySnapshot.getMetadata().isFromCache()); // offline event with fromCache=true - waitFor(collection.firestore.getClient().disableNetwork()); + waitFor(collection.firestore.disableNetwork()); querySnapshot = accum.await(); assertTrue(querySnapshot.getMetadata().isFromCache()); // back online event with fromCache=false - waitFor(collection.firestore.getClient().enableNetwork()); + waitFor(collection.firestore.enableNetwork()); querySnapshot = accum.await(); assertFalse(querySnapshot.getMetadata().isFromCache()); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ServerTimestampTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ServerTimestampTest.java index 4fcc6f4f6f4..fb38935ddc2 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ServerTimestampTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ServerTimestampTest.java @@ -214,7 +214,7 @@ public void testServerTimestampsCanReturnPreviousValueOfDifferentType() { @Test public void testServerTimestampsCanRetainPreviousValueThroughConsecutiveUpdates() { writeInitialData(); - waitFor(docRef.getFirestore().getClient().disableNetwork()); + waitFor(docRef.getFirestore().disableNetwork()); accumulator.awaitRemoteEvent(); docRef.update("a", FieldValue.serverTimestamp()); @@ -226,7 +226,7 @@ public void testServerTimestampsCanRetainPreviousValueThroughConsecutiveUpdates( localSnapshot = accumulator.awaitLocalEvent(); assertEquals(42L, localSnapshot.get("a", ServerTimestampBehavior.PREVIOUS)); - waitFor(docRef.getFirestore().getClient().enableNetwork()); + waitFor(docRef.getFirestore().enableNetwork()); DocumentSnapshot remoteSnapshot = accumulator.awaitRemoteEvent(); assertThat(remoteSnapshot.get("a")).isInstanceOf(Timestamp.class); @@ -235,7 +235,7 @@ public void testServerTimestampsCanRetainPreviousValueThroughConsecutiveUpdates( @Test public void testServerTimestampsUsesPreviousValueFromLocalMutation() { writeInitialData(); - waitFor(docRef.getFirestore().getClient().disableNetwork()); + waitFor(docRef.getFirestore().disableNetwork()); accumulator.awaitRemoteEvent(); docRef.update("a", FieldValue.serverTimestamp()); @@ -249,7 +249,7 @@ public void testServerTimestampsUsesPreviousValueFromLocalMutation() { localSnapshot = accumulator.awaitLocalEvent(); assertEquals(1337L, localSnapshot.get("a", ServerTimestampBehavior.PREVIOUS)); - waitFor(docRef.getFirestore().getClient().enableNetwork()); + waitFor(docRef.getFirestore().enableNetwork()); DocumentSnapshot remoteSnapshot = accumulator.awaitRemoteEvent(); assertThat(remoteSnapshot.get("a")).isInstanceOf(Timestamp.class); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java index f8efddd7893..a1997597733 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java @@ -45,6 +45,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.firestore.Transaction.Function; +import com.google.firebase.firestore.core.FirestoreClient; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import com.google.firebase.firestore.util.Consumer; import java.util.Arrays; @@ -453,7 +454,7 @@ public void queriesCannotBeSortedByAnUncommittedServerTimestamp() { CollectionReference collection = testCollection(); // Ensure the server timestamp stays uncommitted for the first half of the test - waitFor(collection.firestore.getClient().disableNetwork()); + waitFor(collection.firestore.callClient(FirestoreClient::disableNetwork)); TaskCompletionSource offlineCallbackDone = new TaskCompletionSource<>(); TaskCompletionSource onlineCallbackDone = new TaskCompletionSource<>(); @@ -497,7 +498,7 @@ public void queriesCannotBeSortedByAnUncommittedServerTimestamp() { document.set(map("timestamp", FieldValue.serverTimestamp())); waitFor(offlineCallbackDone.getTask()); - waitFor(collection.firestore.getClient().enableNetwork()); + waitFor(collection.firestore.enableNetwork()); waitFor(onlineCallbackDone.getTask()); listenerRegistration.remove(); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java index cb52365683f..7691cb03d9c 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java @@ -30,6 +30,8 @@ import com.google.firebase.firestore.testutil.IntegrationTestUtil; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Consumer; +import com.google.protobuf.ByteString; + import io.grpc.Status; import java.util.concurrent.Semaphore; import org.junit.Test; @@ -68,13 +70,13 @@ public void handleOnlineStateChange(OnlineState onlineState) { networkChangeSemaphore.release(); } - @Override - public void clearCacheData() {} - @Override public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { return null; } + + @Override + public void handleClearPersistence(ByteString sessionToken) {} }; FakeConnectivityMonitor connectivityMonitor = new FakeConnectivityMonitor(); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java index a1dc9d9b771..d8f08deb654 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java @@ -37,6 +37,7 @@ import com.google.firebase.firestore.testutil.IntegrationTestUtil; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.AsyncQueue.TimerId; +import com.google.firestore.v1.InitResponse; import com.google.protobuf.ByteString; import io.grpc.Status; @@ -78,14 +79,25 @@ private static class StreamStatusCallback implements WatchStream.Callback, Write final Semaphore openSemaphore = new Semaphore(0); final Semaphore closeSemaphore = new Semaphore(0); final Semaphore watchChangeSemaphore = new Semaphore(0); + final Semaphore handshakeReadySemaphore = new Semaphore(0); final Semaphore handshakeSemaphore = new Semaphore(0); final Semaphore responseReceivedSemaphore = new Semaphore(0); + @Override + public void onHandshake(InitResponse initResponse) { + handshakeSemaphore.release(); + } + @Override public void onWatchChange(SnapshotVersion snapshotVersion, WatchChange watchChange) { watchChangeSemaphore.release(); } + @Override + public void onHandshakeReady() { + handshakeReadySemaphore.release(); + } + @Override public void onOpen() { openSemaphore.release(); @@ -96,11 +108,6 @@ public void onClose(Status status) { closeSemaphore.release(); } - @Override - public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { - handshakeSemaphore.release(); - } - @Override public void onWriteResponse( SnapshotVersion commitVersion, List mutationResults) { @@ -172,9 +179,9 @@ public void testWriteStreamStopAfterHandshake() throws Exception { StreamStatusCallback streamCallback = new StreamStatusCallback() { @Override - public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { + public void onHandshake(InitResponse initResponse) { assertThat(writeStreamWrapper[0].getLastStreamToken()).isNotEmpty(); - super.onHandshakeComplete(dbToken, clearCache); + super.onHandshake(initResponse); } @Override diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index 0cde1711910..b43b64cd121 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -305,16 +305,13 @@ public static FirebaseFirestore testFirestore( ensureStrictMode(); - AsyncQueue asyncQueue = new AsyncQueue(); - FirebaseFirestore firestore = AccessHelper.newFirebaseFirestore( context, databaseId, persistenceKey, - MockCredentialsProvider.instance(), - new EmptyAppCheckTokenProvider(), - asyncQueue, + () -> MockCredentialsProvider.instance(), + () -> new EmptyAppCheckTokenProvider(), /*firebaseApp=*/ null, /*instanceRegistry=*/ (dbId) -> {}); waitFor(firestore.clearPersistence()); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java index db4015d6386..41e48180cb9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/AggregateQuery.java @@ -64,10 +64,7 @@ public List getAggregateFields() { public Task get(@NonNull AggregateSource source) { Preconditions.checkNotNull(source, "AggregateSource must not be null"); TaskCompletionSource tcs = new TaskCompletionSource<>(); - query - .firestore - .getClient() - .runAggregateQuery(query.query, aggregateFieldList) + query.firestore.callClient(client -> client.runAggregateQuery(query.query, aggregateFieldList)) .continueWith( Executors.DIRECT_EXECUTOR, (task) -> { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java index 3b32b84b383..9e035f2fb53 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java @@ -26,11 +26,8 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.firestore.FirebaseFirestoreException.Code; -import com.google.firebase.firestore.core.ActivityScope; import com.google.firebase.firestore.core.AsyncEventListener; import com.google.firebase.firestore.core.EventManager.ListenOptions; -import com.google.firebase.firestore.core.ListenerRegistrationImpl; -import com.google.firebase.firestore.core.QueryListener; import com.google.firebase.firestore.core.UserData.ParsedSetData; import com.google.firebase.firestore.core.UserData.ParsedUpdateData; import com.google.firebase.firestore.core.ViewSnapshot; @@ -38,11 +35,13 @@ import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.ResourcePath; import com.google.firebase.firestore.model.mutation.DeleteMutation; +import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.model.mutation.Precondition; import com.google.firebase.firestore.util.Assert; import com.google.firebase.firestore.util.Executors; import com.google.firebase.firestore.util.Util; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; @@ -165,9 +164,8 @@ public Task set(@NonNull Object data, @NonNull SetOptions options) { options.isMerge() ? firestore.getUserDataReader().parseMergeData(data, options.getFieldMask()) : firestore.getUserDataReader().parseSetData(data); - return firestore - .getClient() - .write(Collections.singletonList(parsed.toMutation(key, Precondition.NONE))) + List mutations = singletonList(parsed.toMutation(key, Precondition.NONE)); + return firestore.callClient(client -> client.write(mutations)) .continueWith(Executors.DIRECT_EXECUTOR, voidErrorTransformer()); } @@ -229,9 +227,7 @@ public Task update( } private Task update(@NonNull ParsedUpdateData parsedData) { - return firestore - .getClient() - .write(Collections.singletonList(parsedData.toMutation(key, Precondition.exists(true)))) + return firestore.callClient(client -> client.write(singletonList(parsedData.toMutation(key, Precondition.exists(true))))) .continueWith(Executors.DIRECT_EXECUTOR, voidErrorTransformer()); } @@ -242,9 +238,8 @@ private Task update(@NonNull ParsedUpdateData parsedData) { */ @NonNull public Task delete() { - return firestore - .getClient() - .write(singletonList(new DeleteMutation(key, Precondition.NONE))) + List mutations = singletonList(new DeleteMutation(key, Precondition.NONE)); + return firestore.callClient(client -> client.write(mutations)) .continueWith(Executors.DIRECT_EXECUTOR, voidErrorTransformer()); } @@ -273,9 +268,7 @@ public Task get() { @NonNull public Task get(@NonNull Source source) { if (source == Source.CACHE) { - return firestore - .getClient() - .getDocumentFromLocalCache(key) + return firestore.callClient(client -> client.getDocumentFromLocalCache(key)) .continueWith( Executors.DIRECT_EXECUTOR, (Task task) -> { @@ -530,12 +523,7 @@ private ListenerRegistration addSnapshotListenerInternal( AsyncEventListener asyncListener = new AsyncEventListener<>(userExecutor, viewListener); - com.google.firebase.firestore.core.Query query = asQuery(); - QueryListener queryListener = firestore.getClient().listen(query, options, asyncListener); - - return ActivityScope.bind( - activity, - new ListenerRegistrationImpl(firestore.getClient(), queryListener, asyncListener)); + return firestore.callClient(client -> client.listen(asQuery(), options, activity, asyncListener)); } @Override diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 3af05a167ab..4580883e8e9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -24,24 +24,19 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import androidx.core.util.Supplier; import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.annotations.PreviewApi; import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider; import com.google.firebase.auth.internal.InternalAuthProvider; -import com.google.firebase.emulators.EmulatedServiceSettings; -import com.google.firebase.firestore.FirebaseFirestoreException.Code; import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.FirebaseAppCheckTokenProvider; import com.google.firebase.firestore.auth.FirebaseAuthCredentialsProvider; import com.google.firebase.firestore.auth.User; -import com.google.firebase.firestore.core.ActivityScope; import com.google.firebase.firestore.core.AsyncEventListener; -import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.core.FirestoreClient; -import com.google.firebase.firestore.local.SQLitePersistence; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.FieldIndex; import com.google.firebase.firestore.model.FieldPath; @@ -85,24 +80,15 @@ public interface InstanceRegistry { void remove(@NonNull String databaseId); } - private static final String TAG = "FirebaseFirestore"; - private final Context context; // This is also used as private lock object for this instance. There is nothing inherent about // databaseId itself that needs locking; it just saves us creating a separate lock object. private final DatabaseId databaseId; - private final String persistenceKey; - private final CredentialsProvider authProvider; - private final CredentialsProvider appCheckProvider; - private final AsyncQueue asyncQueue; private final FirebaseApp firebaseApp; private final UserDataReader userDataReader; // When user requests to terminate, use this to notify `FirestoreMultiDbComponent` to deregister // this instance. private final InstanceRegistry instanceRegistry; - @Nullable private EmulatedServiceSettings emulatorSettings; - private FirebaseFirestoreSettings settings; - private volatile FirestoreClient client; - private final GrpcMetadataProvider metadataProvider; + private final FirestoreClientProvider client; @Nullable private PersistentCacheIndexManager persistentCacheIndexManager; @@ -192,13 +178,6 @@ static FirebaseFirestore newInstance( } DatabaseId databaseId = DatabaseId.forDatabase(projectId, database); - AsyncQueue queue = new AsyncQueue(); - - CredentialsProvider authProvider = - new FirebaseAuthCredentialsProvider(deferredAuthProvider); - CredentialsProvider appCheckProvider = - new FirebaseAppCheckTokenProvider(deferredAppCheckTokenProvider); - // Firestore uses a different database for each app name. Note that we don't use // app.getPersistenceKey() here because it includes the application ID which is related // to the project ID. We already include the project ID when resolving the database, @@ -210,45 +189,38 @@ static FirebaseFirestore newInstance( context, databaseId, persistenceKey, - authProvider, - appCheckProvider, - queue, + () -> new FirebaseAuthCredentialsProvider(deferredAuthProvider), + () -> new FirebaseAppCheckTokenProvider(deferredAppCheckTokenProvider), app, instanceRegistry, metadataProvider); return firestore; } + + @VisibleForTesting FirebaseFirestore( Context context, DatabaseId databaseId, - String persistenceKey, - CredentialsProvider authProvider, - CredentialsProvider appCheckProvider, - AsyncQueue asyncQueue, + @NonNull String persistenceKey, + @NonNull Supplier> authProviderFactory, + @NonNull Supplier> appCheckTokenProviderFactory, @Nullable FirebaseApp firebaseApp, InstanceRegistry instanceRegistry, @Nullable GrpcMetadataProvider metadataProvider) { - this.context = checkNotNull(context); + this.client = new FirestoreClientProvider(context, databaseId, persistenceKey, authProviderFactory, appCheckTokenProviderFactory, metadataProvider); this.databaseId = checkNotNull(checkNotNull(databaseId)); this.userDataReader = new UserDataReader(databaseId); - this.persistenceKey = checkNotNull(persistenceKey); - this.authProvider = checkNotNull(authProvider); - this.appCheckProvider = checkNotNull(appCheckProvider); - this.asyncQueue = checkNotNull(asyncQueue); // NOTE: We allow firebaseApp to be null in tests only. this.firebaseApp = firebaseApp; this.instanceRegistry = instanceRegistry; - this.metadataProvider = metadataProvider; - - this.settings = new FirebaseFirestoreSettings.Builder().build(); } /** Returns the settings used by this {@code FirebaseFirestore} object. */ @NonNull public FirebaseFirestoreSettings getFirestoreSettings() { - return settings; + return client.getFirestoreSettings(); } /** @@ -256,22 +228,7 @@ public FirebaseFirestoreSettings getFirestoreSettings() { * can only be called before calling any other methods on this object. */ public void setFirestoreSettings(@NonNull FirebaseFirestoreSettings settings) { - settings = mergeEmulatorSettings(settings, this.emulatorSettings); - - synchronized (databaseId) { - checkNotNull(settings, "Provided settings must not be null."); - - // As a special exception, don't throw if the same settings are passed repeatedly. This - // should make it simpler to get a Firestore instance in an activity. - if (client != null && !this.settings.equals(settings)) { - throw new IllegalStateException( - "FirebaseFirestore has already been started and its settings can no longer be changed. " - + "You can only call setFirestoreSettings() before calling any other methods on a " - + "FirebaseFirestore object."); - } - - this.settings = settings; - } + client.setFirestoreSettings(settings); } /** @@ -283,56 +240,11 @@ public void setFirestoreSettings(@NonNull FirebaseFirestoreSettings settings) { * @param port the emulator port (for example, 8080) */ public void useEmulator(@NonNull String host, int port) { - if (this.client != null) { - throw new IllegalStateException( - "Cannot call useEmulator() after instance has already been initialized."); - } - - this.emulatorSettings = new EmulatedServiceSettings(host, port); - this.settings = mergeEmulatorSettings(this.settings, this.emulatorSettings); + client.useEmulator(host, port); } private void ensureClientConfigured() { - if (client != null) { - return; - } - - synchronized (databaseId) { - if (client != null) { - return; - } - DatabaseInfo databaseInfo = - new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled()); - - client = - new FirestoreClient( - context, - databaseInfo, - settings, - authProvider, - appCheckProvider, - asyncQueue, - metadataProvider); - } - } - - private FirebaseFirestoreSettings mergeEmulatorSettings( - @NonNull FirebaseFirestoreSettings settings, - @Nullable EmulatedServiceSettings emulatorSettings) { - if (emulatorSettings == null) { - return settings; - } - - if (!FirebaseFirestoreSettings.DEFAULT_HOST.equals(settings.getHost())) { - Logger.warn( - TAG, - "Host has been set in FirebaseFirestoreSettings and useEmulator, emulator host will be used."); - } - - return new FirebaseFirestoreSettings.Builder(settings) - .setHost(emulatorSettings.getHost() + ":" + emulatorSettings.getPort()) - .setSslEnabled(false) - .build(); + client.ensureClientConfigured(); } /** Returns the FirebaseApp instance to which this {@code FirebaseFirestore} belongs. */ @@ -365,7 +277,7 @@ public FirebaseApp getApp() { public Task setIndexConfiguration(@NonNull String json) { ensureClientConfigured(); Preconditions.checkState( - settings.isPersistenceEnabled(), "Cannot enable indexes when persistence is disabled"); + client.getFirestoreSettings().isPersistenceEnabled(), "Cannot enable indexes when persistence is disabled"); List parsedIndexes = new ArrayList<>(); @@ -406,7 +318,7 @@ public Task setIndexConfiguration(@NonNull String json) { throw new IllegalArgumentException("Failed to parse index configuration", e); } - return client.configureFieldIndexes(parsedIndexes); + return client.safeCall(client -> client.configureFieldIndexes(parsedIndexes)); } /** @@ -422,10 +334,12 @@ public Task setIndexConfiguration(@NonNull String json) { @Nullable public synchronized PersistentCacheIndexManager getPersistentCacheIndexManager() { ensureClientConfigured(); - if (persistentCacheIndexManager == null - && (settings.isPersistenceEnabled() - || settings.getCacheSettings() instanceof PersistentCacheSettings)) { - persistentCacheIndexManager = new PersistentCacheIndexManager(client); + if (persistentCacheIndexManager == null) { + FirebaseFirestoreSettings settings = client.getFirestoreSettings(); + if (settings.isPersistenceEnabled() + || settings.getCacheSettings() instanceof PersistentCacheSettings) { + persistentCacheIndexManager = new PersistentCacheIndexManager(client); + } } return persistentCacheIndexManager; } @@ -511,7 +425,7 @@ private Task runTransaction( updateFunction.apply( new Transaction(internalTransaction, FirebaseFirestore.this))); - return client.transaction(options, wrappedUpdateFunction); + return client.safeCall(client -> client.transaction(options, wrappedUpdateFunction)); } /** @@ -604,9 +518,6 @@ public Task runBatch(@NonNull WriteBatch.Function batchFunction) { @NonNull public Task terminate() { instanceRegistry.remove(this.getDatabaseId().getDatabaseId()); - - // The client must be initialized to ensure that all subsequent API usage throws an exception. - this.ensureClientConfigured(); return client.terminate(); } @@ -626,13 +537,12 @@ public Task terminate() { */ @NonNull public Task waitForPendingWrites() { - ensureClientConfigured(); - return client.waitForPendingWrites(); + return client.safeCall(FirestoreClient::waitForPendingWrites); } @VisibleForTesting AsyncQueue getAsyncQueue() { - return asyncQueue; + return client.getAsyncQueue(); } /** @@ -642,7 +552,6 @@ AsyncQueue getAsyncQueue() { */ @NonNull public Task enableNetwork() { - ensureClientConfigured(); return client.enableNetwork(); } @@ -655,7 +564,6 @@ public Task enableNetwork() { */ @NonNull public Task disableNetwork() { - ensureClientConfigured(); return client.disableNetwork(); } @@ -688,22 +596,7 @@ public static void setLoggingEnabled(boolean loggingEnabled) { */ @NonNull public Task clearPersistence() { - final TaskCompletionSource source = new TaskCompletionSource<>(); - asyncQueue.enqueueAndForgetEvenAfterShutdown( - () -> { - try { - if (client != null && !client.isTerminated()) { - throw new FirebaseFirestoreException( - "Persistence cannot be cleared while the firestore instance is running.", - Code.FAILED_PRECONDITION); - } - SQLitePersistence.clearPersistence(context, databaseId, persistenceKey); - source.setResult(null); - } catch (FirebaseFirestoreException e) { - source.setException(e); - } - }); - return source.getTask(); + return client.clearPersistence(); } /** @@ -776,9 +669,8 @@ public ListenerRegistration addSnapshotsInSyncListener( */ @NonNull public LoadBundleTask loadBundle(@NonNull InputStream bundleData) { - ensureClientConfigured(); LoadBundleTask resultTask = new LoadBundleTask(); - client.loadBundle(bundleData, resultTask); + client.safeCallVoid(client -> client.loadBundle(bundleData, resultTask)); return resultTask; } @@ -816,9 +708,7 @@ public LoadBundleTask loadBundle(@NonNull ByteBuffer bundleData) { // TODO(b/261013682): Use an explicit executor in continuations. @SuppressLint("TaskMainThread") public @NonNull Task getNamedQuery(@NonNull String name) { - ensureClientConfigured(); - return client - .getNamedQuery(name) + return client.safeCall(client -> client.getNamedQuery(name)) .continueWith( task -> { com.google.firebase.firestore.core.Query query = task.getResult(); @@ -843,7 +733,6 @@ public LoadBundleTask loadBundle(@NonNull ByteBuffer bundleData) { */ private ListenerRegistration addSnapshotsInSyncListener( Executor userExecutor, @Nullable Activity activity, @NonNull Runnable runnable) { - ensureClientConfigured(); EventListener eventListener = (Void v, FirebaseFirestoreException error) -> { hardAssert(error == null, "snapshots-in-sync listeners should never get errors."); @@ -851,17 +740,11 @@ private ListenerRegistration addSnapshotsInSyncListener( }; AsyncEventListener asyncListener = new AsyncEventListener(userExecutor, eventListener); - client.addSnapshotsInSyncListener(asyncListener); - return ActivityScope.bind( - activity, - () -> { - asyncListener.mute(); - client.removeSnapshotsInSyncListener(asyncListener); - }); + return client.safeCall(client -> client.addSnapshotsInSyncListener(asyncListener, activity)); } - FirestoreClient getClient() { - return client; + T callClient(com.google.common.base.Function call) { + return client.safeCall(call); } DatabaseId getDatabaseId() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java new file mode 100644 index 00000000000..c2e1554c9b4 --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java @@ -0,0 +1,283 @@ +package com.google.firebase.firestore; + +import static com.google.firebase.firestore.util.Preconditions.checkNotNull; + +import android.content.Context; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Consumer; +import androidx.core.util.Supplier; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.common.base.Function; +import com.google.firebase.emulators.EmulatedServiceSettings; +import com.google.firebase.firestore.auth.CredentialsProvider; +import com.google.firebase.firestore.auth.User; +import com.google.firebase.firestore.core.ComponentProvider; +import com.google.firebase.firestore.core.DatabaseInfo; +import com.google.firebase.firestore.core.FirestoreClient; +import com.google.firebase.firestore.core.MemoryComponentProvider; +import com.google.firebase.firestore.core.SQLiteComponentProvider; +import com.google.firebase.firestore.local.SQLitePersistence; +import com.google.firebase.firestore.model.DatabaseId; +import com.google.firebase.firestore.remote.Datastore; +import com.google.firebase.firestore.remote.GrpcMetadataProvider; +import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firebase.firestore.util.Logger; +import com.google.protobuf.ByteString; + +import java.util.concurrent.Executor; + +/** + * The FirestoreClientProvider handles the life cycle of FirestoreClients. + * + * Coupling to FirestoreClient should go through the {@link FirestoreClientProvider#get()} + * method. The returned FirestoreClient can change over time if there is an event that requires + * restarting the FirestoreClient internally. + */ +final class FirestoreClientProvider { + + private final Context context; + private final DatabaseId databaseId; + private final String persistenceKey; + private final GrpcMetadataProvider metadataProvider; + private final Supplier> authProviderFactory; + private final Supplier> appCheckTokenProviderFactory; + + @GuardedBy("this") + private FirestoreClient client; + private volatile AsyncQueue asyncQueue; + + @GuardedBy("this") + private FirebaseFirestoreSettings settings; + + @GuardedBy("this") + private boolean networkEnabled = true; + + @Nullable private EmulatedServiceSettings emulatorSettings; + private ByteString sessionToken; + + FirestoreClientProvider( + Context context, + DatabaseId databaseId, + @NonNull String persistenceKey, + @NonNull Supplier> authProviderFactory, + @NonNull Supplier> appCheckTokenProviderFactory, + @Nullable GrpcMetadataProvider metadataProvider) { + this.context = checkNotNull(context); + this.databaseId = checkNotNull(checkNotNull(databaseId)); + this.persistenceKey = checkNotNull(persistenceKey); + this.authProviderFactory = checkNotNull(authProviderFactory); + this.appCheckTokenProviderFactory = checkNotNull(appCheckTokenProviderFactory); + this.metadataProvider = metadataProvider; + this.settings = new FirebaseFirestoreSettings.Builder().build(); + this.asyncQueue = new AsyncQueue(); + } + + synchronized FirebaseFirestoreSettings getFirestoreSettings() { + return settings; + } + + synchronized void setFirestoreSettings(@NonNull FirebaseFirestoreSettings settings) { + settings = mergeEmulatorSettings(settings, this.emulatorSettings); + + checkNotNull(settings, "Provided settings must not be null."); + + // As a special exception, don't throw if the same settings are passed repeatedly. This + // should make it simpler to get a Firestore instance in an activity. + if (client != null && !this.settings.equals(settings)) { + throw new IllegalStateException( + "FirebaseFirestore has already been started and its settings can no longer be changed. " + + "You can only call setFirestoreSettings() before calling any other methods on a " + + "FirebaseFirestore object."); + } + + this.settings = settings; + } + + /** + * Modifies this FirebaseDatabase instance to communicate with the Cloud Firestore emulator. + * + *

Note: Call this method before using the instance to do any database operations. + * + * @param host the emulator host (for example, 10.0.2.2) + * @param port the emulator port (for example, 8080) + */ + synchronized void useEmulator(@NonNull String host, int port) { + if (client != null) { + throw new IllegalStateException( + "Cannot call useEmulator() after instance has already been initialized."); + } + + this.emulatorSettings = new EmulatedServiceSettings(host, port); + this.settings = mergeEmulatorSettings(this.settings, this.emulatorSettings); + } + + /** + * Sets any custom settings used to configure this {@code FirebaseFirestore} object. This method + * can only be called before calling any other methods on this object. + */ + private static FirebaseFirestoreSettings mergeEmulatorSettings( + @NonNull FirebaseFirestoreSettings settings, + @Nullable EmulatedServiceSettings emulatorSettings) { + if (emulatorSettings == null) { + return settings; + } + + if (!FirebaseFirestoreSettings.DEFAULT_HOST.equals(settings.getHost())) { + Logger.warn( + "FirestoreClientProvider", + "Host has been set in FirebaseFirestoreSettings and useEmulator, emulator host will be used."); + } + + return new FirebaseFirestoreSettings.Builder(settings) + .setHost(emulatorSettings.getHost() + ":" + emulatorSettings.getPort()) + .setSslEnabled(false) + .build(); + } + + private synchronized FirestoreClient newClient() { + DatabaseInfo databaseInfo = + new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled()); + + CredentialsProvider authProvider = authProviderFactory.get(); + CredentialsProvider appCheckProvider = appCheckTokenProviderFactory.get(); + FirestoreClient client = new FirestoreClient( + databaseInfo, + settings, + authProvider, + appCheckProvider, + asyncQueue); + + Datastore datastore = new Datastore( + databaseInfo, asyncQueue, authProvider, appCheckProvider, context, metadataProvider); + + client.setClearPersistenceCallback(sessionToken -> { + synchronized (this) { + // If this callback is attached to an old FirestoreClient, we want to ignore it. + if (this.client == client) { + this.sessionToken = sessionToken; + clearPersistence(); + } + } + }); + + ComponentProvider componentProvider = settings.isPersistenceEnabled() + ? new SQLiteComponentProvider() + : new MemoryComponentProvider(); + + client.start( + context, + componentProvider, + datastore); + + if (sessionToken != null) { + client.setSessionToken(sessionToken); + } + + if (networkEnabled) { + client.enableNetwork(); + } + + return client; + } + + @NonNull + private synchronized ComponentProvider getComponentProvider() { + return settings.isPersistenceEnabled() + ? new SQLiteComponentProvider() + : new MemoryComponentProvider(); + } + + + void ensureClientConfigured() { + if (client == null) { + ensureClientConfiguredInternal(); + } + } + + synchronized private void ensureClientConfiguredInternal() { + if (client == null) { + client = newClient(); + } + } + + /** + * To facilitate calls to FirestoreClient without risk of FirestoreClient being terminated + * ir restarted by {@link #clearPersistence()} mid call. + */ + synchronized T safeCall(Function call) { + ensureClientConfiguredInternal(); + return call.apply(client); + } + + /** + * To facilitate calls to FirestoreClient without risk of FirestoreClient being terminated + * ir restarted by {@link #clearPersistence()} mid call. + */ + synchronized void safeCallVoid(Consumer call) { + ensureClientConfiguredInternal(); + call.accept(client); + } + + /** + * Shuts down the AsyncQueue and releases resources after which no progress will ever be made + * again. + */ + synchronized Task terminate() { + // The client must be initialized to ensure that all subsequent API usage throws an exception. + ensureClientConfiguredInternal(); + + Task terminate = client.terminate(); + + // Will cause the executor to de-reference all threads, the best we can do + asyncQueue.terminate(); + + return terminate; + } + + synchronized Task clearPersistence() { + // This will block asyncQueue, prevent a new client from being started. + if (client == null || client.isTerminated()) { + return clearPersistence(asyncQueue.getExecutor()); + } else { + client.terminate(); + asyncQueue = asyncQueue.reincarnate(); + Task task = clearPersistence(asyncQueue.getExecutor()); + client = newClient(); + return task; + } + } + + private Task clearPersistence(Executor executor) { + final TaskCompletionSource source = new TaskCompletionSource<>(); + executor.execute(() -> { + try { + SQLitePersistence.clearPersistence(context, databaseId, persistenceKey); + source.setResult(null); + } catch (FirebaseFirestoreException e) { + source.setException(e); + } + }); + return source.getTask(); + } + + AsyncQueue getAsyncQueue() { + return asyncQueue; + } + + synchronized Task enableNetwork() { + networkEnabled = true; + ensureClientConfiguredInternal(); + return client.enableNetwork(); + } + + synchronized Task disableNetwork() { + networkEnabled = false; + ensureClientConfiguredInternal(); + return client.disableNetwork(); + } +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/PersistentCacheIndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/PersistentCacheIndexManager.java index a529acbc8bc..80c3fdeadd2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/PersistentCacheIndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/PersistentCacheIndexManager.java @@ -28,11 +28,11 @@ *

To get an instance, call {@link FirebaseFirestore#getPersistentCacheIndexManager()}. */ public final class PersistentCacheIndexManager { - @NonNull private FirestoreClient client; + @NonNull private FirestoreClientProvider client; /** @hide */ @RestrictTo(RestrictTo.Scope.LIBRARY) - PersistentCacheIndexManager(FirestoreClient client) { + PersistentCacheIndexManager(FirestoreClientProvider client) { this.client = client; } @@ -43,7 +43,7 @@ public final class PersistentCacheIndexManager { *

This feature is disabled by default. */ public void enableIndexAutoCreation() { - client.setIndexAutoCreationEnabled(true); + client.safeCallVoid(client -> client.setIndexAutoCreationEnabled(true)); } /** @@ -51,7 +51,7 @@ public void enableIndexAutoCreation() { * which have been created by calling {@link #enableIndexAutoCreation()} still take effect. */ public void disableIndexAutoCreation() { - client.setIndexAutoCreationEnabled(false); + client.safeCallVoid(client -> client.setIndexAutoCreationEnabled(false)); } /** @@ -59,6 +59,6 @@ public void disableIndexAutoCreation() { * {@link FirebaseFirestore#setIndexConfiguration(String)}, which is deprecated. */ public void deleteAllIndexes() { - client.deleteAllFieldIndexes(); + client.safeCallVoid(FirestoreClient::deleteAllFieldIndexes); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java index a0e709a54e0..03801447e68 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/Query.java @@ -26,16 +26,13 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.firestore.FirebaseFirestoreException.Code; -import com.google.firebase.firestore.core.ActivityScope; import com.google.firebase.firestore.core.AsyncEventListener; import com.google.firebase.firestore.core.Bound; import com.google.firebase.firestore.core.CompositeFilter; import com.google.firebase.firestore.core.EventManager.ListenOptions; import com.google.firebase.firestore.core.FieldFilter; import com.google.firebase.firestore.core.FieldFilter.Operator; -import com.google.firebase.firestore.core.ListenerRegistrationImpl; import com.google.firebase.firestore.core.OrderBy; -import com.google.firebase.firestore.core.QueryListener; import com.google.firebase.firestore.core.ViewSnapshot; import com.google.firebase.firestore.model.Document; import com.google.firebase.firestore.model.DocumentKey; @@ -964,9 +961,7 @@ public Task get() { public Task get(@NonNull Source source) { validateHasExplicitOrderByForLimitToLast(); if (source == Source.CACHE) { - return firestore - .getClient() - .getDocumentsFromLocalCache(query) + return firestore.callClient(client -> client.getDocumentsFromLocalCache(query)) .continueWith( Executors.DIRECT_EXECUTOR, (Task viewSnap) -> @@ -1182,10 +1177,7 @@ private ListenerRegistration addSnapshotListenerInternal( AsyncEventListener asyncListener = new AsyncEventListener<>(executor, viewListener); - QueryListener queryListener = firestore.getClient().listen(query, options, asyncListener); - return ActivityScope.bind( - activity, - new ListenerRegistrationImpl(firestore.getClient(), queryListener, asyncListener)); + return firestore.callClient(client -> client.listen(query, options, activity, asyncListener)); } private void validateHasExplicitOrderByForLimitToLast() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/WriteBatch.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/WriteBatch.java index b8c6e9d1651..af886e759e0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/WriteBatch.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/WriteBatch.java @@ -190,8 +190,8 @@ public WriteBatch delete(@NonNull DocumentReference documentRef) { public Task commit() { verifyNotCommitted(); committed = true; - if (mutations.size() > 0) { - return firestore.getClient().write(mutations); + if (!mutations.isEmpty()) { + return firestore.callClient(client -> client.write(mutations)); } else { return Tasks.forResult(null); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java index e63af145978..a71e3c4ba3b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java @@ -28,6 +28,8 @@ import com.google.firebase.firestore.remote.Datastore; import com.google.firebase.firestore.remote.RemoteStore; import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firebase.firestore.util.Consumer; +import com.google.protobuf.ByteString; /** * Initializes and wires up all core components for Firestore. @@ -55,6 +57,7 @@ public static class Configuration { private final User initialUser; private final int maxConcurrentLimboResolutions; private final FirebaseFirestoreSettings settings; + private final Consumer clearPersistenceCallback; public Configuration( Context context, @@ -63,7 +66,8 @@ public Configuration( Datastore datastore, User initialUser, int maxConcurrentLimboResolutions, - FirebaseFirestoreSettings settings) { + FirebaseFirestoreSettings settings, + Consumer clearPersistenceCallback) { this.context = context; this.asyncQueue = asyncQueue; this.databaseInfo = databaseInfo; @@ -71,6 +75,7 @@ public Configuration( this.initialUser = initialUser; this.maxConcurrentLimboResolutions = maxConcurrentLimboResolutions; this.settings = settings; + this.clearPersistenceCallback = clearPersistenceCallback; } FirebaseFirestoreSettings getSettings() { @@ -100,6 +105,10 @@ int getMaxConcurrentLimboResolutions() { Context getContext() { return context; } + + public Consumer getClearPersistenceCallback() { + return clearPersistenceCallback; + } } public Persistence getPersistence() { @@ -154,7 +163,6 @@ public void initialize(Configuration configuration) { syncEngine = createSyncEngine(configuration); eventManager = createEventManager(configuration); localStore.start(); - remoteStore.start(); garbageCollectionScheduler = createGarbageCollectionScheduler(configuration); indexBackfiller = createIndexBackfiller(configuration); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java index 8eb99124475..dea399b82a1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java @@ -16,8 +16,6 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; -import androidx.annotation.VisibleForTesting; - import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.ListenSource; import com.google.firebase.firestore.core.SyncEngine.SyncEngineCallback; @@ -268,9 +266,12 @@ public void handleOnlineStateChange(OnlineState onlineState) { } } - @VisibleForTesting - public boolean isEmpty() { - return queries.isEmpty() - && snapshotsInSyncListeners.isEmpty(); + public void abortAllTargets() { + for (QueryListenersInfo info : queries.values()) { + for (QueryListener listener : info.listeners) { + listener.onError(Util.exceptionFromStatus(Status.ABORTED)); + } + } + queries.clear(); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index 38b0adef065..a8c29ad5478 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -17,6 +17,7 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.Context; import androidx.annotation.Nullable; import com.google.android.gms.tasks.Task; @@ -27,6 +28,7 @@ import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.FirebaseFirestoreException.Code; import com.google.firebase.firestore.FirebaseFirestoreSettings; +import com.google.firebase.firestore.ListenerRegistration; import com.google.firebase.firestore.LoadBundleTask; import com.google.firebase.firestore.TransactionOptions; import com.google.firebase.firestore.auth.CredentialsProvider; @@ -45,13 +47,15 @@ import com.google.firebase.firestore.model.FieldIndex; import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.remote.Datastore; -import com.google.firebase.firestore.remote.GrpcMetadataProvider; import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.remote.RemoteStore; import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firebase.firestore.util.Consumer; import com.google.firebase.firestore.util.Function; import com.google.firebase.firestore.util.Logger; import com.google.firestore.v1.Value; +import com.google.protobuf.ByteString; + import java.io.InputStream; import java.util.List; import java.util.Map; @@ -72,33 +76,40 @@ public final class FirestoreClient { private final CredentialsProvider appCheckProvider; private final AsyncQueue asyncQueue; private final BundleSerializer bundleSerializer; - private final GrpcMetadataProvider metadataProvider; + private final FirebaseFirestoreSettings settings; private Persistence persistence; private LocalStore localStore; private RemoteStore remoteStore; private SyncEngine syncEngine; private EventManager eventManager; + private Consumer clearPersistenceCallback; // LRU-related @Nullable private Scheduler indexBackfillScheduler; @Nullable private Scheduler gcScheduler; public FirestoreClient( - final Context context, DatabaseInfo databaseInfo, FirebaseFirestoreSettings settings, CredentialsProvider authProvider, CredentialsProvider appCheckProvider, - final AsyncQueue asyncQueue, - @Nullable GrpcMetadataProvider metadataProvider) { + AsyncQueue asyncQueue) { this.databaseInfo = databaseInfo; + this.settings = settings; this.authProvider = authProvider; this.appCheckProvider = appCheckProvider; this.asyncQueue = asyncQueue; - this.metadataProvider = metadataProvider; this.bundleSerializer = - new BundleSerializer(new RemoteSerializer(databaseInfo.getDatabaseId())); + new BundleSerializer(new RemoteSerializer(databaseInfo.getDatabaseId())); + } + + public void start( + Context context, + ComponentProvider provider, + Datastore datastore) { + + asyncQueue.setOnShutdown(this::onAsyncQueueShutdown); TaskCompletionSource firstUser = new TaskCompletionSource<>(); final AtomicBoolean initialized = new AtomicBoolean(false); @@ -111,7 +122,19 @@ public FirestoreClient( try { // Block on initial user being available User initialUser = Tasks.await(firstUser.getTask()); - initialize(context, initialUser, settings); + Logger.debug(LOG_TAG, "Initializing. user=%s", initialUser.getUid()); + ComponentProvider.Configuration configuration = + new ComponentProvider.Configuration( + context, + asyncQueue, + databaseInfo, + datastore, + initialUser, + MAX_CONCURRENT_LIMBO_RESOLUTIONS, + settings, + clearPersistenceCallback); + provider.initialize(configuration); + initialize(provider); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -139,6 +162,21 @@ public FirestoreClient( }); } + public void setClearPersistenceCallback(Consumer clearPersistenceCallback) { + this.clearPersistenceCallback = clearPersistenceCallback; + } + + private void onAsyncQueueShutdown() { + remoteStore.shutdown(); + persistence.shutdown(); + if (gcScheduler != null) { + gcScheduler.stop(); + } + if (indexBackfillScheduler != null) { + indexBackfillScheduler.stop(); + } + } + public Task disableNetwork() { this.verifyNotTerminated(); return asyncQueue.enqueue(() -> remoteStore.disableNetwork()); @@ -153,17 +191,8 @@ public Task enableNetwork() { public Task terminate() { authProvider.removeChangeListener(); appCheckProvider.removeChangeListener(); - return asyncQueue.enqueueAndInitiateShutdown( - () -> { - remoteStore.shutdown(); - persistence.shutdown(); - if (gcScheduler != null) { - gcScheduler.stop(); - } - if (indexBackfillScheduler != null) { - indexBackfillScheduler.stop(); - } - }); + asyncQueue.enqueueAndForget(() -> eventManager.abortAllTargets()); + return asyncQueue.shutdown(); } /** Returns true if this client has been terminated. */ @@ -174,12 +203,15 @@ public boolean isTerminated() { } /** Starts listening to a query. */ - public QueryListener listen( - Query query, ListenOptions options, EventListener listener) { + public ListenerRegistration listen( + Query query, ListenOptions options, @Nullable Activity activity, AsyncEventListener listener) { this.verifyNotTerminated(); QueryListener queryListener = new QueryListener(query, options, listener); asyncQueue.enqueueAndForget(() -> eventManager.addQueryListener(queryListener)); - return queryListener; + + return ActivityScope.bind( + activity, + new ListenerRegistrationImpl(this, queryListener, listener)); } /** Stops listening to a query previously listened to. */ @@ -234,13 +266,25 @@ public Task write(final List mutations) { return source.getTask(); } - /** Tries to execute the transaction in updateFunction. */ + /** + * Takes an updateFunction in which a set of reads and writes can be performed atomically. In the + * updateFunction, the client can read and write values using the supplied transaction object. + * After the updateFunction, all changes will be committed. If a retryable error occurs (ex: some + * other client has changed any of the data referenced), then the updateFunction will be called + * again after a backoff. If the updateFunction still fails after all retries, then the + * transaction will be rejected. + * + *

The transaction object passed to the updateFunction contains methods for accessing documents + * and collections. Unlike other datastore access, data accessed with the transaction will not + * reflect local changes that have not been committed. For this reason, it is required that all + * reads are performed before any writes. Transactions must be performed while online. + * + *

The Task returned is resolved when the transaction is fully committed. + */ public Task transaction( TransactionOptions options, Function> updateFunction) { this.verifyNotTerminated(); - return AsyncQueue.callTask( - asyncQueue.getExecutor(), - () -> syncEngine.transaction(asyncQueue, options, updateFunction)); + return new TransactionRunner<>(asyncQueue, remoteStore, options, updateFunction).run(); } // TODO(b/261013682): Use an explicit executor in continuations. @@ -251,7 +295,7 @@ public Task> runAggregateQuery( final TaskCompletionSource> result = new TaskCompletionSource<>(); asyncQueue.enqueueAndForget( () -> - syncEngine + remoteStore .runAggregateQuery(query, aggregateFields) .addOnSuccessListener(data -> result.setResult(data)) .addOnFailureListener(e -> result.setException(e))); @@ -270,30 +314,10 @@ public Task waitForPendingWrites() { return source.getTask(); } - private void initialize(Context context, User user, FirebaseFirestoreSettings settings) { + private void initialize(ComponentProvider provider) { // Note: The initialization work must all be synchronous (we can't dispatch more work) since // external write/listen operations could get queued to run before that subsequent work // completes. - Logger.debug(LOG_TAG, "Initializing. user=%s", user.getUid()); - - Datastore datastore = - new Datastore( - databaseInfo, asyncQueue, authProvider, appCheckProvider, context, metadataProvider); - ComponentProvider.Configuration configuration = - new ComponentProvider.Configuration( - context, - asyncQueue, - databaseInfo, - datastore, - user, - MAX_CONCURRENT_LIMBO_RESOLUTIONS, - settings); - - ComponentProvider provider = - settings.isPersistenceEnabled() - ? new SQLiteComponentProvider() - : new MemoryComponentProvider(); - provider.initialize(configuration); persistence = provider.getPersistence(); gcScheduler = provider.getGarbageCollectionScheduler(); localStore = provider.getLocalStore(); @@ -312,9 +336,15 @@ private void initialize(Context context, User user, FirebaseFirestoreSettings se } } - public void addSnapshotsInSyncListener(EventListener listener) { + public ListenerRegistration addSnapshotsInSyncListener(AsyncEventListener listener, Activity activity) { verifyNotTerminated(); asyncQueue.enqueueAndForget(() -> eventManager.addSnapshotsInSyncListener(listener)); + return ActivityScope.bind( + activity, + () -> { + listener.mute(); + removeSnapshotsInSyncListener(listener); + }); } public void loadBundle(InputStream bundleData, LoadBundleTask resultTask) { @@ -376,4 +406,14 @@ private void verifyNotTerminated() { throw new IllegalStateException("The client has already been terminated"); } } + + public AsyncQueue getAsyncQueue() { + verifyNotTerminated(); + return asyncQueue; + } + + public void setSessionToken(ByteString sessionToken) { + verifyNotTerminated(); + asyncQueue.enqueueAndForget(() -> localStore.setSessionsToken(sessionToken)); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java index af95fca3e69..de7f089cb4f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java @@ -33,6 +33,8 @@ import com.google.firebase.firestore.remote.RemoteEvent; import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.remote.RemoteStore; +import com.google.protobuf.ByteString; + import io.grpc.Status; /** @@ -109,7 +111,8 @@ protected SyncEngine createSyncEngine(Configuration configuration) { getLocalStore(), getRemoteStore(), configuration.getInitialUser(), - configuration.getMaxConcurrentLimboResolutions()); + configuration.getMaxConcurrentLimboResolutions(), + configuration.getClearPersistenceCallback()); } /** @@ -146,13 +149,13 @@ public void handleOnlineStateChange(OnlineState onlineState) { } @Override - public void clearCacheData() { - getSyncEngine().clearCacheData(); + public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { + return getSyncEngine().getRemoteKeysForTarget(targetId); } @Override - public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { - return getSyncEngine().getRemoteKeysForTarget(targetId); + public void handleClearPersistence(ByteString sessionToken) { + getSyncEngine().handleClearPersistence(sessionToken); } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index 254b93bd0e7..c39422e3bf1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -52,6 +52,7 @@ import com.google.firebase.firestore.remote.RemoteStore; import com.google.firebase.firestore.remote.TargetChange; import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firebase.firestore.util.Consumer; import com.google.firebase.firestore.util.Function; import com.google.firebase.firestore.util.Logger; import com.google.firebase.firestore.util.Util; @@ -86,6 +87,8 @@ */ public class SyncEngine implements RemoteStore.RemoteStoreCallback { + private final Consumer clearPersistenceCallback; + /** Tracks a limbo resolution. */ private static class LimboResolution { private final DocumentKey key; @@ -165,10 +168,12 @@ public SyncEngine( LocalStore localStore, RemoteStore remoteStore, User initialUser, - int maxConcurrentLimboResolutions) { + int maxConcurrentLimboResolutions, + Consumer clearPersistenceCallback) { this.localStore = localStore; this.remoteStore = remoteStore; this.maxConcurrentLimboResolutions = maxConcurrentLimboResolutions; + this.clearPersistenceCallback = clearPersistenceCallback; queryViewsByQuery = new HashMap<>(); queriesByTarget = new HashMap<>(); @@ -337,33 +342,6 @@ private void addUserCallback(int batchId, TaskCompletionSource userTask) { userTasks.put(batchId, userTask); } - /** - * Takes an updateFunction in which a set of reads and writes can be performed atomically. In the - * updateFunction, the client can read and write values using the supplied transaction object. - * After the updateFunction, all changes will be committed. If a retryable error occurs (ex: some - * other client has changed any of the data referenced), then the updateFunction will be called - * again after a backoff. If the updateFunction still fails after all retries, then the - * transaction will be rejected. - * - *

The transaction object passed to the updateFunction contains methods for accessing documents - * and collections. Unlike other datastore access, data accessed with the transaction will not - * reflect local changes that have not been committed. For this reason, it is required that all - * reads are performed before any writes. Transactions must be performed while online. - * - *

The Task returned is resolved when the transaction is fully committed. - */ - public Task transaction( - AsyncQueue asyncQueue, - TransactionOptions options, - Function> updateFunction) { - return new TransactionRunner(asyncQueue, remoteStore, options, updateFunction).run(); - } - - public Task> runAggregateQuery( - Query query, List aggregateFields) { - return remoteStore.runAggregateQuery(query, aggregateFields); - } - /** Called by FirestoreClient to notify us of a new remote event. */ @Override public void handleRemoteEvent(RemoteEvent event) { @@ -516,21 +494,9 @@ public void handleRejectedWrite(int batchId, Status status) { } @Override - public void clearCacheData() { - assertCallback("clearCacheData"); - - boolean canUseNetwork = remoteStore.canUseNetwork(); - - if (canUseNetwork) { - remoteStore.disableNetwork(); - } - - abortAllTargets(); - localStore.clearCacheData(); - - if (canUseNetwork) { - remoteStore.enableNetwork(); - } + public void handleClearPersistence(ByteString sessionToken) { + assertCallback("handleClearPersistence"); + clearPersistenceCallback.accept(sessionToken); } /** @@ -838,28 +804,4 @@ private boolean errorIsInteresting(Status error) { return false; } - @VisibleForTesting - public boolean isEmpty() { - return queryViewsByQuery.isEmpty() - && queriesByTarget.isEmpty() - && enqueuedLimboResolutions.isEmpty() - && activeLimboTargetsByKey.isEmpty() - && activeLimboResolutionsByTarget.isEmpty() - && limboDocumentRefs.isEmpty() - && mutationUserCallbacks.isEmpty() - && pendingWritesCallbacks.isEmpty(); - } - - public void abortAllTargets() { - hardAssert(!remoteStore.canUseNetwork(), "Network should be disabled during abort of all targets."); - - List targetIds = new ArrayList<>(); - for (QueryView queryView : queryViewsByQuery.values()) { - targetIds.add(queryView.getTargetId()); - } - for (Integer targetId : targetIds) { - handleRejectedListen(targetId, Status.ABORTED); - } - } - } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java index c88e1744112..872ada71760 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/BundleCache.java @@ -39,8 +39,4 @@ public interface BundleCache { /** Saves a NamedQuery from a bundle, using its name as the persistent key. */ void saveNamedQuery(NamedQuery query); - - void clear(); - - boolean isEmpty(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java index e9a7fe2e0f7..6d7f999587c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/DocumentOverlayCache.java @@ -77,11 +77,4 @@ public interface DocumentOverlayCache { * @return Mapping of each document key in the collection group to its overlay. */ Map getOverlays(String collectionGroup, int sinceBatchId, int count); - - /** - * Clear overlays. This should only be done when mutation queue is cleared. - */ - void clear(); - - boolean isEmpty(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/GlobalsCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/GlobalsCache.java index 984908be4c4..e5c04552f67 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/GlobalsCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/GlobalsCache.java @@ -26,8 +26,8 @@ */ interface GlobalsCache { - ByteString getDbToken(); + ByteString getSessionsToken(); - void setDbToken(ByteString value); + void setSessionToken(ByteString value); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/IndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/IndexManager.java index f66598cfcd2..4751ae6e208 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/IndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/IndexManager.java @@ -32,7 +32,6 @@ * Collection Group queries. */ public interface IndexManager { - /** Represents the index state as it relates to a particular target. */ enum IndexType { /** Indicates that no index could be found for serving the target. */ @@ -52,8 +51,6 @@ enum IndexType { /** Initializes the IndexManager. */ void start(); - void clearParents(); - /** * Creates an index entry mapping the collectionId (last segment of the path) to the parent path * (either the containing document location or the empty path for root-level collections). Index @@ -131,6 +128,4 @@ enum IndexType { /** Updates the index entries for the provided documents. */ void updateIndexEntries(ImmutableSortedMap documents); - - void clearIndexData(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java index 2ee44458c04..9d268a503ba 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/LocalStore.java @@ -99,7 +99,7 @@ *

The LocalStore must be able to efficiently execute queries against its local cache of the * documents, to provide the initial set of results before any remote changes have been received. */ -public class LocalStore implements BundleCallback { +public final class LocalStore implements BundleCallback { /** * The maximum time to leave a resume token buffered without writing it out. This value is * arbitrary: it's long enough to avoid several writes (possibly indefinitely if updates come more @@ -230,23 +230,6 @@ public ImmutableSortedMap handleUserChange(User user) { return localDocuments.getDocuments(changedKeys); } - public void clearCacheData() { - mutationQueue.clear(); - - // Clearing the mutation queue requires also clearing document overlays. - documentOverlayCache.clear(); - - remoteDocuments.clear(); - targetCache.clear(); - bundleCache.clear(); - - // Clearing parents is only possible when both mutations and document cache are cleared. - indexManager.clearParents(); - - // Note that index configuration is preserved. - indexManager.clearIndexData(); - } - /** Accepts locally generated Mutations and commits them to storage. */ public LocalDocumentsResult writeLocally(List mutations) { Timestamp localWriteTime = Timestamp.now(); @@ -413,12 +396,12 @@ public SnapshotVersion getLastRemoteSnapshotVersion() { return targetCache.getLastRemoteSnapshotVersion(); } - public ByteString getDbToken() { - return globalsCache.getDbToken(); + public ByteString getSessionToken() { + return globalsCache.getSessionsToken(); } - public void setDbToken(ByteString dbToken) { - globalsCache.setDbToken(dbToken); + public void setSessionsToken(ByteString sessionToken) { + globalsCache.setSessionToken(sessionToken); } /** @@ -941,16 +924,4 @@ private static Target newUmbrellaTarget(String bundleName) { // queried. return Query.atPath(ResourcePath.fromString("__bundle__/docs/" + bundleName)).toTarget(); } - - @VisibleForTesting - public boolean isEmpty() { - return localViewReferences.isEmpty() - && queryDataByTarget.size() == 0 - && targetIdByTarget.isEmpty() - && mutationQueue.isEmpty() - && documentOverlayCache.isEmpty() - && remoteDocuments.isEmpty() - && bundleCache.isEmpty() - && targetCache.isEmpty(); - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java index 7ad3dc4cbf7..147af879453 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryBundleCache.java @@ -46,15 +46,4 @@ public NamedQuery getNamedQuery(String queryName) { public void saveNamedQuery(NamedQuery query) { namedQueries.put(query.getName(), query); } - - @Override - public void clear() { - bundles.clear(); - namedQueries.clear(); - } - - @Override - public boolean isEmpty() { - return bundles.isEmpty() && namedQueries.isEmpty(); - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java index 925bb7085f6..0731591c5d2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryDocumentOverlayCache.java @@ -146,15 +146,4 @@ public Map getOverlays( return result; } - - @Override - public void clear() { - overlays.clear(); - overlayByBatchId.clear(); - } - - @Override - public boolean isEmpty() { - return overlays.isEmpty() && overlayByBatchId.isEmpty(); - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryGlobalsCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryGlobalsCache.java index f21f0e0ba45..e02dcbb2f7b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryGlobalsCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryGlobalsCache.java @@ -19,15 +19,15 @@ /** In-memory cache of global values */ final class MemoryGlobalsCache implements GlobalsCache { - private ByteString dbToken; + private ByteString sessionToken; @Override - public ByteString getDbToken() { - return dbToken; + public ByteString getSessionsToken() { + return sessionToken; } @Override - public void setDbToken(ByteString value) { - dbToken = value; + public void setSessionToken(ByteString value) { + sessionToken = value; } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryIndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryIndexManager.java index e1c0a815d32..a55734b3859 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryIndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryIndexManager.java @@ -40,11 +40,6 @@ public MemoryIndexManager() {} @Override public void start() {} - @Override - public void clearParents() { - collectionParentsIndex.clear(); - } - @Override public void addToCollectionParentIndex(ResourcePath collectionPath) { collectionParentsIndex.add(collectionPath); @@ -124,11 +119,6 @@ public void updateIndexEntries(ImmutableSortedMap documen // Field indices are not supported with memory persistence. } - @Override - public void clearIndexData() { - // Field indices are not supported with memory persistence. - } - /** * Internal implementation of the collection-parent index. Also used for in-memory caching by * SQLiteIndexManager and initial index population in SQLiteSchema. @@ -154,9 +144,5 @@ List getEntries(String collectionId) { HashSet existingParents = index.get(collectionId); return existingParents != null ? new ArrayList<>(existingParents) : Collections.emptyList(); } - - void clear() { - index.clear(); - } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java index e7eee857f40..7a238dd20c1 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryMutationQueue.java @@ -18,7 +18,6 @@ import static com.google.firebase.firestore.util.Preconditions.checkNotNull; import static java.util.Collections.emptyList; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.Timestamp; import com.google.firebase.database.collection.ImmutableSortedSet; @@ -76,15 +75,11 @@ final class MemoryMutationQueue implements MutationQueue { MemoryMutationQueue(MemoryPersistence persistence, User user) { this.persistence = persistence; queue = new ArrayList<>(); - indexManager = persistence.getIndexManager(user); - init(); - } - private void init() { - queue.clear(); batchesByDocumentKey = new ImmutableSortedSet<>(emptyList(), DocumentReference.BY_KEY); nextBatchId = 1; lastStreamToken = WriteStream.EMPTY_STREAM_TOKEN; + indexManager = persistence.getIndexManager(user); } // MutationQueue implementation @@ -325,11 +320,6 @@ public void performConsistencyCheck() { } } - @Override - public void clear() { - init(); - } - boolean containsKey(DocumentKey key) { // Create a reference with a zero ID as the start position to find any document reference with // this key. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java index 47bd23b996d..3b4fa1048b2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryRemoteDocumentCache.java @@ -74,13 +74,6 @@ public void removeAll(Collection keys) { indexManager.updateIndexEntries(deletedDocs); } - @Override - public void clear() { - hardAssert(indexManager != null, "setIndexManager() not called"); - docs = emptyDocumentMap(); - indexManager.clearIndexData(); - } - @Override public MutableDocument get(DocumentKey key) { Document doc = docs.get(key); @@ -146,11 +139,6 @@ public Map getDocumentsMatchingQuery( return result; } - @Override - public boolean isEmpty() { - return docs.isEmpty(); - } - @Override public Map getDocumentsMatchingQuery( Query query, IndexOffset offset, @Nonnull Set mutatedKeys) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java index 937604fa3ac..41881f16f04 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MemoryTargetCache.java @@ -16,7 +16,6 @@ import android.util.SparseArray; import androidx.annotation.Nullable; - import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.core.Target; import com.google.firebase.firestore.model.DocumentKey; @@ -42,21 +41,14 @@ final class MemoryTargetCache implements TargetCache { private int highestTargetId; /** The last received snapshot version. */ - private SnapshotVersion lastRemoteSnapshotVersion; + private SnapshotVersion lastRemoteSnapshotVersion = SnapshotVersion.NONE; - private long highestSequenceNumber; + private long highestSequenceNumber = 0; private final MemoryPersistence persistence; MemoryTargetCache(MemoryPersistence persistence) { this.persistence = persistence; - init(); - } - - private void init() { - highestTargetId = 0; - highestSequenceNumber = 0; - lastRemoteSnapshotVersion = SnapshotVersion.NONE; } @Override @@ -180,21 +172,6 @@ public boolean containsKey(DocumentKey key) { return references.containsKey(key); } - @Override - public void clear() { - targets.clear(); - init(); - } - - @Override - public boolean isEmpty() { - return targets.isEmpty() - && references.isEmpty() - && highestTargetId == 0 - && highestSequenceNumber == 0 - && lastRemoteSnapshotVersion == SnapshotVersion.NONE; - } - long getByteSize(LocalSerializer serializer) { long count = 0; for (Map.Entry entry : targets.entrySet()) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MutationQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MutationQueue.java index 89e14f51eb4..02a7f7ea359 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MutationQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/MutationQueue.java @@ -133,9 +133,4 @@ List getAllMutationBatchesAffectingDocumentKeys( /** Performs a consistency check, examining the mutation queue for any leaks, if possible. */ void performConsistencyCheck(); - - /** - * Removes all mutations. - */ - void clear(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/ReferenceSet.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/ReferenceSet.java index 83cf40f19b1..b46174a201e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/ReferenceSet.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/ReferenceSet.java @@ -42,10 +42,6 @@ public class ReferenceSet { private ImmutableSortedSet referencesByTarget; public ReferenceSet() { - init(); - } - - private void init() { referencesByKey = new ImmutableSortedSet<>(emptyList(), DocumentReference.BY_KEY); referencesByTarget = new ImmutableSortedSet<>(emptyList(), DocumentReference.BY_TARGET); } @@ -106,7 +102,9 @@ public ImmutableSortedSet removeReferencesForId(int targetId) { /** Clears all references for all IDs. */ public void removeAllReferences() { - init(); + for (DocumentReference reference : referencesByKey) { + removeReference(reference); + } } private void removeReference(DocumentReference ref) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java index a41285fc853..8ff90864342 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/RemoteDocumentCache.java @@ -51,9 +51,6 @@ interface RemoteDocumentCache { /** Removes the cached entries for the given keys (no-op if no entry exists). */ void removeAll(Collection keys); - /** Removes all cached entries. */ - void clear(); - /** * Looks up an entry in the cache. * @@ -110,6 +107,4 @@ Map getDocumentsMatchingQuery( IndexOffset offset, @Nonnull Set mutatedKeys, @Nullable QueryContext context); - - boolean isEmpty(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java index 348aa4e076d..444f03a03e4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteBundleCache.java @@ -24,8 +24,6 @@ import com.google.firestore.bundle.BundledQuery; import com.google.protobuf.InvalidProtocolBufferException; -import kotlin.NotImplementedError; - class SQLiteBundleCache implements BundleCache { private final SQLitePersistence db; private final LocalSerializer serializer; @@ -106,15 +104,4 @@ public void saveNamedQuery(NamedQuery query) { query.getReadTime().getTimestamp().getNanoseconds(), bundledQuery.toByteArray()); } - - @Override - public void clear() { - db.execute("DELETE FROM bundles"); - db.execute("DELETE FROM named_queries"); - } - - @Override - public boolean isEmpty() { - throw new NotImplementedError(); - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java index c41a8ea5403..84628edd4a2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteDocumentOverlayCache.java @@ -199,18 +199,6 @@ public Map getOverlays( return result; } - @Override - public void clear() { - db.execute("DELETE FROM document_overlays"); - } - - @Override - public boolean isEmpty() { - return db.query("SELECT COUNT(*) FROM overlay_mutation") - .firstValue(row -> row.getInt(0)) - .intValue() == 0; - } - private void processOverlaysInBackground( BackgroundQueue backgroundQueue, Map results, Cursor row) { byte[] rawMutation = row.getBlob(0); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteGlobalsCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteGlobalsCache.java index b4f7a2ba91a..a26c99a1490 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteGlobalsCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteGlobalsCache.java @@ -18,7 +18,7 @@ public class SQLiteGlobalsCache implements GlobalsCache { - private static final String DB_TOKEN = "dbToken"; + private static final String SESSION_TOKEN = "sessionToken"; private final SQLitePersistence db; public SQLiteGlobalsCache(SQLitePersistence persistence) { @@ -26,14 +26,14 @@ public SQLiteGlobalsCache(SQLitePersistence persistence) { } @Override - public ByteString getDbToken() { - byte[] bytes = get(DB_TOKEN); + public ByteString getSessionsToken() { + byte[] bytes = get(SESSION_TOKEN); return bytes == null ? null : ByteString.copyFrom(bytes); } @Override - public void setDbToken(ByteString value) { - set(DB_TOKEN, value.toByteArray()); + public void setSessionToken(ByteString value) { + set(SESSION_TOKEN, value.toByteArray()); } private byte[] get(String name) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java index e124733e648..4a060d2fe02 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteIndexManager.java @@ -169,12 +169,6 @@ public void start() { started = true; } - @Override - public void clearParents() { - db.execute("DELETE FROM collection_parents"); - collectionParentsCache.clear(); - } - @Override public void addToCollectionParentIndex(ResourcePath collectionPath) { hardAssert(started, "IndexManager not started"); @@ -247,8 +241,6 @@ public void deleteAllFieldIndexes() { nextIndexToUpdate.clear(); memoizedIndexes.clear(); - memoizedMaxIndexId = -1; - memoizedMaxSequenceNumber = -1; } @Override @@ -290,32 +282,6 @@ public void updateIndexEntries(ImmutableSortedMap documen } } - @Override - public void clearIndexData() { - db.execute("DELETE FROM index_entries"); - db.execute("DELETE FROM index_state"); - nextIndexToUpdate.clear(); - memoizedIndexes.clear(); - memoizedMaxIndexId = -1; - memoizedMaxSequenceNumber = -1; - - db.query("SELECT index_id, collection_group, index_proto FROM index_configuration") - .forEach( - row -> { - try { - int indexId = row.getInt(0); - String collectionGroup = row.getString(1); - List segments = - serializer.decodeFieldIndexSegments(Index.parseFrom(row.getBlob(2))); - - // Store the index and update `memoizedMaxIndexId` and `memoizedMaxSequenceNumber`. - memoizeIndex(FieldIndex.create(indexId, collectionGroup, segments, FieldIndex.INITIAL_STATE)); - } catch (InvalidProtocolBufferException e) { - throw fail("Failed to decode index: " + e); - } - }); - } - /** * Updates the index entries for the provided document by deleting entries that are no longer * referenced in {@code newEntries} and adding all newly added entries. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java index 4a763eaf988..dd70a58d02b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteMutationQueue.java @@ -437,15 +437,6 @@ public void performConsistencyCheck() { danglingMutationReferences); } - @Override - public void clear() { - db.execute("DELETE FROM mutations"); - db.execute("DELETE FROM document_mutations"); - db.execute("DELETE FROM mutation_queues"); - nextBatchId = 1; - lastStreamToken = WriteStream.EMPTY_STREAM_TOKEN; - } - /** * Decodes mutation batch bytes obtained via substring. If the blob is smaller than * BLOB_MAX_INLINE_LENGTH, executes additional queries to load the rest of the blob. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java index 825da2014f2..b26f9601a81 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteRemoteDocumentCache.java @@ -47,8 +47,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import kotlin.NotImplementedError; - final class SQLiteRemoteDocumentCache implements RemoteDocumentCache { /** The number of bind args per collection group in {@link #getAll(String, IndexOffset, int)} */ @VisibleForTesting static final int BINDS_PER_STATEMENT = 9; @@ -113,12 +111,6 @@ public void removeAll(Collection keys) { indexManager.updateIndexEntries(deletedDocs); } - @Override - public void clear() { - db.execute("DELETE FROM remote_documents"); - indexManager.clearIndexData(); - } - @Override public MutableDocument get(DocumentKey documentKey) { return getAll(Collections.singletonList(documentKey)).get(documentKey); @@ -290,11 +282,6 @@ public Map getDocumentsMatchingQuery( context); } - @Override - public boolean isEmpty() { - throw new NotImplementedError(); - } - private MutableDocument decodeMaybeDocument( byte[] bytes, int readTimeSeconds, int readTimeNanos) { try { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java index ab1e4e5f678..12105419fd6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLiteTargetCache.java @@ -28,8 +28,6 @@ import com.google.firebase.firestore.util.Consumer; import com.google.protobuf.InvalidProtocolBufferException; -import kotlin.NotImplementedError; - /** Cached Queries backed by SQLite. */ final class SQLiteTargetCache implements TargetCache { @@ -312,20 +310,4 @@ public boolean containsKey(DocumentKey key) { .binding(path) .isEmpty(); } - - @Override - public void clear() { - db.execute("DELETE FROM targets"); - db.execute("DELETE FROM target_documents"); - highestTargetId = 0; - lastListenSequenceNumber = 0; - lastRemoteSnapshotVersion = SnapshotVersion.NONE; - targetCount = 0; - writeMetadata(); - } - - @Override - public boolean isEmpty() { - throw new NotImplementedError(); - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java index bbe424a32c6..0b39babfb5f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/TargetCache.java @@ -115,8 +115,4 @@ interface TargetCache { /** @return True if the document is part of any target */ boolean containsKey(DocumentKey key); - - void clear(); - - boolean isEmpty(); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java index 30123f6bdc2..1d9fffd5ea8 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java @@ -77,6 +77,8 @@ void run(Runnable task) { class StreamObserver implements IncomingStreamObserver { private final CloseGuardedRunner dispatcher; + private int responseCount = 0; + StreamObserver(CloseGuardedRunner dispatcher) { this.dispatcher = dispatcher; } @@ -107,17 +109,24 @@ public void onHeaders(Metadata headers) { @Override public void onNext(RespT response) { + final int currentResponseCount = responseCount + 1; dispatcher.run( - () -> { - if (Logger.isDebugEnabled()) { - Logger.debug( - AbstractStream.this.getClass().getSimpleName(), - "(%x) Stream received: %s", - System.identityHashCode(AbstractStream.this), - response); - } - AbstractStream.this.onNext(response); - }); + () -> { + if (Logger.isDebugEnabled()) { + Logger.debug( + AbstractStream.this.getClass().getSimpleName(), + "(%x) Stream received (%s): %s", + System.identityHashCode(AbstractStream.this), + currentResponseCount, + response); + } + if (currentResponseCount == 1) { + AbstractStream.this.onFirst(response); + } else { + AbstractStream.this.onNext(response); + } + }); + responseCount = currentResponseCount; } @Override @@ -429,6 +438,8 @@ private void onOpen() { } } + public abstract void onFirst(RespT change); + public abstract void onNext(RespT change); private void performBackoff() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index fe8ddfa06e0..ee04ef42263 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -20,6 +20,8 @@ import android.content.Context; import android.os.Build; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.firestore.AggregateField; @@ -125,6 +127,7 @@ void shutdown() { channel.shutdown(); } + @VisibleForTesting(otherwise = VisibleForTesting.NONE) AsyncQueue getWorkerQueue() { return workerQueue; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java index cb980eb39af..bb7d291c165 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteSerializer.java @@ -75,7 +75,6 @@ import com.google.firestore.v1.Target; import com.google.firestore.v1.Target.DocumentsTarget; import com.google.firestore.v1.Target.QueryTarget; -import com.google.firestore.v1.TargetChange; import com.google.firestore.v1.Value; import com.google.protobuf.Int32Value; import io.grpc.Status; @@ -1017,11 +1016,10 @@ public SnapshotVersion decodeVersionFromListenResponse(ListenResponse watchChang if (watchChange.getResponseTypeCase() != ResponseTypeCase.TARGET_CHANGE) { return SnapshotVersion.NONE; } - TargetChange targetChange = watchChange.getTargetChange(); - if (targetChange.getTargetIdsCount() != 0) { + if (watchChange.getTargetChange().getTargetIdsCount() != 0) { return SnapshotVersion.NONE; } - return decodeVersion(targetChange.getReadTime()); + return decodeVersion(watchChange.getTargetChange().getReadTime()); } private Status fromStatus(com.google.rpc.Status status) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index a587c8909fa..8b73668ed13 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -14,7 +14,6 @@ package com.google.firebase.firestore.remote; -import static com.google.firebase.firestore.util.Assert.fail; import static com.google.firebase.firestore.util.Assert.hardAssert; import androidx.annotation.Nullable; @@ -44,11 +43,11 @@ import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Logger; import com.google.firebase.firestore.util.Util; +import com.google.firestore.v1.InitResponse; import com.google.firestore.v1.Value; import com.google.protobuf.ByteString; import io.grpc.Status; import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Deque; import java.util.HashMap; import java.util.List; @@ -66,8 +65,6 @@ public class RemoteStore implements WatchChangeAggregator.TargetMetadataProvider /** The log tag to use for this class. */ private static final String LOG_TAG = "RemoteStore"; - private boolean writeStreamHandshakeInProgress; - private boolean watchStreamHandshakeInProgress; /** A callback interface for events from RemoteStore. */ public interface RemoteStoreCallback { @@ -108,11 +105,6 @@ public interface RemoteStoreCallback { */ void handleOnlineStateChange(OnlineState onlineState); - /** - * Synchronization event that requires cache be cleared. - */ - void clearCacheData(); - /** * Returns the set of remote document keys for the given target ID. This list includes the * documents that were assigned to the target when we received the last snapshot. @@ -120,6 +112,8 @@ public interface RemoteStoreCallback { *

Returns an empty set of document keys for unknown targets. */ ImmutableSortedSet getRemoteKeysForTarget(int targetId); + + void handleClearPersistence(ByteString sessionToken); } private final RemoteStoreCallback remoteStoreCallback; @@ -162,11 +156,11 @@ public interface RemoteStoreCallback { private final Deque writePipeline; public RemoteStore( - RemoteStoreCallback remoteStoreCallback, - LocalStore localStore, - Datastore datastore, - AsyncQueue workerQueue, - ConnectivityMonitor connectivityMonitor) { + RemoteStoreCallback remoteStoreCallback, + LocalStore localStore, + Datastore datastore, + AsyncQueue workerQueue, + ConnectivityMonitor connectivityMonitor) { this.remoteStoreCallback = remoteStoreCallback; this.localStore = localStore; this.datastore = datastore; @@ -178,25 +172,29 @@ public RemoteStore( onlineStateTracker = new OnlineStateTracker(workerQueue, remoteStoreCallback::handleOnlineStateChange); - watchStreamHandshakeInProgress = false; - writeStreamHandshakeInProgress = false; - // Create new streams (but note they're not started yet). watchStream = datastore.createWatchStream( new WatchStream.Callback() { @Override - public void onOpen() { - hardAssert(!watchStreamHandshakeInProgress, "Watch handshake already in progress."); - if (!writeStreamHandshakeInProgress) { - watchStream.sendHandshake(localStore.getDbToken()); + public void onHandshakeReady() { + if (!writeStream.isHandshakeInProgress()) { + watchStream.sendHandshake(localStore.getSessionToken()); } - watchStreamHandshakeInProgress = true; } @Override - public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { - handleWatchStreamHandshakeComplete(dbToken, clearCache); + public void onHandshake(InitResponse initResponse) { + if (initResponse.getClearCache()) { + remoteStoreCallback.handleClearPersistence(initResponse.getSessionToken()); + } else { + handleWatchStreamHandshakeComplete(initResponse.getSessionToken()); + } + } + + @Override + public void onOpen() { + handleWatchStreamOpen(); } @Override @@ -206,7 +204,6 @@ public void onWatchChange(SnapshotVersion snapshotVersion, WatchChange watchChan @Override public void onClose(Status status) { - watchStreamHandshakeInProgress = false; handleWatchStreamClose(status); } }); @@ -215,17 +212,24 @@ public void onClose(Status status) { datastore.createWriteStream( new WriteStream.Callback() { @Override - public void onOpen() { - hardAssert(!writeStreamHandshakeInProgress, "Watch handshake already in progress."); - if (!watchStreamHandshakeInProgress) { - writeStream.sendHandshake(localStore.getDbToken()); + public void onHandshakeReady() { + if (!watchStream.isHandshakeInProgress()) { + writeStream.sendHandshake(localStore.getSessionToken()); + } + } + + @Override + public void onHandshake(InitResponse initResponse) { + if (initResponse.getClearCache()) { + remoteStoreCallback.handleClearPersistence(initResponse.getSessionToken()); + } else { + handleWriteStreamHandshakeComplete(initResponse.getSessionToken()); } - writeStreamHandshakeInProgress = true; } @Override - public void onHandshakeComplete(ByteString dbToken, boolean clearCache) { - handleWriteStreamHandshakeComplete(dbToken, clearCache); + public void onOpen() { + handleWriteStreamOpen(); } @Override @@ -236,7 +240,6 @@ public void onWriteResponse( @Override public void onClose(Status status) { - writeStreamHandshakeInProgress = false; handleWriteStreamClose(status); } }); @@ -334,15 +337,6 @@ private void restartNetwork() { enableNetwork(); } - /** - * Starts up the remote store, creating streams, restoring state from LocalStore, etc. This should - * called before using any other API endpoints in this class. - */ - public void start() { - // For now, all setup is handled by enableNetwork(). We might expand on this in the future. - enableNetwork(); - } - /** * Shuts down the remote store, tearing down connections and otherwise cleaning up. This is not * reversible and renders the Remote Store unusable. @@ -392,7 +386,7 @@ public void listen(TargetData targetData) { if (shouldStartWatchStream()) { startWatchStream(); - } else if (watchStream.isOpen() && watchStream.isHandshakeComplete()) { + } else if (watchStream.isHandshakeComplete()) { sendWatchRequest(targetData); } } @@ -422,7 +416,7 @@ public void stopListening(int targetId) { targetData != null, "stopListening called on target no currently watched: %d", targetId); // The watch stream might not be started if we're in a disconnected state - if (watchStream.isOpen()) { + if (watchStream.isHandshakeComplete()) { sendUnwatchRequest(targetId); } @@ -476,19 +470,21 @@ private void startWatchStream() { onlineStateTracker.handleWatchStreamStart(); } - private void handleWatchStreamHandshakeComplete(ByteString dbToken, boolean clearCache) { - if (clearCache) { - remoteStoreCallback.clearCacheData(); + private void handleWatchStreamHandshakeComplete(ByteString sessionToken) { + if (!sessionToken.isEmpty()) { + localStore.setSessionsToken(sessionToken); + } else { + sessionToken = localStore.getSessionToken(); } - localStore.setDbToken(dbToken); - watchStreamHandshakeInProgress = false; // If write stream started handshake, but was waiting for listen handshake to complete, we // can continue write handshake now. - if (writeStreamHandshakeInProgress) { - writeStream.sendHandshake(dbToken); + if (writeStream.isHandshakeInProgress()) { + writeStream.sendHandshake(sessionToken); } + } + private void handleWatchStreamOpen() { // Restore any existing watches. for (TargetData targetData : listenTargets.values()) { sendWatchRequest(targetData); @@ -680,7 +676,7 @@ private void addToWritePipeline(MutationBatch mutationBatch) { writePipeline.add(mutationBatch); - if (writeStream.isOpen() && writeStream.isHandshakeComplete()) { + if (writeStream.isHandshakeComplete()) { writeStream.writeMutations(mutationBatch.getMutations()); } } @@ -696,22 +692,24 @@ private void startWriteStream() { * Handles a successful handshake response from the server, which is our cue to send any pending * writes. */ - private void handleWriteStreamHandshakeComplete(ByteString dbToken, boolean clearCache) { - if (clearCache) { - remoteStoreCallback.clearCacheData(); + private void handleWriteStreamHandshakeComplete(ByteString sessionToken) { + if (!sessionToken.isEmpty()) { + localStore.setSessionsToken(sessionToken); + } else { + sessionToken = localStore.getSessionToken(); } - localStore.setDbToken(dbToken); - writeStreamHandshakeInProgress = false; // If listen stream started handshake, but was waiting for write handshake to complete, we // can continue listen handshake now. - if (watchStreamHandshakeInProgress) { - watchStream.sendHandshake(dbToken); + if (watchStream.isHandshakeInProgress()) { + watchStream.sendHandshake(sessionToken); } // Record the stream token. localStore.setLastStreamToken(writeStream.getLastStreamToken()); + } + private void handleWriteStreamOpen() { // Send the write pipeline now that stream is established. for (MutationBatch batch : writePipeline) { writeStream.writeMutations(batch.getMutations()); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java index d20a5be1eeb..d5064fa3d8e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchChangeAggregator.java @@ -109,7 +109,7 @@ public void handleDocumentChange(DocumentChange documentChange) { } for (int targetId : documentChange.getRemovedTargetIds()) { - removeDocumentFromTarget(targetId, documentKey, document); + removeDocumentFromTarget(targetId, documentKey, documentChange.getNewDocument()); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java index 910e553181f..56c0618a71c 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java @@ -25,7 +25,6 @@ import com.google.firestore.v1.InitResponse; import com.google.firestore.v1.ListenRequest; import com.google.firestore.v1.ListenResponse; -import com.google.firestore.v1.WriteRequest; import com.google.protobuf.ByteString; import java.util.Map; @@ -49,8 +48,10 @@ public class WatchStream /** A callback interface for the set of events that can be emitted by the WatchStream */ interface Callback extends AbstractStream.StreamCallback { + void onHandshakeReady(); + /** The handshake for this write stream has completed */ - void onHandshakeComplete(ByteString dbToken, boolean clearCache); + void onHandshake(InitResponse initResponse); /** A new change from the watch stream. Snapshot version will ne non-null if it was set */ void onWatchChange(SnapshotVersion snapshotVersion, WatchChange watchChange); @@ -84,12 +85,12 @@ public void start() { /** * Sends an InitRequest to the server. */ - void sendHandshake(ByteString dbToken) { + void sendHandshake(ByteString sessionToken) { hardAssert(isOpen(), "Writing handshake requires an opened stream"); hardAssert(!handshakeComplete, "Handshake already completed"); InitRequest.Builder initRequest = InitRequest.newBuilder(); - if (dbToken != null) initRequest.setDbToken(dbToken); + if (sessionToken != null) initRequest.setSessionToken(sessionToken); ListenRequest.Builder request = ListenRequest.newBuilder() .setDatabase(serializer.databaseName()) @@ -98,6 +99,10 @@ void sendHandshake(ByteString dbToken) { writeRequest(request.build()); } + boolean isHandshakeInProgress() { + return isOpen() && !handshakeComplete; + } + /** * Tracks whether or not a handshake has been successfully exchanged and the stream is ready to * accept watch queries. @@ -140,22 +145,22 @@ public void unwatchTarget(int targetId) { } @Override - public void onNext(com.google.firestore.v1.ListenResponse response) { - // A successful response means the stream is healthy - backoff.reset(); + public void onFirst(ListenResponse response) { + hardAssert(response.hasInitResponse(), "InitResponse expected as part of Handshake response"); - if (!handshakeComplete) { - hardAssert(response.hasInitResponse(), "InitResponse expected as part of Handshake response"); + // The first response is the handshake response + handshakeComplete = true; - // The first response is the handshake response - handshakeComplete = true; + listener.onHandshake(response.getInitResponse()); + } - InitResponse initResponse = response.getInitResponse(); - listener.onHandshakeComplete(initResponse.getDbToken(), initResponse.getClearCache()); - } else { - WatchChange watchChange = serializer.decodeWatchChange(response); - SnapshotVersion snapshotVersion = serializer.decodeVersionFromListenResponse(response); - listener.onWatchChange(snapshotVersion, watchChange); - } + @Override + public void onNext(ListenResponse response) { + // A successful response means the stream is healthy + backoff.reset(); + + WatchChange watchChange = serializer.decodeWatchChange(response); + SnapshotVersion snapshotVersion = serializer.decodeVersionFromListenResponse(response); + listener.onWatchChange(snapshotVersion, watchChange); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java index 20c2b41ffe7..d5842652b8f 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java @@ -55,8 +55,11 @@ public class WriteStream extends AbstractStream mutationResults); @@ -97,6 +100,10 @@ protected void tearDown() { } } + boolean isHandshakeInProgress() { + return isOpen() && !handshakeComplete; + } + /** * Tracks whether or not a handshake has been successfully exchanged and the stream is ready to * accept mutations. @@ -131,16 +138,16 @@ void setLastStreamToken(ByteString streamToken) { /** * Sends an initial streamToken to the server, performing the handshake required to make the * StreamingWrite RPC work. Subsequent {@link #writeMutations} calls should wait until a response - * has been delivered to {@link WriteStream.Callback#onHandshakeComplete}. + * has been delivered to {@link WriteStream.Callback#onHandshake}. */ - void sendHandshake(ByteString dbToken) { + void sendHandshake(ByteString sessionToken) { hardAssert(isOpen(), "Writing handshake requires an opened stream"); hardAssert(!handshakeComplete, "Handshake already completed"); // TODO: Support stream resumption. We intentionally do not set the stream token on the // handshake, ignoring any stream token we might have. - InitRequest.Builder initRequest = InitRequest.newBuilder() - .setDbToken(dbToken); + InitRequest.Builder initRequest = InitRequest.newBuilder(); + if (sessionToken != null) initRequest.setSessionToken(sessionToken); WriteRequest.Builder request = WriteRequest.newBuilder() .setDatabase(serializer.databaseName()) @@ -167,33 +174,35 @@ void writeMutations(List mutations) { writeRequest(request.build()); } + @Override + public void onFirst(WriteResponse response) { + lastStreamToken = response.getStreamToken(); + + hardAssert(response.hasInitResponse(),"InitResponse expected as part of Handshake response"); + + // The first response is the handshake response + handshakeComplete = true; + + listener.onHandshake(response.getInitResponse()); + } + @Override public void onNext(WriteResponse response) { lastStreamToken = response.getStreamToken(); - if (!handshakeComplete) { - hardAssert(response.hasInitResponse(),"InitResponse expected as part of Handshake response"); - - // The first response is the handshake response - handshakeComplete = true; - - InitResponse initResponse = response.getInitResponse(); - listener.onHandshakeComplete(initResponse.getDbToken(), initResponse.getClearCache()); - } else { - // A successful first write response means the stream is healthy, - // Note, that we could consider a successful handshake healthy, however, - // the write itself might be causing an error we want to back off from. - backoff.reset(); - - SnapshotVersion commitVersion = serializer.decodeVersion(response.getCommitTime()); - - int count = response.getWriteResultsCount(); - List results = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - com.google.firestore.v1.WriteResult result = response.getWriteResults(i); - results.add(serializer.decodeMutationResult(result, commitVersion)); - } - listener.onWriteResponse(commitVersion, results); + // A successful first write response means the stream is healthy, + // Note, that we could consider a successful handshake healthy, however, + // the write itself might be causing an error we want to back off from. + backoff.reset(); + + SnapshotVersion commitVersion = serializer.decodeVersion(response.getCommitTime()); + + int count = response.getWriteResultsCount(); + List results = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + com.google.firestore.v1.WriteResult result = response.getWriteResults(i); + results.add(serializer.decodeMutationResult(result, commitVersion)); } + listener.onWriteResponse(commitVersion, results); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java index 34d4ef0d8de..83cc9dade6a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java @@ -20,7 +20,7 @@ import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; -import androidx.annotation.NonNull; + import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; @@ -29,17 +29,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.concurrent.Callable; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.Semaphore; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import javax.annotation.CheckReturnValue; @@ -191,232 +183,35 @@ public static Task callTask(Executor executor, Callable - *

  • Synchronized task scheduling. This is different from function 3, which is about task - * execution in a single thread. - *
  • Ability to do soft-shutdown: only critical tasks related to shutting Firestore SDK down - * can be executed once the shutdown process initiated. - *
  • Single threaded execution service, no concurrent execution among the `Runnable`s - * scheduled in this Executor. - * - */ - private class SynchronizedShutdownAwareExecutor implements Executor { - /** - * The single threaded executor that is backing this Executor. This is also the executor used - * when some tasks explicitly request to run after shutdown has been initiated. - */ - private final ScheduledThreadPoolExecutor internalExecutor; - - /** Whether the shutdown process has initiated, once it is started, it is not revertable. */ - private boolean isShuttingDown; - - /** - * The single thread that will be used by the executor. This is created early and managed - * directly so that it's possible later to make assertions about executing on the correct - * thread. - */ - private final Thread thread; - - /** A ThreadFactory for a single, pre-created thread. */ - private class DelayedStartFactory implements Runnable, ThreadFactory { - private final CountDownLatch latch = new CountDownLatch(1); - private Runnable delegate; - - @Override - public void run() { - try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - delegate.run(); - } - - @Override - public Thread newThread(@NonNull Runnable runnable) { - hardAssert(delegate == null, "Only one thread may be created in an AsyncQueue."); - delegate = runnable; - latch.countDown(); - return thread; - } - } - - // TODO(b/258277574): Migrate to go/firebase-android-executors - @SuppressLint("ThreadPoolCreation") - SynchronizedShutdownAwareExecutor() { - DelayedStartFactory threadFactory = new DelayedStartFactory(); - - thread = Executors.defaultThreadFactory().newThread(threadFactory); - thread.setName("FirestoreWorker"); - thread.setDaemon(true); - thread.setUncaughtExceptionHandler((crashingThread, throwable) -> panic(throwable)); - - internalExecutor = - new ScheduledThreadPoolExecutor(1, threadFactory) { - @Override - protected void afterExecute(Runnable r, Throwable t) { - super.afterExecute(r, t); - if (t == null && r instanceof Future) { - Future future = (Future) r; - try { - // Not all Futures will be done, for example when used with scheduledAtFixedRate. - if (future.isDone()) { - future.get(); - } - } catch (CancellationException ce) { - // Cancellation exceptions are okay, we expect them to happen sometimes - } catch (ExecutionException ee) { - t = ee.getCause(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - if (t != null) { - panic(t); - } - } - }; - - // Core threads don't time out, this only takes effect when we drop the number of required - // core threads - internalExecutor.setKeepAliveTime(3, TimeUnit.SECONDS); - - isShuttingDown = false; - } - - /** Synchronized access to isShuttingDown */ - private synchronized boolean isShuttingDown() { - return isShuttingDown; - } - - /** - * Check if shutdown is initiated before scheduling. If it is initiated, the command will not be - * executed. - */ - @Override - public synchronized void execute(Runnable command) { - if (!isShuttingDown) { - internalExecutor.execute(command); - } - } - - /** Execute the command, regardless if shutdown has been initiated. */ - public void executeEvenAfterShutdown(Runnable command) { - try { - internalExecutor.execute(command); - } catch (RejectedExecutionException e) { - // The only way we can get here is if the AsyncQueue has panicked and we're now racing with - // the post to the main looper that will crash the app. - Logger.warn(AsyncQueue.class.getSimpleName(), "Refused to enqueue task after panic"); - } - } - - /** - * Run a given `Callable` on this executor, and report the result of the `Callable` in a {@link - * Task}. The `Callable` will not be run if the executor started shutting down already. - * - * @return A {@link Task} resolves when the requested `Callable` completes, or reports error - * when the `Callable` runs into exceptions. - */ - private Task executeAndReportResult(Callable task) { - final TaskCompletionSource completionSource = new TaskCompletionSource<>(); - try { - this.execute( - () -> { - try { - completionSource.setResult(task.call()); - } catch (Exception e) { - completionSource.setException(e); - throw new RuntimeException(e); - } - }); - } catch (RejectedExecutionException e) { - // The only way we can get here is if the AsyncQueue has panicked and we're now racing with - // the post to the main looper that will crash the app. - Logger.warn(AsyncQueue.class.getSimpleName(), "Refused to enqueue task after panic"); - } - return completionSource.getTask(); - } - - /** - * Initiate the shutdown process. Once called, the only possible way to run `Runnable`s are by - * holding the `internalExecutor` reference. - */ - private synchronized Task executeAndInitiateShutdown(Runnable task) { - if (isShuttingDown()) { - TaskCompletionSource source = new TaskCompletionSource<>(); - source.setResult(null); - return source.getTask(); - } - - // Not shutting down yet, execute and return a Task. - Task t = - executeAndReportResult( - () -> { - task.run(); - return null; - }); - - // Mark the initiation of shut down. - isShuttingDown = true; - - return t; - } - - /** - * Wraps {@link ScheduledThreadPoolExecutor#schedule(Runnable, long, TimeUnit)} and provides - * shutdown state check: the command will not be scheduled if the shutdown has been initiated. - */ - private synchronized ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { - if (!isShuttingDown) { - return internalExecutor.schedule(command, delay, unit); - } - return null; - } - - /** Wraps around {@link ScheduledThreadPoolExecutor#shutdownNow()}. */ - private void shutdownNow() { - internalExecutor.shutdownNow(); - } - - /** Wraps around {@link ScheduledThreadPoolExecutor#setCorePoolSize(int)}. */ - private void setCorePoolSize(int size) { - internalExecutor.setCorePoolSize(size); - } - } - /** The executor backing this AsyncQueue. */ private final SynchronizedShutdownAwareExecutor executor; // Tasks scheduled to be queued in the future. Tasks are automatically removed after they are run // or canceled. // NOTE: We disallow duplicates currently, so this could be a Set<> which might have better // theoretical removal speed, except this list will always be small so ArrayList is fine. - private final ArrayList delayedTasks; + private final ArrayList delayedTasks = new ArrayList<>(); // List of TimerIds to fast-forward delays for. private final ArrayList timerIdsToSkip = new ArrayList<>(); public AsyncQueue() { - delayedTasks = new ArrayList<>(); - executor = new SynchronizedShutdownAwareExecutor(); + this(new SynchronizedShutdownAwareExecutor()); + } + private AsyncQueue(SynchronizedShutdownAwareExecutor executor) { + this.executor = executor; + } + + public void setOnShutdown(Runnable onShutdown) { + executor.setOnShutdown(onShutdown); } public Executor getExecutor() { - return executor; + return executor.internalExecutor; } /** Verifies that the current thread is the managed AsyncQueue thread. */ public void verifyIsCurrentThread() { - Thread current = Thread.currentThread(); - if (executor.thread != current) { - throw fail( - "We are running on the wrong thread. Expected to be on the AsyncQueue " - + "thread %s/%d but was %s/%d", - executor.thread.getName(), executor.thread.getId(), current.getName(), current.getId()); - } + executor.verifyIsCurrentThread(); } /** @@ -446,23 +241,6 @@ public Task enqueue(Runnable task) { }); } - /** - * Queue a Runnable and immediately mark the initiation of shutdown process. Tasks queued after - * this method is called are not run unless they explicitly are requested via {@link - * AsyncQueue#enqueueAndForgetEvenAfterShutdown(Runnable)}. - */ - public Task enqueueAndInitiateShutdown(Runnable task) { - return executor.executeAndInitiateShutdown(task); - } - - /** - * Queue and run this Runnable task immediately after every other already queued task, regardless - * if shutdown has been initiated. - */ - public void enqueueAndForgetEvenAfterShutdown(Runnable task) { - executor.executeEvenAfterShutdown(task); - } - /** Has the shutdown process been initiated. */ public boolean isShuttingDown() { return executor.isShuttingDown(); @@ -522,7 +300,10 @@ public void skipDelaysForTimerId(TimerId timerId) { */ public void panic(Throwable t) { executor.shutdownNow(); + halt(t); + } + static void halt(Throwable t) { // TODO(b/258277574): Migrate to go/firebase-android-executors @SuppressLint("ThreadPoolCreation") Handler handler = new Handler(Looper.getMainLooper()); @@ -580,10 +361,6 @@ public boolean containsDelayedTask(TimerId timerId) { return false; } - public boolean isIdle() { - return executor.internalExecutor.getActiveCount() == 0; - } - /** * Runs some or all delayed tasks early, blocking until completion. * @@ -616,13 +393,23 @@ public void runDelayedTasksUntil(TimerId lastTimerId) throws InterruptedExceptio }); } + /** + * Shuts down the AsyncQueue after which no progress will ever be made again. + */ + public Task shutdown() { + return executor.shutdown(); + } + /** * Shuts down the AsyncQueue and releases resources after which no progress will ever be made - * again. + * again. Attempts to use executor with throw exception. */ - public void shutdown() { - // Will cause the executor to de-reference all threads, the best we can do - executor.setCorePoolSize(0); + public void terminate() { + executor.terminate(); + } + + public AsyncQueue reincarnate() { + return new AsyncQueue(executor.reincarnate()); } /** diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java new file mode 100644 index 00000000000..9ac80ffd0bf --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java @@ -0,0 +1,255 @@ +package com.google.firebase.firestore.util; + +import static com.google.firebase.firestore.util.Assert.fail; +import static com.google.firebase.firestore.util.Assert.hardAssert; + +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; + +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * A wrapper around a {@link ScheduledThreadPoolExecutor} class that provides: + * + *
      + *
    1. Synchronized task scheduling. This is different from function 3, which is about task + * execution in a single thread. + *
    2. Ability to do soft-shutdown: only critical tasks related to shutting Firestore SDK down + * can be executed once the shutdown process initiated. + *
    3. Single threaded execution service, no concurrent execution among the `Runnable`s + * scheduled in this Executor. + *
    + */ +class SynchronizedShutdownAwareExecutor implements Executor { + /** + * The single threaded executor that is backing this Executor. This is also the executor used + * when some tasks explicitly request to run after shutdown has been initiated. + */ + final ScheduledThreadPoolExecutor internalExecutor; + + /** + * Task ss assigned when the shutdown process has been initiated, once it is started, it is not revertable. + */ + private Task shutdownTask; + + private Runnable onShutdown = null; + + /** + * The single thread that will be used by the executor. This is created early and managed + * directly so that it's possible later to make assertions about executing on the correct + * thread. + */ + private final Thread thread; + + /** + * A ThreadFactory for a single, pre-created thread. + */ + private class DelayedStartFactory implements Runnable, ThreadFactory { + private final CountDownLatch latch = new CountDownLatch(1); + private Runnable delegate; + + @Override + public void run() { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + delegate.run(); + } + + @Override + public Thread newThread(@NonNull Runnable runnable) { + hardAssert(delegate == null, "Only one thread may be created in an AsyncQueue."); + delegate = runnable; + latch.countDown(); + return thread; + } + } + + // TODO(b/258277574): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") + public SynchronizedShutdownAwareExecutor() { + DelayedStartFactory threadFactory = new DelayedStartFactory(); + + thread = Executors.defaultThreadFactory().newThread(threadFactory); + thread.setName("FirestoreWorker"); + thread.setDaemon(true); + thread.setUncaughtExceptionHandler((crashingThread, throwable) -> { + shutdownNow(); + AsyncQueue.halt(throwable); + }); + + internalExecutor = + new ScheduledThreadPoolExecutor(1, threadFactory) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t == null && r instanceof Future) { + Future future = (Future) r; + try { + // Not all Futures will be done, for example when used with scheduledAtFixedRate. + if (future.isDone()) { + future.get(); + } + } catch (CancellationException ce) { + // Cancellation exceptions are okay, we expect them to happen sometimes + } catch (ExecutionException ee) { + t = ee.getCause(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (t != null) { + shutdownNow(); + AsyncQueue.halt(t); + } + } + }; + + // Core threads don't time out, this only takes effect when we drop the number of required + // core threads + internalExecutor.setKeepAliveTime(3, TimeUnit.SECONDS); + } + + private SynchronizedShutdownAwareExecutor(ScheduledThreadPoolExecutor internalExecutor, Thread thread) { + this.internalExecutor = internalExecutor; + this.thread = thread; + } + + synchronized void verifyNotShutdown() { + if (shutdownTask != null) { + throw new RejectedExecutionException("AsyncQueue is shutdown"); + } + } + + void setOnShutdown(Runnable onShutdown) { + verifyNotShutdown(); + hardAssert(this.onShutdown == null, "setOnShutdown can only be called once."); + this.onShutdown = onShutdown; + } + + /** + * Synchronized access to isShuttingDown + */ + synchronized boolean isShuttingDown() { + return shutdownTask != null; + } + + /** + * Check if shutdown is initiated before scheduling. If it is initiated, the command will not be + * executed. + */ + + void verifyIsCurrentThread() { + Thread current = Thread.currentThread(); + if (thread != current) { + throw fail( + "We are running on the wrong thread. Expected to be on the AsyncQueue " + + "thread %s/%d but was %s/%d", + thread.getName(), thread.getId(), current.getName(), current.getId()); + } + } + + @Override + public synchronized void execute(Runnable command) { + verifyNotShutdown(); + internalExecutor.execute(command); + } + + /** + * Run a given `Callable` on this executor, and report the result of the `Callable` in a {@link + * Task}. The `Callable` will not be run if the executor started shutting down already. + * + * @return A {@link Task} resolves when the requested `Callable` completes, or reports error + * when the `Callable` runs into exceptions. + */ + Task executeAndReportResult(Callable task) { + final TaskCompletionSource completionSource = new TaskCompletionSource<>(); + try { + this.execute( + () -> { + try { + completionSource.setResult(task.call()); + } catch (Exception e) { + completionSource.setException(e); + throw new RuntimeException(e); + } + }); + } catch (RejectedExecutionException e) { + // The only way we can get here is if the AsyncQueue has panicked and we're now racing with + // the post to the main looper that will crash the app. + Logger.warn(AsyncQueue.class.getSimpleName(), "Refused to enqueue task after panic"); + completionSource.setException(e); + } + return completionSource.getTask(); + } + + /** + * Initiate the shutdown process. Once called, the only possible way to run `Runnable`s are by + * holding the `internalExecutor` reference. + */ + synchronized Task shutdown() { + if (shutdownTask == null) { + shutdownTask = executeAndReportResult(() -> { + if (this.onShutdown != null) { + this.onShutdown.run(); + } + return null; + }); + } + return shutdownTask; + } + + /** + * Initiate the shutdown process and reduce thread pool to 0. + */ + synchronized void terminate() { + shutdown(); + + // Will cause the executor to de-reference all threads, the best we can do + internalExecutor.setCorePoolSize(0); + } + + + synchronized SynchronizedShutdownAwareExecutor reincarnate() { + hardAssert(isShuttingDown(), "Executor must be shutting down to be eligible for reincarnation."); + hardAssert(!isTerminated(), "Cannot reincarnate executor that is terminated."); + return new SynchronizedShutdownAwareExecutor(internalExecutor, thread); + } + + private boolean isTerminated() { + return internalExecutor.getCorePoolSize() == 0; + } + + /** + * Wraps {@link ScheduledThreadPoolExecutor#schedule(Runnable, long, TimeUnit)} and provides + * shutdown state check: the command will not be scheduled if the shutdown has been initiated. + */ + synchronized ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + verifyNotShutdown(); + return internalExecutor.schedule(command, delay, unit); + } + + /** + * Wraps around {@link ScheduledThreadPoolExecutor#shutdownNow()}. + */ + void shutdownNow() { + internalExecutor.shutdownNow(); + } +} diff --git a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto index b29c3359c79..c3e7e82b846 100644 --- a/firebase-firestore/src/proto/google/firestore/v1/firestore.proto +++ b/firebase-firestore/src/proto/google/firestore/v1/firestore.proto @@ -570,31 +570,31 @@ message RunAggregationQueryResponse { message InitRequest { // Token for synchronization. // - // The `db_token`, that was received previously as part of InitResponse, should be + // The `session_token`, that was received previously as part of InitResponse, should be // passed back in the next `InitRequest`. // - // If this is the first time SDK connects, then the `db_token` should be empty. + // If this is the first time SDK connects, then the `session_token` should be empty. // // The token contains database information used to determine whether SDK is out of // sync. Contents are opaque and can change in the future. // - // The `db_token` on the ListenStream has the same contents as the WriteStream. - // Whichever stream was last to receive a `db_token`, is the `db_token` that should + // The `session_token` on the ListenStream has the same contents as the WriteStream. + // Whichever stream was last to receive a `session_token`, is the `session_token` that should // be used as part of the InitRequest, regardless of whether it was from the other // stream. // // The InitResponse will signal whether to `clear_cache`. - bytes db_token = 1; + bytes session_token = 1; } // New message message InitResponse { // Token for synchronization // - // The `db_token` should be returned as part of the next InitRequest. - bytes db_token = 1; + // The `session_token` should be returned as part of the next InitRequest. + bytes session_token = 1; - // Depending on `db_token`, changes may have occurred that require SDK to clear + // Depending on `session_token`, changes may have occurred that require SDK to clear // cache. bool clear_cache = 2; } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java index 70dbbfec8e8..2d9dfad7164 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java @@ -22,6 +22,7 @@ import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.*; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.inOrder; @@ -49,6 +50,8 @@ import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.remote.RemoteStore; +import com.google.firebase.firestore.util.Consumer; +import com.google.protobuf.ByteString; import java.util.ArrayList; import java.util.Arrays; @@ -173,21 +176,30 @@ public void testWillForwardOnOnlineStateChangedCalls() { public void xxx() { Query query = Query.atPath(path("foo/bar")); + Consumer clearPersistenceCallback = spy(new Consumer() { + @Override + public void accept(ByteString value) { + + } + }); EventListener eventListener1 = mock(EventListener.class); EventListener eventListener2 = mock(EventListener.class); QueryListener listener1 = new QueryListener(query, new ListenOptions(), eventListener1); QueryListener listener2 = new QueryListener(query, new ListenOptions(), eventListener2); + SyncEngine syncEngine; + EventManager eventManager; RemoteStore remoteStore = mockRemoteStore(); LocalStore localStore = createLruGcMemoryLocalStore(); - SyncEngine syncEngine = spy(new SyncEngine(localStore, remoteStore, User.UNAUTHENTICATED, 100)); - EventManager eventManager = new EventManager(syncEngine); + syncEngine = spy(new SyncEngine(localStore, remoteStore, User.UNAUTHENTICATED, 100, clearPersistenceCallback)); + eventManager = new EventManager(syncEngine); + eventManager.abortAllTargets(); eventManager.addQueryListener(listener1); eventManager.addQueryListener(listener2); - syncEngine.handleClearCache(); + syncEngine.handleClearPersistence(ByteString.copyFromUtf8("sessionToken")); verify(syncEngine, times(1)) .listen( @@ -202,15 +214,13 @@ public void xxx() { verify(eventListener2, times(1)) .onEvent(isNull(), argThat(abortedExceptionMatcher)); - Mockito.verify(remoteStore, times(1)).listen(any(TargetData.class)); - Mockito.verify(remoteStore, atLeastOnce()).canUseNetwork(); - Mockito.verify(remoteStore, times(1)).disableNetwork(); - Mockito.verify(remoteStore, times(1)).enableNetwork(); - Mockito.verifyNoMoreInteractions(remoteStore); + verify(clearPersistenceCallback, times(1)).accept(ByteString.copyFromUtf8("sessionToken")); - assertTrue(syncEngine.isEmpty()); - assertTrue(eventManager.isEmpty()); - assertTrue(localStore.isEmpty()); + verify(remoteStore, times(1)).listen(any(TargetData.class)); + verify(remoteStore, atLeastOnce()).canUseNetwork(); + verify(remoteStore, times(1)).disableNetwork(); + verify(remoteStore, times(1)).enableNetwork(); + verifyNoMoreInteractions(remoteStore); } @NonNull diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java index dd691616127..d93231ad215 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/CountingQueryEngine.java @@ -159,11 +159,6 @@ public void removeAll(Collection keys) { subject.removeAll(keys); } - @Override - public void clear() { - subject.clear(); - } - @Override public MutableDocument get(DocumentKey documentKey) { MutableDocument result = subject.get(documentKey); @@ -205,11 +200,6 @@ public Map getDocumentsMatchingQuery( documentsReadByCollection[0] += result.size(); return result; } - - @Override - public boolean isEmpty() { - return subject.isEmpty(); - } }; } @@ -266,20 +256,8 @@ public Map getOverlays( return result; } - @Override - public void clear() { - subject.clear(); - } - - @Override - public boolean isEmpty() { - return subject.isEmpty(); - } - private OverlayType getOverlayType(Overlay overlay) { - if (overlay == null) { - return null; - } else if (overlay.getMutation() instanceof SetMutation) { + if (overlay.getMutation() instanceof SetMutation) { return OverlayType.Set; } else if (overlay.getMutation() instanceof PatchMutation) { return OverlayType.Patch; diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/GlobalsCacheTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/GlobalsCacheTest.java index 2693277ed95..fde94288282 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/GlobalsCacheTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/GlobalsCacheTest.java @@ -45,7 +45,7 @@ public void tearDown() { @Test public void setAndGetDbToken() { ByteString value = ByteString.copyFrom("TestData", StandardCharsets.UTF_8); - globalsCache.setDbToken(value); - assertEquals(value, globalsCache.getDbToken()); + globalsCache.setSessionToken(value); + assertEquals(value, globalsCache.getSessionsToken()); } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java index f7716430a01..2b2fef7159f 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/local/LocalStoreTestCase.java @@ -958,25 +958,6 @@ public void testCanExecuteDocumentQueries() { .containsExactly(doc("foo/bar", 0, map("foo", "bar")).setHasLocalMutations()); } - @Test - public void testCanExecuteDocumentQueriesAfterClearingCacheData() { - localStore.writeLocally( - asList( - setMutation("foo/bar", map("foo", "bar")), - setMutation("foo/baz", map("foo", "baz")), - setMutation("foo/bar/Foo/Bar", map("Foo", "Bar")))); - Query query = Query.atPath(ResourcePath.fromSegments(asList("foo", "bar"))); - QueryResult resultBefore = localStore.executeQuery(query, /* usePreviousResults= */ true); - - localStore.clearCacheData(); - - assertThat(values(resultBefore.getDocuments())) - .containsExactly(doc("foo/bar", 0, map("foo", "bar")).setHasLocalMutations()); - - QueryResult resultAfter = localStore.executeQuery(query, /* usePreviousResults= */ true); - assertThat(values(resultAfter.getDocuments())).isEmpty(); - } - @Test public void testCanExecuteCollectionQueries() { localStore.writeLocally( @@ -994,30 +975,6 @@ public void testCanExecuteCollectionQueries() { doc("foo/baz", 0, map("foo", "baz")).setHasLocalMutations()); } - @Test - public void testCanExecuteCollectionQueriesAfterClearingCacheData() { - localStore.writeLocally( - asList( - setMutation("fo/bar", map("fo", "bar")), - setMutation("foo/bar", map("foo", "bar")), - setMutation("foo/baz", map("foo", "baz")), - setMutation("foo/bar/Foo/Bar", map("Foo", "Bar")), - setMutation("fooo/blah", map("fooo", "blah")))); - Query query = query("foo"); - QueryResult resultBefore = localStore.executeQuery(query, /* usePreviousResults= */ true); - - localStore.clearCacheData(); - - assertThat(values(resultBefore.getDocuments())) - .containsExactly( - doc("foo/bar", 0, map("foo", "bar")).setHasLocalMutations(), - doc("foo/baz", 0, map("foo", "baz")).setHasLocalMutations()); - - QueryResult resultAfter = localStore.executeQuery(query, /* usePreviousResults= */ true); - - assertThat(values(resultAfter.getDocuments())).isEmpty(); - } - @Test public void testCanExecuteMixedCollectionQueries() { Query query = query("foo"); @@ -1036,31 +993,6 @@ public void testCanExecuteMixedCollectionQueries() { doc("foo/bonk", 0, map("a", "b")).setHasLocalMutations()); } - @Test - public void testCanExecuteMixedCollectionQueriesAfterClearingCacheData() { - Query query = query("foo"); - allocateQuery(query); - assertTargetId(2); - - applyRemoteEvent(updateRemoteEvent(doc("foo/baz", 10, map("a", "b")), asList(2), emptyList())); - applyRemoteEvent(updateRemoteEvent(doc("foo/bar", 20, map("a", "b")), asList(2), emptyList())); - writeMutation(setMutation("foo/bonk", map("a", "b"))); - - QueryResult resultBefore = localStore.executeQuery(query, /* usePreviousResults= */ true); - - localStore.clearCacheData(); - - assertThat(values(resultBefore.getDocuments())) - .containsExactly( - doc("foo/bar", 20, map("a", "b")), - doc("foo/baz", 10, map("a", "b")), - doc("foo/bonk", 0, map("a", "b")).setHasLocalMutations()); - - QueryResult resultAfter = localStore.executeQuery(query, /* usePreviousResults= */ true); - - assertThat(values(resultAfter.getDocuments())).isEmpty(); - } - @Test public void testReadsAllDocumentsForInitialCollectionQueries() { Query query = query("foo"); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java index 998201b497c..b33ebb8af7c 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java @@ -31,6 +31,7 @@ import com.google.firebase.firestore.testutil.EmptyCredentialsProvider; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Util; +import com.google.firestore.v1.InitResponse; import com.google.protobuf.ByteString; import io.grpc.Status; @@ -85,10 +86,14 @@ public boolean isOpen() { } @Override - void sendHandshake(ByteString dbToken) { + void sendHandshake(ByteString sessionToken) { hardAssert(!handshakeComplete, "Handshake already completed"); handshakeComplete = true; - getWorkerQueue().enqueue(() -> listener.onHandshakeComplete(dbToken == null ? ByteString.EMPTY :dbToken, false)); + InitResponse initResponse = InitResponse.newBuilder() + .setSessionToken(sessionToken == null ? ByteString.EMPTY : sessionToken) + .setClearCache(false) + .build(); + getWorkerQueue().enqueue(() -> listener.onHandshake(initResponse)); } @Override @@ -189,11 +194,15 @@ public boolean isOpen() { } @Override - public void sendHandshake(ByteString dbToken) { + public void sendHandshake(ByteString sessionToken) { hardAssert(!handshakeComplete, "Handshake already completed"); writeStreamRequestCount += 1; handshakeComplete = true; - getWorkerQueue().enqueue(() -> listener.onHandshakeComplete(dbToken == null ? ByteString.EMPTY :dbToken, false)); + InitResponse initResponse = InitResponse.newBuilder() + .setSessionToken(sessionToken == null ? ByteString.EMPTY : sessionToken) + .setClearCache(false) + .build(); + getWorkerQueue().enqueue(() -> listener.onHandshake(initResponse)); } @Override diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index a390948dd4e..dce129af472 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -168,7 +168,7 @@ public abstract class SpecTestCase implements RemoteStoreCallback { private boolean useEagerGcForMemory; private int maxConcurrentLimboResolutions; - private boolean networkEnabled = true; + private boolean networkEnabled = false; // // Parts of the Firestore system that the spec tests need to control. @@ -325,7 +325,8 @@ private void initClient() { datastore, currentUser, maxConcurrentLimboResolutions, - new FirebaseFirestoreSettings.Builder().build()); + new FirebaseFirestoreSettings.Builder().build(), + null); ComponentProvider provider = initializeComponentProvider(configuration, useEagerGcForMemory); localPersistence = provider.getPersistence(); @@ -337,6 +338,7 @@ private void initClient() { localStore = provider.getLocalStore(); syncEngine = provider.getSyncEngine(); eventManager = provider.getEventManager(); + remoteStore.enableNetwork(); } @Override @@ -345,8 +347,8 @@ public void handleOnlineStateChange(OnlineState onlineState) { } @Override - public void clearCacheData() { - syncEngine.clearCacheData(); + public void handleClearPersistence(ByteString sessionToken) { + syncEngine.handleClearPersistence(sessionToken); } private List>> getCurrentOutstandingWrites() { @@ -1295,9 +1297,9 @@ private void runSteps(JSONArray steps, JSONObject config) throws Exception { backgroundExecutor.execute(() -> drainBackgroundQueue.setResult(null)); waitFor(drainBackgroundQueue.getTask()); - while (!queue.isIdle()) { - Thread.sleep(1); - } +// while (!queue.isIdle()) { +// Thread.sleep(1); +// } if (expectedSnapshotEvents != null) { log(" Validating expected snapshot events " + expectedSnapshotEvents); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/AsyncQueueTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/AsyncQueueTest.java index b6f817ef4d8..d0aa8923ccb 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/AsyncQueueTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/AsyncQueueTest.java @@ -131,11 +131,9 @@ public void tasksAreScheduledWithRespectToShutdown() { // From this point on, `normal` tasks are not scheduled. Only those who explicitly request to // run after shutdown initiated will run. - queue.enqueueAndInitiateShutdown(runnableForStep(2)); + queue.shutdown(); queue.enqueueAndForget(runnableForStep(3)); - queue.enqueueAndForgetEvenAfterShutdown(runnableForStep(4)); - queue.getExecutor().execute(runnableForStep(5)); waitForExpectedSteps(); } From 1706efef702d5b788dd17cba4d801e153d33cd87 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Tue, 4 Jun 2024 16:03:34 -0400 Subject: [PATCH 15/30] Cleanup --- .../firebase/firestore/AccessHelper.java | 16 ++++---- .../firebase/firestore/remote/StreamTest.java | 20 ++++----- .../firestore/core/FirestoreClient.java | 9 +--- .../firebase/firestore/core/SyncEngine.java | 3 -- .../firestore/remote/AbstractStream.java | 30 +++++++------- .../firebase/firestore/remote/Datastore.java | 1 - .../firestore/remote/RemoteStore.java | 41 ++++++------------- .../firestore/remote/WatchStream.java | 2 - .../firestore/remote/WriteStream.java | 3 -- 9 files changed, 47 insertions(+), 78 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java index a4fc82cef35..23c38a3a7c9 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java @@ -15,9 +15,7 @@ package com.google.firebase.firestore; import android.content.Context; - import androidx.core.util.Supplier; - import com.google.firebase.FirebaseApp; import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.User; @@ -29,13 +27,13 @@ public final class AccessHelper { /** Makes the FirebaseFirestore constructor accessible. */ public static FirebaseFirestore newFirebaseFirestore( - Context context, - DatabaseId databaseId, - String persistenceKey, - Supplier> authProviderFactory, - Supplier> appCheckTokenProviderFactory, - FirebaseApp firebaseApp, - FirebaseFirestore.InstanceRegistry instanceRegistry) { + Context context, + DatabaseId databaseId, + String persistenceKey, + Supplier> authProviderFactory, + Supplier> appCheckTokenProviderFactory, + FirebaseApp firebaseApp, + FirebaseFirestore.InstanceRegistry instanceRegistry) { return new FirebaseFirestore( context, databaseId, diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java index d8f08deb654..919807f290c 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java @@ -83,21 +83,11 @@ private static class StreamStatusCallback implements WatchStream.Callback, Write final Semaphore handshakeSemaphore = new Semaphore(0); final Semaphore responseReceivedSemaphore = new Semaphore(0); - @Override - public void onHandshake(InitResponse initResponse) { - handshakeSemaphore.release(); - } - @Override public void onWatchChange(SnapshotVersion snapshotVersion, WatchChange watchChange) { watchChangeSemaphore.release(); } - @Override - public void onHandshakeReady() { - handshakeReadySemaphore.release(); - } - @Override public void onOpen() { openSemaphore.release(); @@ -108,6 +98,16 @@ public void onClose(Status status) { closeSemaphore.release(); } + @Override + public void onHandshakeReady() { + handshakeReadySemaphore.release(); + } + + @Override + public void onHandshake(InitResponse initResponse) { + handshakeSemaphore.release(); + } + @Override public void onWriteResponse( SnapshotVersion commitVersion, List mutationResults) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index a8c29ad5478..399e216d560 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -101,7 +101,7 @@ public FirestoreClient( this.appCheckProvider = appCheckProvider; this.asyncQueue = asyncQueue; this.bundleSerializer = - new BundleSerializer(new RemoteSerializer(databaseInfo.getDatabaseId())); + new BundleSerializer(new RemoteSerializer(databaseInfo.getDatabaseId())); } public void start( @@ -204,7 +204,7 @@ public boolean isTerminated() { /** Starts listening to a query. */ public ListenerRegistration listen( - Query query, ListenOptions options, @Nullable Activity activity, AsyncEventListener listener) { + Query query, ListenOptions options, @Nullable Activity activity, AsyncEventListener listener) { this.verifyNotTerminated(); QueryListener queryListener = new QueryListener(query, options, listener); asyncQueue.enqueueAndForget(() -> eventManager.addQueryListener(queryListener)); @@ -407,11 +407,6 @@ private void verifyNotTerminated() { } } - public AsyncQueue getAsyncQueue() { - verifyNotTerminated(); - return asyncQueue; - } - public void setSessionToken(ByteString sessionToken) { verifyNotTerminated(); asyncQueue.enqueueAndForget(() -> localStore.setSessionsToken(sessionToken)); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index c39422e3bf1..c8b41b8d612 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -51,12 +51,9 @@ import com.google.firebase.firestore.remote.RemoteEvent; import com.google.firebase.firestore.remote.RemoteStore; import com.google.firebase.firestore.remote.TargetChange; -import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Consumer; -import com.google.firebase.firestore.util.Function; import com.google.firebase.firestore.util.Logger; import com.google.firebase.firestore.util.Util; -import com.google.firestore.v1.Value; import com.google.protobuf.ByteString; import io.grpc.Status; import java.io.IOException; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java index 1d9fffd5ea8..c1cd01dfbe9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AbstractStream.java @@ -111,21 +111,21 @@ public void onHeaders(Metadata headers) { public void onNext(RespT response) { final int currentResponseCount = responseCount + 1; dispatcher.run( - () -> { - if (Logger.isDebugEnabled()) { - Logger.debug( - AbstractStream.this.getClass().getSimpleName(), - "(%x) Stream received (%s): %s", - System.identityHashCode(AbstractStream.this), - currentResponseCount, - response); - } - if (currentResponseCount == 1) { - AbstractStream.this.onFirst(response); - } else { - AbstractStream.this.onNext(response); - } - }); + () -> { + if (Logger.isDebugEnabled()) { + Logger.debug( + AbstractStream.this.getClass().getSimpleName(), + "(%x) Stream received (%s): %s", + System.identityHashCode(AbstractStream.this), + currentResponseCount, + response); + } + if (currentResponseCount == 1) { + AbstractStream.this.onFirst(response); + } else { + AbstractStream.this.onNext(response); + } + }); responseCount = currentResponseCount; } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index ee04ef42263..92e6ae85888 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -21,7 +21,6 @@ import android.os.Build; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; - import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.firestore.AggregateField; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index 8b73668ed13..e0602c0e7de 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -156,11 +156,11 @@ public interface RemoteStoreCallback { private final Deque writePipeline; public RemoteStore( - RemoteStoreCallback remoteStoreCallback, - LocalStore localStore, - Datastore datastore, - AsyncQueue workerQueue, - ConnectivityMonitor connectivityMonitor) { + RemoteStoreCallback remoteStoreCallback, + LocalStore localStore, + Datastore datastore, + AsyncQueue workerQueue, + ConnectivityMonitor connectivityMonitor) { this.remoteStoreCallback = remoteStoreCallback; this.localStore = localStore; this.datastore = datastore; @@ -176,13 +176,6 @@ public RemoteStore( watchStream = datastore.createWatchStream( new WatchStream.Callback() { - @Override - public void onHandshakeReady() { - if (!writeStream.isHandshakeInProgress()) { - watchStream.sendHandshake(localStore.getSessionToken()); - } - } - @Override public void onHandshake(InitResponse initResponse) { if (initResponse.getClearCache()) { @@ -194,7 +187,9 @@ public void onHandshake(InitResponse initResponse) { @Override public void onOpen() { - handleWatchStreamOpen(); + if (!writeStream.isHandshakeInProgress()) { + watchStream.sendHandshake(localStore.getSessionToken()); + } } @Override @@ -211,13 +206,6 @@ public void onClose(Status status) { writeStream = datastore.createWriteStream( new WriteStream.Callback() { - @Override - public void onHandshakeReady() { - if (!watchStream.isHandshakeInProgress()) { - writeStream.sendHandshake(localStore.getSessionToken()); - } - } - @Override public void onHandshake(InitResponse initResponse) { if (initResponse.getClearCache()) { @@ -229,7 +217,9 @@ public void onHandshake(InitResponse initResponse) { @Override public void onOpen() { - handleWriteStreamOpen(); + if (!watchStream.isHandshakeInProgress()) { + writeStream.sendHandshake(localStore.getSessionToken()); + } } @Override @@ -482,9 +472,7 @@ private void handleWatchStreamHandshakeComplete(ByteString sessionToken) { if (writeStream.isHandshakeInProgress()) { writeStream.sendHandshake(sessionToken); } - } - private void handleWatchStreamOpen() { // Restore any existing watches. for (TargetData targetData : listenTargets.values()) { sendWatchRequest(targetData); @@ -617,14 +605,13 @@ private void raiseWatchSnapshot(SnapshotVersion snapshotVersion) { } private void processTargetError(WatchTargetChange targetChange) { - Status cause = targetChange.getCause(); - hardAssert(cause != null, "Processing target error without a cause"); + hardAssert(targetChange.getCause() != null, "Processing target error without a cause"); for (Integer targetId : targetChange.getTargetIds()) { // Ignore targets that have been removed already. if (listenTargets.containsKey(targetId)) { listenTargets.remove(targetId); watchChangeAggregator.removeTarget(targetId); - remoteStoreCallback.handleRejectedListen(targetId, cause); + remoteStoreCallback.handleRejectedListen(targetId, targetChange.getCause()); } } } @@ -707,9 +694,7 @@ private void handleWriteStreamHandshakeComplete(ByteString sessionToken) { // Record the stream token. localStore.setLastStreamToken(writeStream.getLastStreamToken()); - } - private void handleWriteStreamOpen() { // Send the write pipeline now that stream is established. for (MutationBatch batch : writePipeline) { writeStream.writeMutations(batch.getMutations()); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java index 56c0618a71c..01bd53605cc 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java @@ -48,8 +48,6 @@ public class WatchStream /** A callback interface for the set of events that can be emitted by the WatchStream */ interface Callback extends AbstractStream.StreamCallback { - void onHandshakeReady(); - /** The handshake for this write stream has completed */ void onHandshake(InitResponse initResponse); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java index d5842652b8f..3c20d8a007d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java @@ -55,9 +55,6 @@ public class WriteStream extends AbstractStream Date: Tue, 4 Jun 2024 16:16:57 -0400 Subject: [PATCH 16/30] Cleanup --- .../com/google/firebase/firestore/DocumentReference.java | 6 ++++-- .../com/google/firebase/firestore/FirebaseFirestore.java | 2 -- .../java/com/google/firebase/firestore/util/AsyncQueue.java | 3 ++- .../google/firebase/firestore/core/EventManagerTest.java | 2 -- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java index 9e035f2fb53..7ab549210cd 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/DocumentReference.java @@ -227,7 +227,8 @@ public Task update( } private Task update(@NonNull ParsedUpdateData parsedData) { - return firestore.callClient(client -> client.write(singletonList(parsedData.toMutation(key, Precondition.exists(true))))) + List mutations = singletonList(parsedData.toMutation(key, Precondition.exists(true))); + return firestore.callClient(client -> client.write(mutations)) .continueWith(Executors.DIRECT_EXECUTOR, voidErrorTransformer()); } @@ -523,7 +524,8 @@ private ListenerRegistration addSnapshotListenerInternal( AsyncEventListener asyncListener = new AsyncEventListener<>(userExecutor, viewListener); - return firestore.callClient(client -> client.listen(asQuery(), options, activity, asyncListener)); + com.google.firebase.firestore.core.Query query = asQuery(); + return firestore.callClient(client -> client.listen(query, options, activity, asyncListener)); } @Override diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 4580883e8e9..68777f5f265 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -197,8 +197,6 @@ static FirebaseFirestore newInstance( return firestore; } - - @VisibleForTesting FirebaseFirestore( Context context, diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java index 83cc9dade6a..678e607efb6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java @@ -206,6 +206,7 @@ public void setOnShutdown(Runnable onShutdown) { } public Executor getExecutor() { + // Returns internal executor that will continue to work, even after shutdown() return executor.internalExecutor; } @@ -402,7 +403,7 @@ public Task shutdown() { /** * Shuts down the AsyncQueue and releases resources after which no progress will ever be made - * again. Attempts to use executor with throw exception. + * again. Attempts to use executor will throw exception. */ public void terminate() { executor.terminate(); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java index 2d9dfad7164..e987d58541a 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java @@ -16,7 +16,6 @@ import static com.google.firebase.firestore.testutil.TestUtil.path; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.argThat; @@ -62,7 +61,6 @@ import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; import org.mockito.InOrder; -import org.mockito.Mockito; import org.mockito.stubbing.Answer; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; From 203873df1f44935be24e08a08312c25a7812ee92 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 5 Jun 2024 15:47:31 -0400 Subject: [PATCH 17/30] Comments --- .../com/google/firebase/firestore/FirestoreClientProvider.java | 3 +++ .../com/google/firebase/firestore/local/SQLitePersistence.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java index c2e1554c9b4..46db3b32971 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java @@ -57,7 +57,10 @@ final class FirestoreClientProvider { @GuardedBy("this") private boolean networkEnabled = true; + @GuardedBy("this") @Nullable private EmulatedServiceSettings emulatorSettings; + + @GuardedBy("this") private ByteString sessionToken; FirestoreClientProvider( diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java index 5bc77f2b5ba..e3f77e846a4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java @@ -242,6 +242,8 @@ T runTransaction(String action, Supplier operation) { public static void clearPersistence(Context context, DatabaseId databaseId, String persistenceKey) throws FirebaseFirestoreException { + //TODO Could we change this with SQLiteDatabase.deleteDatabase(). + String databaseName = SQLitePersistence.databaseName(persistenceKey, databaseId); String sqLitePath = context.getDatabasePath(databaseName).getPath(); String journalPath = sqLitePath + "-journal"; From 9bc769267a1522b6cbe3f13481d3782939d652db Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 5 Jun 2024 20:36:52 -0400 Subject: [PATCH 18/30] Fix --- .../firebase/firestore/remote/StreamTest.java | 14 ++++++++------ .../firebase/firestore/remote/WatchStream.java | 2 +- .../firebase/firestore/remote/WriteStream.java | 5 ++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java index 919807f290c..cd04370036f 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java @@ -45,7 +45,11 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.Timeout; import org.junit.runner.RunWith; class MockCredentialsProvider extends EmptyCredentialsProvider { @@ -70,6 +74,10 @@ public List observedStates() { @RunWith(AndroidJUnit4.class) public class StreamTest { + + @Rule + public Timeout timeout = new Timeout(10, TimeUnit.SECONDS); + /** Single mutation to send to the write stream. */ private static final List mutations = Collections.singletonList(setMutation("foo/bar", map())); @@ -79,7 +87,6 @@ private static class StreamStatusCallback implements WatchStream.Callback, Write final Semaphore openSemaphore = new Semaphore(0); final Semaphore closeSemaphore = new Semaphore(0); final Semaphore watchChangeSemaphore = new Semaphore(0); - final Semaphore handshakeReadySemaphore = new Semaphore(0); final Semaphore handshakeSemaphore = new Semaphore(0); final Semaphore responseReceivedSemaphore = new Semaphore(0); @@ -98,11 +105,6 @@ public void onClose(Status status) { closeSemaphore.release(); } - @Override - public void onHandshakeReady() { - handshakeReadySemaphore.release(); - } - @Override public void onHandshake(InitResponse initResponse) { handshakeSemaphore.release(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java index 01bd53605cc..3b3ba0c7520 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java @@ -106,7 +106,7 @@ boolean isHandshakeInProgress() { * accept watch queries. */ boolean isHandshakeComplete() { - return handshakeComplete; + return isOpen() && handshakeComplete; } /** diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java index 3c20d8a007d..02b40ac42a5 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java @@ -106,7 +106,7 @@ boolean isHandshakeInProgress() { * accept mutations. */ boolean isHandshakeComplete() { - return handshakeComplete; + return isOpen() && handshakeComplete; } /** @@ -173,9 +173,8 @@ void writeMutations(List mutations) { @Override public void onFirst(WriteResponse response) { - lastStreamToken = response.getStreamToken(); - hardAssert(response.hasInitResponse(),"InitResponse expected as part of Handshake response"); + lastStreamToken = response.getStreamToken(); // The first response is the handshake response handshakeComplete = true; From 6a0fd9a45da43e5731632db03a26184f79d54e46 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 5 Jun 2024 23:43:20 -0400 Subject: [PATCH 19/30] Refactor parts back into FirebaseFirestore --- .../firebase/firestore/FirebaseFirestore.java | 195 +++++++++++--- .../firestore/FirestoreClientProvider.java | 251 +++--------------- .../PersistentCacheIndexManager.java | 6 +- .../firestore/core/FirestoreClient.java | 12 +- 4 files changed, 207 insertions(+), 257 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 68777f5f265..503c4dfcd28 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -26,17 +26,24 @@ import androidx.annotation.VisibleForTesting; import androidx.core.util.Supplier; import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.FirebaseApp; import com.google.firebase.annotations.PreviewApi; import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider; import com.google.firebase.auth.internal.InternalAuthProvider; +import com.google.firebase.emulators.EmulatedServiceSettings; import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.FirebaseAppCheckTokenProvider; import com.google.firebase.firestore.auth.FirebaseAuthCredentialsProvider; import com.google.firebase.firestore.auth.User; import com.google.firebase.firestore.core.AsyncEventListener; +import com.google.firebase.firestore.core.ComponentProvider; +import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.core.FirestoreClient; +import com.google.firebase.firestore.core.MemoryComponentProvider; +import com.google.firebase.firestore.core.SQLiteComponentProvider; +import com.google.firebase.firestore.local.SQLitePersistence; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.FieldIndex; import com.google.firebase.firestore.model.FieldPath; @@ -51,6 +58,8 @@ import com.google.firebase.firestore.util.Logger.Level; import com.google.firebase.firestore.util.Preconditions; import com.google.firebase.inject.Deferred; +import com.google.protobuf.ByteString; + import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.ByteBuffer; @@ -70,6 +79,9 @@ */ public class FirebaseFirestore { + private volatile ByteString sessionToken; + private boolean networkEnabled = false; + /** * Provides a registry management interface for {@code FirebaseFirestore} instances. * @@ -80,15 +92,23 @@ public interface InstanceRegistry { void remove(@NonNull String databaseId); } + private static final String TAG = "FirebaseFirestore"; + private final Context context; // This is also used as private lock object for this instance. There is nothing inherent about // databaseId itself that needs locking; it just saves us creating a separate lock object. private final DatabaseId databaseId; + private final String persistenceKey; + private final Supplier> authProviderFactory; + private final Supplier> appCheckTokenProviderFactory; private final FirebaseApp firebaseApp; private final UserDataReader userDataReader; // When user requests to terminate, use this to notify `FirestoreMultiDbComponent` to deregister // this instance. private final InstanceRegistry instanceRegistry; - private final FirestoreClientProvider client; + @Nullable private EmulatedServiceSettings emulatorSettings; + private FirebaseFirestoreSettings settings; + private final FirestoreClientProvider clientProvider; + private final GrpcMetadataProvider metadataProvider; @Nullable private PersistentCacheIndexManager persistentCacheIndexManager; @@ -207,18 +227,25 @@ static FirebaseFirestore newInstance( @Nullable FirebaseApp firebaseApp, InstanceRegistry instanceRegistry, @Nullable GrpcMetadataProvider metadataProvider) { - this.client = new FirestoreClientProvider(context, databaseId, persistenceKey, authProviderFactory, appCheckTokenProviderFactory, metadataProvider); + this.context = checkNotNull(context); this.databaseId = checkNotNull(checkNotNull(databaseId)); this.userDataReader = new UserDataReader(databaseId); + this.persistenceKey = checkNotNull(persistenceKey); + this.authProviderFactory = checkNotNull(authProviderFactory); + this.appCheckTokenProviderFactory = checkNotNull(appCheckTokenProviderFactory); + this.clientProvider = new FirestoreClientProvider(this::newClient); // NOTE: We allow firebaseApp to be null in tests only. this.firebaseApp = firebaseApp; this.instanceRegistry = instanceRegistry; + this.metadataProvider = metadataProvider; } /** Returns the settings used by this {@code FirebaseFirestore} object. */ @NonNull public FirebaseFirestoreSettings getFirestoreSettings() { - return client.getFirestoreSettings(); + synchronized (clientProvider) { + return settings; + } } /** @@ -226,7 +253,21 @@ public FirebaseFirestoreSettings getFirestoreSettings() { * can only be called before calling any other methods on this object. */ public void setFirestoreSettings(@NonNull FirebaseFirestoreSettings settings) { - client.setFirestoreSettings(settings); + checkNotNull(settings, "Provided settings must not be null."); + synchronized (clientProvider) { + settings = mergeEmulatorSettings(settings, this.emulatorSettings); + + // As a special exception, don't throw if the same settings are passed repeatedly. This + // should make it simpler to get a Firestore instance in an activity. + if (clientProvider.isConfigured() && !this.settings.equals(settings)) { + throw new IllegalStateException( + "FirebaseFirestore has already been started and its settings can no longer be changed. " + + "You can only call setFirestoreSettings() before calling any other methods on a " + + "FirebaseFirestore object."); + } + + this.settings = settings; + } } /** @@ -238,11 +279,74 @@ public void setFirestoreSettings(@NonNull FirebaseFirestoreSettings settings) { * @param port the emulator port (for example, 8080) */ public void useEmulator(@NonNull String host, int port) { - client.useEmulator(host, port); + synchronized (clientProvider) { + if (clientProvider.isConfigured()) { + throw new IllegalStateException( + "Cannot call useEmulator() after instance has already been initialized."); + } + + emulatorSettings = new EmulatedServiceSettings(host, port); + settings = mergeEmulatorSettings(settings, emulatorSettings); + } } - private void ensureClientConfigured() { - client.ensureClientConfigured(); + private FirestoreClient newClient(AsyncQueue asyncQueue) { + DatabaseInfo databaseInfo = + new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled()); + + FirestoreClient client = new FirestoreClient( + databaseInfo, + settings, + authProviderFactory.get(), + appCheckTokenProviderFactory.get(), + asyncQueue, + metadataProvider); + + client.setClearPersistenceCallback(sessionToken -> { + clientProvider.ifCurrentClient(client, () -> { + this.sessionToken = sessionToken; + clearPersistence(); + }); + }); + + client.start( + context, + newComponentProvider()); + + if (sessionToken != null) { + client.setSessionToken(sessionToken); + } + + if (networkEnabled) { + client.enableNetwork(); + } + + return client; + } + + private ComponentProvider newComponentProvider() { + return settings.isPersistenceEnabled() + ? new SQLiteComponentProvider() + : new MemoryComponentProvider(); + } + + private FirebaseFirestoreSettings mergeEmulatorSettings( + @NonNull FirebaseFirestoreSettings settings, + @Nullable EmulatedServiceSettings emulatorSettings) { + if (emulatorSettings == null) { + return settings; + } + + if (!FirebaseFirestoreSettings.DEFAULT_HOST.equals(settings.getHost())) { + Logger.warn( + TAG, + "Host has been set in FirebaseFirestoreSettings and useEmulator, emulator host will be used."); + } + + return new FirebaseFirestoreSettings.Builder(settings) + .setHost(emulatorSettings.getHost() + ":" + emulatorSettings.getPort()) + .setSslEnabled(false) + .build(); } /** Returns the FirebaseApp instance to which this {@code FirebaseFirestore} belongs. */ @@ -273,9 +377,9 @@ public FirebaseApp getApp() { @PreviewApi @NonNull public Task setIndexConfiguration(@NonNull String json) { - ensureClientConfigured(); + clientProvider.ensureConfigured(); Preconditions.checkState( - client.getFirestoreSettings().isPersistenceEnabled(), "Cannot enable indexes when persistence is disabled"); + settings.isPersistenceEnabled(), "Cannot enable indexes when persistence is disabled"); List parsedIndexes = new ArrayList<>(); @@ -316,7 +420,7 @@ public Task setIndexConfiguration(@NonNull String json) { throw new IllegalArgumentException("Failed to parse index configuration", e); } - return client.safeCall(client -> client.configureFieldIndexes(parsedIndexes)); + return clientProvider.call(client -> client.configureFieldIndexes(parsedIndexes)); } /** @@ -330,14 +434,12 @@ public Task setIndexConfiguration(@NonNull String json) { * not in use. */ @Nullable - public synchronized PersistentCacheIndexManager getPersistentCacheIndexManager() { - ensureClientConfigured(); - if (persistentCacheIndexManager == null) { - FirebaseFirestoreSettings settings = client.getFirestoreSettings(); - if (settings.isPersistenceEnabled() - || settings.getCacheSettings() instanceof PersistentCacheSettings) { - persistentCacheIndexManager = new PersistentCacheIndexManager(client); - } + public PersistentCacheIndexManager getPersistentCacheIndexManager() { + clientProvider.ensureConfigured(); + if (persistentCacheIndexManager == null + && (settings.isPersistenceEnabled() + || settings.getCacheSettings() instanceof PersistentCacheSettings)) { + persistentCacheIndexManager = new PersistentCacheIndexManager(clientProvider); } return persistentCacheIndexManager; } @@ -352,7 +454,7 @@ public synchronized PersistentCacheIndexManager getPersistentCacheIndexManager() @NonNull public CollectionReference collection(@NonNull String collectionPath) { checkNotNull(collectionPath, "Provided collection path must not be null."); - ensureClientConfigured(); + clientProvider.ensureConfigured(); return new CollectionReference(ResourcePath.fromString(collectionPath), this); } @@ -366,7 +468,7 @@ public CollectionReference collection(@NonNull String collectionPath) { @NonNull public DocumentReference document(@NonNull String documentPath) { checkNotNull(documentPath, "Provided document path must not be null."); - ensureClientConfigured(); + clientProvider.ensureConfigured(); return DocumentReference.forPath(ResourcePath.fromString(documentPath), this); } @@ -387,7 +489,7 @@ public Query collectionGroup(@NonNull String collectionId) { "Invalid collectionId '%s'. Collection IDs must not contain '/'.", collectionId)); } - ensureClientConfigured(); + clientProvider.ensureConfigured(); return new Query( new com.google.firebase.firestore.core.Query(ResourcePath.EMPTY, collectionId), this); } @@ -409,7 +511,7 @@ public Query collectionGroup(@NonNull String collectionId) { */ private Task runTransaction( TransactionOptions options, Transaction.Function updateFunction, Executor executor) { - ensureClientConfigured(); + clientProvider.ensureConfigured(); // We wrap the function they provide in order to // 1. Use internal implementation classes for Transaction, @@ -423,7 +525,7 @@ private Task runTransaction( updateFunction.apply( new Transaction(internalTransaction, FirebaseFirestore.this))); - return client.safeCall(client -> client.transaction(options, wrappedUpdateFunction)); + return clientProvider.call(client -> client.transaction(options, wrappedUpdateFunction)); } /** @@ -474,7 +576,7 @@ public Task runTransaction( */ @NonNull public WriteBatch batch() { - ensureClientConfigured(); + clientProvider.ensureConfigured(); return new WriteBatch(this); } @@ -516,7 +618,7 @@ public Task runBatch(@NonNull WriteBatch.Function batchFunction) { @NonNull public Task terminate() { instanceRegistry.remove(this.getDatabaseId().getDatabaseId()); - return client.terminate(); + return clientProvider.terminate(); } /** @@ -535,12 +637,12 @@ public Task terminate() { */ @NonNull public Task waitForPendingWrites() { - return client.safeCall(FirestoreClient::waitForPendingWrites); + return clientProvider.call(FirestoreClient::waitForPendingWrites); } @VisibleForTesting AsyncQueue getAsyncQueue() { - return client.getAsyncQueue(); + return clientProvider.getAsyncQueue(); } /** @@ -550,7 +652,14 @@ AsyncQueue getAsyncQueue() { */ @NonNull public Task enableNetwork() { - return client.enableNetwork(); + synchronized (clientProvider) { + networkEnabled = true; + if (clientProvider.isConfigured()) { + return clientProvider.call(FirestoreClient::enableNetwork); + } else { + return Tasks.forResult(null); + } + } } /** @@ -562,7 +671,14 @@ public Task enableNetwork() { */ @NonNull public Task disableNetwork() { - return client.disableNetwork(); + synchronized (clientProvider) { + networkEnabled = false; + if (clientProvider.isConfigured()) { + return clientProvider.call(FirestoreClient::disableNetwork); + } else { + return Tasks.forResult(null); + } + } } /** Globally enables / disables Cloud Firestore logging for the SDK. */ @@ -594,7 +710,18 @@ public static void setLoggingEnabled(boolean loggingEnabled) { */ @NonNull public Task clearPersistence() { - return client.clearPersistence(); + return clientProvider.executeWhileShutdown(executor -> { + final TaskCompletionSource source = new TaskCompletionSource<>(); + executor.execute(() -> { + try { + SQLitePersistence.clearPersistence(context, databaseId, persistenceKey); + source.setResult(null); + } catch (FirebaseFirestoreException e) { + source.setException(e); + } + }); + return source.getTask(); + }); } /** @@ -668,7 +795,7 @@ public ListenerRegistration addSnapshotsInSyncListener( @NonNull public LoadBundleTask loadBundle(@NonNull InputStream bundleData) { LoadBundleTask resultTask = new LoadBundleTask(); - client.safeCallVoid(client -> client.loadBundle(bundleData, resultTask)); + clientProvider.procedure(client -> client.loadBundle(bundleData, resultTask)); return resultTask; } @@ -706,7 +833,7 @@ public LoadBundleTask loadBundle(@NonNull ByteBuffer bundleData) { // TODO(b/261013682): Use an explicit executor in continuations. @SuppressLint("TaskMainThread") public @NonNull Task getNamedQuery(@NonNull String name) { - return client.safeCall(client -> client.getNamedQuery(name)) + return clientProvider.call(client -> client.getNamedQuery(name)) .continueWith( task -> { com.google.firebase.firestore.core.Query query = task.getResult(); @@ -738,11 +865,11 @@ private ListenerRegistration addSnapshotsInSyncListener( }; AsyncEventListener asyncListener = new AsyncEventListener(userExecutor, eventListener); - return client.safeCall(client -> client.addSnapshotsInSyncListener(asyncListener, activity)); + return clientProvider.call(client -> client.addSnapshotsInSyncListener(asyncListener, activity)); } T callClient(com.google.common.base.Function call) { - return client.safeCall(call); + return clientProvider.call(call); } DatabaseId getDatabaseId() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java index 46db3b32971..888800936fe 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java @@ -2,237 +2,92 @@ import static com.google.firebase.firestore.util.Preconditions.checkNotNull; -import android.content.Context; - import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.util.Consumer; -import androidx.core.util.Supplier; import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; import com.google.common.base.Function; -import com.google.firebase.emulators.EmulatedServiceSettings; -import com.google.firebase.firestore.auth.CredentialsProvider; -import com.google.firebase.firestore.auth.User; -import com.google.firebase.firestore.core.ComponentProvider; -import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.core.FirestoreClient; -import com.google.firebase.firestore.core.MemoryComponentProvider; -import com.google.firebase.firestore.core.SQLiteComponentProvider; -import com.google.firebase.firestore.local.SQLitePersistence; -import com.google.firebase.firestore.model.DatabaseId; -import com.google.firebase.firestore.remote.Datastore; -import com.google.firebase.firestore.remote.GrpcMetadataProvider; import com.google.firebase.firestore.util.AsyncQueue; -import com.google.firebase.firestore.util.Logger; import com.google.protobuf.ByteString; import java.util.concurrent.Executor; /** * The FirestoreClientProvider handles the life cycle of FirestoreClients. - * - * Coupling to FirestoreClient should go through the {@link FirestoreClientProvider#get()} - * method. The returned FirestoreClient can change over time if there is an event that requires - * restarting the FirestoreClient internally. */ final class FirestoreClientProvider { - private final Context context; - private final DatabaseId databaseId; - private final String persistenceKey; - private final GrpcMetadataProvider metadataProvider; - private final Supplier> authProviderFactory; - private final Supplier> appCheckTokenProviderFactory; - + private final Function clientFactory; @GuardedBy("this") private FirestoreClient client; - private volatile AsyncQueue asyncQueue; - - @GuardedBy("this") - private FirebaseFirestoreSettings settings; - - @GuardedBy("this") - private boolean networkEnabled = true; - - @GuardedBy("this") - @Nullable private EmulatedServiceSettings emulatorSettings; @GuardedBy("this") - private ByteString sessionToken; + private AsyncQueue asyncQueue; - FirestoreClientProvider( - Context context, - DatabaseId databaseId, - @NonNull String persistenceKey, - @NonNull Supplier> authProviderFactory, - @NonNull Supplier> appCheckTokenProviderFactory, - @Nullable GrpcMetadataProvider metadataProvider) { - this.context = checkNotNull(context); - this.databaseId = checkNotNull(checkNotNull(databaseId)); - this.persistenceKey = checkNotNull(persistenceKey); - this.authProviderFactory = checkNotNull(authProviderFactory); - this.appCheckTokenProviderFactory = checkNotNull(appCheckTokenProviderFactory); - this.metadataProvider = metadataProvider; - this.settings = new FirebaseFirestoreSettings.Builder().build(); + FirestoreClientProvider(Function clientFactory) { + this.clientFactory = clientFactory; this.asyncQueue = new AsyncQueue(); } - synchronized FirebaseFirestoreSettings getFirestoreSettings() { - return settings; + private synchronized void makeNewClient() { + client = clientFactory.apply(asyncQueue); } - synchronized void setFirestoreSettings(@NonNull FirebaseFirestoreSettings settings) { - settings = mergeEmulatorSettings(settings, this.emulatorSettings); - - checkNotNull(settings, "Provided settings must not be null."); - - // As a special exception, don't throw if the same settings are passed repeatedly. This - // should make it simpler to get a Firestore instance in an activity. - if (client != null && !this.settings.equals(settings)) { - throw new IllegalStateException( - "FirebaseFirestore has already been started and its settings can no longer be changed. " - + "You can only call setFirestoreSettings() before calling any other methods on a " - + "FirebaseFirestore object."); - } - - this.settings = settings; - } - - /** - * Modifies this FirebaseDatabase instance to communicate with the Cloud Firestore emulator. - * - *

    Note: Call this method before using the instance to do any database operations. - * - * @param host the emulator host (for example, 10.0.2.2) - * @param port the emulator port (for example, 8080) - */ - synchronized void useEmulator(@NonNull String host, int port) { - if (client != null) { - throw new IllegalStateException( - "Cannot call useEmulator() after instance has already been initialized."); - } - - this.emulatorSettings = new EmulatedServiceSettings(host, port); - this.settings = mergeEmulatorSettings(this.settings, this.emulatorSettings); + boolean isConfigured() { + return client != null; } - /** - * Sets any custom settings used to configure this {@code FirebaseFirestore} object. This method - * can only be called before calling any other methods on this object. - */ - private static FirebaseFirestoreSettings mergeEmulatorSettings( - @NonNull FirebaseFirestoreSettings settings, - @Nullable EmulatedServiceSettings emulatorSettings) { - if (emulatorSettings == null) { - return settings; - } - - if (!FirebaseFirestoreSettings.DEFAULT_HOST.equals(settings.getHost())) { - Logger.warn( - "FirestoreClientProvider", - "Host has been set in FirebaseFirestoreSettings and useEmulator, emulator host will be used."); + synchronized void ensureConfigured() { + if (!isConfigured()) { + makeNewClient(); } - - return new FirebaseFirestoreSettings.Builder(settings) - .setHost(emulatorSettings.getHost() + ":" + emulatorSettings.getPort()) - .setSslEnabled(false) - .build(); } - private synchronized FirestoreClient newClient() { - DatabaseInfo databaseInfo = - new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled()); - - CredentialsProvider authProvider = authProviderFactory.get(); - CredentialsProvider appCheckProvider = appCheckTokenProviderFactory.get(); - FirestoreClient client = new FirestoreClient( - databaseInfo, - settings, - authProvider, - appCheckProvider, - asyncQueue); - - Datastore datastore = new Datastore( - databaseInfo, asyncQueue, authProvider, appCheckProvider, context, metadataProvider); - - client.setClearPersistenceCallback(sessionToken -> { - synchronized (this) { - // If this callback is attached to an old FirestoreClient, we want to ignore it. - if (this.client == client) { - this.sessionToken = sessionToken; - clearPersistence(); - } - } - }); - - ComponentProvider componentProvider = settings.isPersistenceEnabled() - ? new SQLiteComponentProvider() - : new MemoryComponentProvider(); - - client.start( - context, - componentProvider, - datastore); - - if (sessionToken != null) { - client.setSessionToken(sessionToken); - } - - if (networkEnabled) { - client.enableNetwork(); - } - - return client; - } - - @NonNull - private synchronized ComponentProvider getComponentProvider() { - return settings.isPersistenceEnabled() - ? new SQLiteComponentProvider() - : new MemoryComponentProvider(); - } - - - void ensureClientConfigured() { - if (client == null) { - ensureClientConfiguredInternal(); - } - } - - synchronized private void ensureClientConfiguredInternal() { - if (client == null) { - client = newClient(); + synchronized void ifCurrentClient(FirestoreClient client, Runnable runnable) { + if (this.client == client) { + runnable.run(); } } /** * To facilitate calls to FirestoreClient without risk of FirestoreClient being terminated - * ir restarted by {@link #clearPersistence()} mid call. + * or restarted mid call. */ - synchronized T safeCall(Function call) { - ensureClientConfiguredInternal(); + synchronized T call(Function call) { + ensureConfigured(); return call.apply(client); } /** * To facilitate calls to FirestoreClient without risk of FirestoreClient being terminated - * ir restarted by {@link #clearPersistence()} mid call. + * or restarted mid call. */ - synchronized void safeCallVoid(Consumer call) { - ensureClientConfiguredInternal(); + synchronized void procedure(Consumer call) { + ensureConfigured(); call.accept(client); } + synchronized T executeWhileShutdown(Function call) { + // This will block asyncQueue, prevent a new client from being started. + if (client == null || client.isTerminated()) { + return call.apply(asyncQueue.getExecutor()); + } else { + client.terminate(); + asyncQueue = asyncQueue.reincarnate(); + T result = call.apply(asyncQueue.getExecutor()); + makeNewClient(); + return result; + } + } + /** * Shuts down the AsyncQueue and releases resources after which no progress will ever be made * again. */ synchronized Task terminate() { // The client must be initialized to ensure that all subsequent API usage throws an exception. - ensureClientConfiguredInternal(); + ensureConfigured(); Task terminate = client.terminate(); @@ -242,45 +97,7 @@ synchronized Task terminate() { return terminate; } - synchronized Task clearPersistence() { - // This will block asyncQueue, prevent a new client from being started. - if (client == null || client.isTerminated()) { - return clearPersistence(asyncQueue.getExecutor()); - } else { - client.terminate(); - asyncQueue = asyncQueue.reincarnate(); - Task task = clearPersistence(asyncQueue.getExecutor()); - client = newClient(); - return task; - } - } - - private Task clearPersistence(Executor executor) { - final TaskCompletionSource source = new TaskCompletionSource<>(); - executor.execute(() -> { - try { - SQLitePersistence.clearPersistence(context, databaseId, persistenceKey); - source.setResult(null); - } catch (FirebaseFirestoreException e) { - source.setException(e); - } - }); - return source.getTask(); - } - AsyncQueue getAsyncQueue() { return asyncQueue; } - - synchronized Task enableNetwork() { - networkEnabled = true; - ensureClientConfiguredInternal(); - return client.enableNetwork(); - } - - synchronized Task disableNetwork() { - networkEnabled = false; - ensureClientConfiguredInternal(); - return client.disableNetwork(); - } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/PersistentCacheIndexManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/PersistentCacheIndexManager.java index 80c3fdeadd2..d9927c63489 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/PersistentCacheIndexManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/PersistentCacheIndexManager.java @@ -43,7 +43,7 @@ public final class PersistentCacheIndexManager { *

    This feature is disabled by default. */ public void enableIndexAutoCreation() { - client.safeCallVoid(client -> client.setIndexAutoCreationEnabled(true)); + client.procedure(client -> client.setIndexAutoCreationEnabled(true)); } /** @@ -51,7 +51,7 @@ public void enableIndexAutoCreation() { * which have been created by calling {@link #enableIndexAutoCreation()} still take effect. */ public void disableIndexAutoCreation() { - client.safeCallVoid(client -> client.setIndexAutoCreationEnabled(false)); + client.procedure(client -> client.setIndexAutoCreationEnabled(false)); } /** @@ -59,6 +59,6 @@ public void disableIndexAutoCreation() { * {@link FirebaseFirestore#setIndexConfiguration(String)}, which is deprecated. */ public void deleteAllIndexes() { - client.safeCallVoid(FirestoreClient::deleteAllFieldIndexes); + client.procedure(FirestoreClient::deleteAllFieldIndexes); } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index 399e216d560..5ac6b497a5d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -47,6 +47,7 @@ import com.google.firebase.firestore.model.FieldIndex; import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.remote.Datastore; +import com.google.firebase.firestore.remote.GrpcMetadataProvider; import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.remote.RemoteStore; import com.google.firebase.firestore.util.AsyncQueue; @@ -77,6 +78,7 @@ public final class FirestoreClient { private final AsyncQueue asyncQueue; private final BundleSerializer bundleSerializer; private final FirebaseFirestoreSettings settings; + private final GrpcMetadataProvider metadataProvider; private Persistence persistence; private LocalStore localStore; @@ -94,20 +96,24 @@ public FirestoreClient( FirebaseFirestoreSettings settings, CredentialsProvider authProvider, CredentialsProvider appCheckProvider, - AsyncQueue asyncQueue) { + AsyncQueue asyncQueue, + @Nullable GrpcMetadataProvider metadataProvider) { this.databaseInfo = databaseInfo; this.settings = settings; this.authProvider = authProvider; this.appCheckProvider = appCheckProvider; this.asyncQueue = asyncQueue; + this.metadataProvider = metadataProvider; this.bundleSerializer = new BundleSerializer(new RemoteSerializer(databaseInfo.getDatabaseId())); } public void start( Context context, - ComponentProvider provider, - Datastore datastore) { + ComponentProvider provider) { + + Datastore datastore = new Datastore( + databaseInfo, asyncQueue, authProvider, appCheckProvider, context, metadataProvider); asyncQueue.setOnShutdown(this::onAsyncQueueShutdown); From 45f22b4304d62a8fff152dbc1117216b040c354e Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 5 Jun 2024 23:48:22 -0400 Subject: [PATCH 20/30] Fix --- .../com/google/firebase/firestore/FirebaseFirestore.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 503c4dfcd28..033ab054e8a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -80,7 +80,7 @@ public class FirebaseFirestore { private volatile ByteString sessionToken; - private boolean networkEnabled = false; + private boolean networkEnabled; /** * Provides a registry management interface for {@code FirebaseFirestore} instances. @@ -227,6 +227,7 @@ static FirebaseFirestore newInstance( @Nullable FirebaseApp firebaseApp, InstanceRegistry instanceRegistry, @Nullable GrpcMetadataProvider metadataProvider) { + this.networkEnabled = true; this.context = checkNotNull(context); this.databaseId = checkNotNull(checkNotNull(databaseId)); this.userDataReader = new UserDataReader(databaseId); @@ -243,9 +244,7 @@ static FirebaseFirestore newInstance( /** Returns the settings used by this {@code FirebaseFirestore} object. */ @NonNull public FirebaseFirestoreSettings getFirestoreSettings() { - synchronized (clientProvider) { - return settings; - } + return settings; } /** From 6cba3f44967b22204e229f39d5717c21d47fa047 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 6 Jun 2024 12:56:04 -0400 Subject: [PATCH 21/30] Cleanup --- .../firebase/firestore/FirebaseFirestore.java | 71 ++++++++++--------- .../firestore/FirestoreClientProvider.java | 19 ++--- .../firestore/core/ComponentProvider.java | 11 +-- .../firestore/core/FirestoreClient.java | 67 +++++++++-------- .../core/MemoryComponentProvider.java | 8 +-- .../firebase/firestore/core/SyncEngine.java | 13 +--- .../firestore/remote/RemoteStore.java | 28 +++++--- .../firebase/firestore/spec/SpecTestCase.java | 8 +-- 8 files changed, 100 insertions(+), 125 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 033ab054e8a..4e16aae92e0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -20,6 +20,7 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; +import androidx.annotation.GuardedBy; import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -79,7 +80,10 @@ */ public class FirebaseFirestore { + private final Function componentProviderFactory; private volatile ByteString sessionToken; + + @GuardedBy("clientProvider") private boolean networkEnabled; /** @@ -211,6 +215,9 @@ static FirebaseFirestore newInstance( persistenceKey, () -> new FirebaseAuthCredentialsProvider(deferredAuthProvider), () -> new FirebaseAppCheckTokenProvider(deferredAppCheckTokenProvider), + settings -> settings.isPersistenceEnabled() + ? new SQLiteComponentProvider() + : new MemoryComponentProvider(), app, instanceRegistry, metadataProvider); @@ -224,6 +231,7 @@ static FirebaseFirestore newInstance( @NonNull String persistenceKey, @NonNull Supplier> authProviderFactory, @NonNull Supplier> appCheckTokenProviderFactory, + @NonNull Function componentProviderFactory, @Nullable FirebaseApp firebaseApp, InstanceRegistry instanceRegistry, @Nullable GrpcMetadataProvider metadataProvider) { @@ -234,6 +242,7 @@ static FirebaseFirestore newInstance( this.persistenceKey = checkNotNull(persistenceKey); this.authProviderFactory = checkNotNull(authProviderFactory); this.appCheckTokenProviderFactory = checkNotNull(appCheckTokenProviderFactory); + this.componentProviderFactory = checkNotNull(componentProviderFactory); this.clientProvider = new FirestoreClientProvider(this::newClient); // NOTE: We allow firebaseApp to be null in tests only. this.firebaseApp = firebaseApp; @@ -290,43 +299,41 @@ public void useEmulator(@NonNull String host, int port) { } private FirestoreClient newClient(AsyncQueue asyncQueue) { - DatabaseInfo databaseInfo = - new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled()); - - FirestoreClient client = new FirestoreClient( - databaseInfo, - settings, - authProviderFactory.get(), - appCheckTokenProviderFactory.get(), - asyncQueue, - metadataProvider); - - client.setClearPersistenceCallback(sessionToken -> { - clientProvider.ifCurrentClient(client, () -> { - this.sessionToken = sessionToken; - clearPersistence(); + synchronized (clientProvider) { + DatabaseInfo databaseInfo = + new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled()); + + FirestoreClient client = new FirestoreClient( + context, + databaseInfo, + settings, + authProviderFactory.get(), + appCheckTokenProviderFactory.get(), + asyncQueue, + metadataProvider, + componentProviderFactory.apply(settings) + ); + + client.setClearPersistenceCallback(sessionToken -> { + synchronized (clientProvider) { + if (client.isTerminated()) return; + this.sessionToken = sessionToken; + clearPersistence(); + } }); - }); - client.start( - context, - newComponentProvider()); + // Session token must be set before we enable network, since it is part of stream handshake. + if (sessionToken != null) { + client.setSessionToken(sessionToken); + sessionToken = null; + } - if (sessionToken != null) { - client.setSessionToken(sessionToken); - } + if (networkEnabled) { + client.enableNetwork(); + } - if (networkEnabled) { - client.enableNetwork(); + return client; } - - return client; - } - - private ComponentProvider newComponentProvider() { - return settings.isPersistenceEnabled() - ? new SQLiteComponentProvider() - : new MemoryComponentProvider(); } private FirebaseFirestoreSettings mergeEmulatorSettings( diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java index 888800936fe..c487f7d3fb6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java @@ -9,7 +9,6 @@ import com.google.common.base.Function; import com.google.firebase.firestore.core.FirestoreClient; import com.google.firebase.firestore.util.AsyncQueue; -import com.google.protobuf.ByteString; import java.util.concurrent.Executor; @@ -30,23 +29,13 @@ final class FirestoreClientProvider { this.asyncQueue = new AsyncQueue(); } - private synchronized void makeNewClient() { - client = clientFactory.apply(asyncQueue); - } - boolean isConfigured() { return client != null; } synchronized void ensureConfigured() { if (!isConfigured()) { - makeNewClient(); - } - } - - synchronized void ifCurrentClient(FirestoreClient client, Runnable runnable) { - if (this.client == client) { - runnable.run(); + client = clientFactory.apply(asyncQueue); } } @@ -73,10 +62,10 @@ synchronized T executeWhileShutdown(Function call) { if (client == null || client.isTerminated()) { return call.apply(asyncQueue.getExecutor()); } else { - client.terminate(); + client.shutdown(); asyncQueue = asyncQueue.reincarnate(); T result = call.apply(asyncQueue.getExecutor()); - makeNewClient(); + client = clientFactory.apply(asyncQueue); return result; } } @@ -89,7 +78,7 @@ synchronized Task terminate() { // The client must be initialized to ensure that all subsequent API usage throws an exception. ensureConfigured(); - Task terminate = client.terminate(); + Task terminate = client.shutdown(); // Will cause the executor to de-reference all threads, the best we can do asyncQueue.terminate(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java index a71e3c4ba3b..3fde4140ca4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java @@ -28,8 +28,6 @@ import com.google.firebase.firestore.remote.Datastore; import com.google.firebase.firestore.remote.RemoteStore; import com.google.firebase.firestore.util.AsyncQueue; -import com.google.firebase.firestore.util.Consumer; -import com.google.protobuf.ByteString; /** * Initializes and wires up all core components for Firestore. @@ -57,7 +55,6 @@ public static class Configuration { private final User initialUser; private final int maxConcurrentLimboResolutions; private final FirebaseFirestoreSettings settings; - private final Consumer clearPersistenceCallback; public Configuration( Context context, @@ -66,8 +63,7 @@ public Configuration( Datastore datastore, User initialUser, int maxConcurrentLimboResolutions, - FirebaseFirestoreSettings settings, - Consumer clearPersistenceCallback) { + FirebaseFirestoreSettings settings) { this.context = context; this.asyncQueue = asyncQueue; this.databaseInfo = databaseInfo; @@ -75,7 +71,6 @@ public Configuration( this.initialUser = initialUser; this.maxConcurrentLimboResolutions = maxConcurrentLimboResolutions; this.settings = settings; - this.clearPersistenceCallback = clearPersistenceCallback; } FirebaseFirestoreSettings getSettings() { @@ -105,10 +100,6 @@ int getMaxConcurrentLimboResolutions() { Context getContext() { return context; } - - public Consumer getClearPersistenceCallback() { - return clearPersistenceCallback; - } } public Persistence getPersistence() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index 5ac6b497a5d..9ca432aabeb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -77,7 +77,6 @@ public final class FirestoreClient { private final CredentialsProvider appCheckProvider; private final AsyncQueue asyncQueue; private final BundleSerializer bundleSerializer; - private final FirebaseFirestoreSettings settings; private final GrpcMetadataProvider metadataProvider; private Persistence persistence; @@ -85,35 +84,26 @@ public final class FirestoreClient { private RemoteStore remoteStore; private SyncEngine syncEngine; private EventManager eventManager; - private Consumer clearPersistenceCallback; - // LRU-related @Nullable private Scheduler indexBackfillScheduler; @Nullable private Scheduler gcScheduler; public FirestoreClient( - DatabaseInfo databaseInfo, - FirebaseFirestoreSettings settings, - CredentialsProvider authProvider, - CredentialsProvider appCheckProvider, - AsyncQueue asyncQueue, - @Nullable GrpcMetadataProvider metadataProvider) { + final Context context, + DatabaseInfo databaseInfo, + FirebaseFirestoreSettings settings, + CredentialsProvider authProvider, + CredentialsProvider appCheckProvider, + AsyncQueue asyncQueue, + @Nullable GrpcMetadataProvider metadataProvider, + ComponentProvider componentProvider) { this.databaseInfo = databaseInfo; - this.settings = settings; this.authProvider = authProvider; this.appCheckProvider = appCheckProvider; this.asyncQueue = asyncQueue; this.metadataProvider = metadataProvider; this.bundleSerializer = new BundleSerializer(new RemoteSerializer(databaseInfo.getDatabaseId())); - } - - public void start( - Context context, - ComponentProvider provider) { - - Datastore datastore = new Datastore( - databaseInfo, asyncQueue, authProvider, appCheckProvider, context, metadataProvider); asyncQueue.setOnShutdown(this::onAsyncQueueShutdown); @@ -128,19 +118,7 @@ public void start( try { // Block on initial user being available User initialUser = Tasks.await(firstUser.getTask()); - Logger.debug(LOG_TAG, "Initializing. user=%s", initialUser.getUid()); - ComponentProvider.Configuration configuration = - new ComponentProvider.Configuration( - context, - asyncQueue, - databaseInfo, - datastore, - initialUser, - MAX_CONCURRENT_LIMBO_RESOLUTIONS, - settings, - clearPersistenceCallback); - provider.initialize(configuration); - initialize(provider); + initialize(context, initialUser, settings, componentProvider); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -169,7 +147,8 @@ public void start( } public void setClearPersistenceCallback(Consumer clearPersistenceCallback) { - this.clearPersistenceCallback = clearPersistenceCallback; + this.verifyNotTerminated(); + asyncQueue.enqueueAndForget(() -> remoteStore.setClearPersistenceCallback(clearPersistenceCallback)); } private void onAsyncQueueShutdown() { @@ -194,7 +173,7 @@ public Task enableNetwork() { } /** Terminates this client, cancels all writes / listeners, and releases all resources. */ - public Task terminate() { + public Task shutdown() { authProvider.removeChangeListener(); appCheckProvider.removeChangeListener(); asyncQueue.enqueueAndForget(() -> eventManager.abortAllTargets()); @@ -320,10 +299,25 @@ public Task waitForPendingWrites() { return source.getTask(); } - private void initialize(ComponentProvider provider) { + private void initialize(Context context, User user, FirebaseFirestoreSettings settings, ComponentProvider provider) { // Note: The initialization work must all be synchronous (we can't dispatch more work) since // external write/listen operations could get queued to run before that subsequent work // completes. + Logger.debug(LOG_TAG, "Initializing. user=%s", user.getUid()); + + Datastore datastore = + new Datastore( + databaseInfo, asyncQueue, authProvider, appCheckProvider, context, metadataProvider); + ComponentProvider.Configuration configuration = + new ComponentProvider.Configuration( + context, + asyncQueue, + databaseInfo, + datastore, + user, + MAX_CONCURRENT_LIMBO_RESOLUTIONS, + settings); + provider.initialize(configuration); persistence = provider.getPersistence(); gcScheduler = provider.getGarbageCollectionScheduler(); localStore = provider.getLocalStore(); @@ -417,4 +411,9 @@ public void setSessionToken(ByteString sessionToken) { verifyNotTerminated(); asyncQueue.enqueueAndForget(() -> localStore.setSessionsToken(sessionToken)); } + + public ByteString getSessionToken() { + verifyNotTerminated(); + return localStore.getSessionToken(); + } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java index de7f089cb4f..47d787ff12d 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java @@ -111,8 +111,7 @@ protected SyncEngine createSyncEngine(Configuration configuration) { getLocalStore(), getRemoteStore(), configuration.getInitialUser(), - configuration.getMaxConcurrentLimboResolutions(), - configuration.getClearPersistenceCallback()); + configuration.getMaxConcurrentLimboResolutions()); } /** @@ -152,10 +151,5 @@ public void handleOnlineStateChange(OnlineState onlineState) { public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { return getSyncEngine().getRemoteKeysForTarget(targetId); } - - @Override - public void handleClearPersistence(ByteString sessionToken) { - getSyncEngine().handleClearPersistence(sessionToken); - } } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index c8b41b8d612..c0418fc3a77 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -51,7 +51,6 @@ import com.google.firebase.firestore.remote.RemoteEvent; import com.google.firebase.firestore.remote.RemoteStore; import com.google.firebase.firestore.remote.TargetChange; -import com.google.firebase.firestore.util.Consumer; import com.google.firebase.firestore.util.Logger; import com.google.firebase.firestore.util.Util; import com.google.protobuf.ByteString; @@ -84,8 +83,6 @@ */ public class SyncEngine implements RemoteStore.RemoteStoreCallback { - private final Consumer clearPersistenceCallback; - /** Tracks a limbo resolution. */ private static class LimboResolution { private final DocumentKey key; @@ -165,12 +162,10 @@ public SyncEngine( LocalStore localStore, RemoteStore remoteStore, User initialUser, - int maxConcurrentLimboResolutions, - Consumer clearPersistenceCallback) { + int maxConcurrentLimboResolutions) { this.localStore = localStore; this.remoteStore = remoteStore; this.maxConcurrentLimboResolutions = maxConcurrentLimboResolutions; - this.clearPersistenceCallback = clearPersistenceCallback; queryViewsByQuery = new HashMap<>(); queriesByTarget = new HashMap<>(); @@ -490,12 +485,6 @@ public void handleRejectedWrite(int batchId, Status status) { emitNewSnapsAndNotifyLocalStore(changes, /*remoteEvent=*/ null); } - @Override - public void handleClearPersistence(ByteString sessionToken) { - assertCallback("handleClearPersistence"); - clearPersistenceCallback.accept(sessionToken); - } - /** * Takes a snapshot of current mutation queue, and register a user task which will resolve when * all those mutations are either accepted or rejected by the server. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index e0602c0e7de..732647e11bf 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -41,6 +41,7 @@ import com.google.firebase.firestore.remote.WatchChange.WatchTargetChange; import com.google.firebase.firestore.remote.WatchChange.WatchTargetChangeType; import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firebase.firestore.util.Consumer; import com.google.firebase.firestore.util.Logger; import com.google.firebase.firestore.util.Util; import com.google.firestore.v1.InitResponse; @@ -65,6 +66,7 @@ public class RemoteStore implements WatchChangeAggregator.TargetMetadataProvider /** The log tag to use for this class. */ private static final String LOG_TAG = "RemoteStore"; + private Consumer clearPersistenceCallback; /** A callback interface for events from RemoteStore. */ public interface RemoteStoreCallback { @@ -112,9 +114,7 @@ public interface RemoteStoreCallback { *

    Returns an empty set of document keys for unknown targets. */ ImmutableSortedSet getRemoteKeysForTarget(int targetId); - - void handleClearPersistence(ByteString sessionToken); - } +} private final RemoteStoreCallback remoteStoreCallback; private final LocalStore localStore; @@ -179,7 +179,7 @@ public RemoteStore( @Override public void onHandshake(InitResponse initResponse) { if (initResponse.getClearCache()) { - remoteStoreCallback.handleClearPersistence(initResponse.getSessionToken()); + handleClearCache(initResponse.getSessionToken()); } else { handleWatchStreamHandshakeComplete(initResponse.getSessionToken()); } @@ -209,7 +209,7 @@ public void onClose(Status status) { @Override public void onHandshake(InitResponse initResponse) { if (initResponse.getClearCache()) { - remoteStoreCallback.handleClearPersistence(initResponse.getSessionToken()); + handleClearCache(initResponse.getSessionToken()); } else { handleWriteStreamHandshakeComplete(initResponse.getSessionToken()); } @@ -268,6 +268,18 @@ public void onClose(Status status) { }); } + private void handleClearCache(ByteString sessionToken) { + hardAssert(clearPersistenceCallback != null, "Cannot clear persistence without callback"); + if (sessionToken.isEmpty()) { + sessionToken = localStore.getSessionToken(); + } + clearPersistenceCallback.accept(sessionToken); + } + + public void setClearPersistenceCallback(Consumer clearPersistenceCallback) { + this.clearPersistenceCallback = clearPersistenceCallback; + } + /** Re-enables the network. Only to be called as the counterpart to disableNetwork(). */ public void enableNetwork() { networkEnabled = true; @@ -461,10 +473,10 @@ private void startWatchStream() { } private void handleWatchStreamHandshakeComplete(ByteString sessionToken) { - if (!sessionToken.isEmpty()) { - localStore.setSessionsToken(sessionToken); - } else { + if (sessionToken.isEmpty()) { sessionToken = localStore.getSessionToken(); + } else { + localStore.setSessionsToken(sessionToken); } // If write stream started handshake, but was waiting for listen handshake to complete, we diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index dce129af472..cedc0395a97 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -325,8 +325,7 @@ private void initClient() { datastore, currentUser, maxConcurrentLimboResolutions, - new FirebaseFirestoreSettings.Builder().build(), - null); + new FirebaseFirestoreSettings.Builder().build()); ComponentProvider provider = initializeComponentProvider(configuration, useEagerGcForMemory); localPersistence = provider.getPersistence(); @@ -346,11 +345,6 @@ public void handleOnlineStateChange(OnlineState onlineState) { syncEngine.handleOnlineStateChange(onlineState); } - @Override - public void handleClearPersistence(ByteString sessionToken) { - syncEngine.handleClearPersistence(sessionToken); - } - private List>> getCurrentOutstandingWrites() { List>> writes = outstandingWrites.get(currentUser); if (writes == null) { From 03c70b9b6e1b1c9bd911413bdb05386df87c9010 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 6 Jun 2024 12:58:28 -0400 Subject: [PATCH 22/30] Cleanup --- .../google/firebase/firestore/remote/RemoteStoreTest.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java index 7691cb03d9c..bf2b97219f6 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java @@ -30,8 +30,6 @@ import com.google.firebase.firestore.testutil.IntegrationTestUtil; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Consumer; -import com.google.protobuf.ByteString; - import io.grpc.Status; import java.util.concurrent.Semaphore; import org.junit.Test; @@ -74,9 +72,6 @@ public void handleOnlineStateChange(OnlineState onlineState) { public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { return null; } - - @Override - public void handleClearPersistence(ByteString sessionToken) {} }; FakeConnectivityMonitor connectivityMonitor = new FakeConnectivityMonitor(); From fdc425db54314189a0d048f5c6fe8130a84345e1 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 10 Jun 2024 22:53:06 -0400 Subject: [PATCH 23/30] Add tests --- .../firebase/firestore/AccessHelper.java | 6 +- .../firebase/firestore/TransactionTest.java | 8 +- .../firestore/remote/RemoteStoreTest.java | 21 +- .../firebase/firestore/remote/StreamTest.java | 70 ++--- .../testutil/IntegrationTestUtil.java | 7 +- .../firebase/firestore/FirebaseFirestore.java | 46 ++- .../firestore/FirestoreClientProvider.java | 4 +- .../firestore/core/ComponentProvider.java | 92 +++--- .../firestore/core/FirestoreClient.java | 22 +- .../core/MemoryComponentProvider.java | 27 +- .../core/SQLiteComponentProvider.java | 14 +- .../remote/AndroidConnectivityMonitor.java | 4 +- .../firebase/firestore/remote/Datastore.java | 40 +-- .../firestore/remote/FirestoreChannel.java | 29 +- .../firestore/remote/GrpcCallProvider.java | 4 +- .../remote/RemoteComponenetProvider.java | 72 +++++ .../firestore/remote/RemoteStore.java | 5 +- .../SynchronizedShutdownAwareExecutor.java | 6 +- .../FirebaseFirestoreTestFactory.java | 155 ++++++++++ .../firestore/core/EventManagerTest.java | 152 +++++----- .../integration/FirebaseFirestoreTest.java | 264 ++++++++++++++++++ .../firestore/remote/MockDatastore.java | 21 +- .../firestore/spec/MemorySpecTest.java | 16 +- .../firestore/spec/SQLiteSpecTest.java | 5 +- .../firebase/firestore/spec/SpecTestCase.java | 25 +- .../src/test/java/io/grpc/MockClientCall.java | 74 +++++ 26 files changed, 865 insertions(+), 324 deletions(-) create mode 100644 firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java create mode 100644 firebase-firestore/src/test/java/io/grpc/MockClientCall.java diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java index 23c38a3a7c9..ebb061ebb96 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/AccessHelper.java @@ -19,8 +19,10 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.User; +import com.google.firebase.firestore.core.ComponentProvider; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firebase.firestore.util.Function; /** Gives access to package private methods in integration tests. */ public final class AccessHelper { @@ -32,6 +34,7 @@ public static FirebaseFirestore newFirebaseFirestore( String persistenceKey, Supplier> authProviderFactory, Supplier> appCheckTokenProviderFactory, + Function componentProviderFactory, FirebaseApp firebaseApp, FirebaseFirestore.InstanceRegistry instanceRegistry) { return new FirebaseFirestore( @@ -40,12 +43,13 @@ public static FirebaseFirestore newFirebaseFirestore( persistenceKey, authProviderFactory, appCheckTokenProviderFactory, + componentProviderFactory, firebaseApp, instanceRegistry, null); } public static AsyncQueue getAsyncQueue(FirebaseFirestore firestore) { - return firestore.getAsyncQueue(); + return firestore.clientProvider.getAsyncQueue(); } } diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java index f83289d3d5a..bd90a4b3bb4 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/TransactionTest.java @@ -371,7 +371,7 @@ public void testIncrementTransactionally() { AtomicInteger started = new AtomicInteger(0); FirebaseFirestore firestore = testFirestore(); - firestore.getAsyncQueue().skipDelaysForTimerId(TimerId.RETRY_TRANSACTION); + AccessHelper.getAsyncQueue(firestore).skipDelaysForTimerId(TimerId.RETRY_TRANSACTION); DocumentReference doc = firestore.collection("counters").document(); waitFor(doc.set(map("count", 5.0))); @@ -437,7 +437,7 @@ public void testUpdateTransactionally() { AtomicInteger counter = new AtomicInteger(0); FirebaseFirestore firestore = testFirestore(); - firestore.getAsyncQueue().skipDelaysForTimerId(TimerId.RETRY_TRANSACTION); + AccessHelper.getAsyncQueue(firestore).skipDelaysForTimerId(TimerId.RETRY_TRANSACTION); DocumentReference doc = firestore.collection("counters").document(); waitFor(doc.set(map("count", 5.0, "other", "yes"))); @@ -532,7 +532,7 @@ public void testUpdatePOJOTransactionally() { AtomicInteger started = new AtomicInteger(0); FirebaseFirestore firestore = testFirestore(); - firestore.getAsyncQueue().skipDelaysForTimerId(TimerId.RETRY_TRANSACTION); + AccessHelper.getAsyncQueue(firestore).skipDelaysForTimerId(TimerId.RETRY_TRANSACTION); DocumentReference doc = firestore.collection("counters").document(); waitFor(doc.set(new POJO(5.0, "no", "clean"))); @@ -601,7 +601,7 @@ public void testRetriesWhenDocumentThatWasReadWithoutBeingWrittenChanges() { @Test public void testReadingADocTwiceWithDifferentVersions() { FirebaseFirestore firestore = testFirestore(); - firestore.getAsyncQueue().skipDelaysForTimerId(TimerId.RETRY_TRANSACTION); + AccessHelper.getAsyncQueue(firestore).skipDelaysForTimerId(TimerId.RETRY_TRANSACTION); DocumentReference doc = firestore.collection("counters").document(); waitFor(doc.set(map("count", 15.0))); AtomicInteger counter = new AtomicInteger(0); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java index bf2b97219f6..3d1fe8be163 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java @@ -20,6 +20,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.firebase.database.collection.ImmutableSortedSet; import com.google.firebase.firestore.auth.User; +import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.core.OnlineState; import com.google.firebase.firestore.local.LocalStore; import com.google.firebase.firestore.local.MemoryPersistence; @@ -40,14 +41,16 @@ public class RemoteStoreTest { @Test public void testRemoteStoreStreamStopsWhenNetworkUnreachable() { AsyncQueue testQueue = new AsyncQueue(); - Datastore datastore = - new Datastore( - IntegrationTestUtil.testEnvDatabaseInfo(), - testQueue, - null, - null, - ApplicationProvider.getApplicationContext(), - null); + DatabaseInfo databaseInfo = IntegrationTestUtil.testEnvDatabaseInfo(); + RemoteSerializer serializer = new RemoteSerializer(databaseInfo.getDatabaseId()); + FirestoreChannel channel = new FirestoreChannel( + testQueue, + ApplicationProvider.getApplicationContext(), + null, + null, + databaseInfo, + null); + Datastore datastore = new Datastore(testQueue, serializer, channel); Semaphore networkChangeSemaphore = new Semaphore(0); RemoteStore.RemoteStoreCallback callback = new RemoteStore.RemoteStoreCallback() { @@ -80,7 +83,7 @@ public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { persistence.start(); LocalStore localStore = new LocalStore(persistence, queryEngine, User.UNAUTHENTICATED); RemoteStore remoteStore = - new RemoteStore(callback, localStore, datastore, testQueue, connectivityMonitor); + new RemoteStore(databaseInfo.getDatabaseId(), callback, localStore, datastore, testQueue, connectivityMonitor); waitFor(testQueue.enqueue(remoteStore::forceEnableNetwork)); drain(testQueue); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java index cd04370036f..7383082c822 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java @@ -26,9 +26,11 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.tasks.Task; +import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.model.SnapshotVersion; import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.model.mutation.MutationResult; @@ -120,14 +122,7 @@ public void onWriteResponse( /** Creates a WriteStream and gets it in a state that accepts mutations. */ private WriteStream createAndOpenWriteStream( AsyncQueue testQueue, StreamStatusCallback callback) { - Datastore datastore = - new Datastore( - IntegrationTestUtil.testEnvDatabaseInfo(), - testQueue, - new EmptyCredentialsProvider(), - new EmptyAppCheckTokenProvider(), - ApplicationProvider.getApplicationContext(), - null); + Datastore datastore = createTestDatastore(testQueue, null); final WriteStream writeStream = datastore.createWriteStream(callback); waitForWriteStreamOpen(testQueue, writeStream, callback); return writeStream; @@ -142,18 +137,25 @@ private void waitForWriteStreamOpen( waitFor(callback.handshakeSemaphore); } + @NonNull + private static Datastore createTestDatastore(AsyncQueue testQueue, GrpcMetadataProvider metadataProvider) { + DatabaseInfo databaseInfo = IntegrationTestUtil.testEnvDatabaseInfo(); + RemoteSerializer remoteSerializer = new RemoteSerializer(databaseInfo.getDatabaseId()); + FirestoreChannel firestoreChannel = new FirestoreChannel( + testQueue, + ApplicationProvider.getApplicationContext(), + new EmptyCredentialsProvider(), + new EmptyAppCheckTokenProvider(), + databaseInfo, + metadataProvider); + return new Datastore(testQueue, remoteSerializer, firestoreChannel); + } + @Test public void testWatchStreamStopBeforeHandshake() throws Exception { AsyncQueue testQueue = new AsyncQueue(); GrpcMetadataProvider mockGrpcProvider = mock(GrpcMetadataProvider.class); - Datastore datastore = - new Datastore( - IntegrationTestUtil.testEnvDatabaseInfo(), - testQueue, - new EmptyCredentialsProvider(), - new EmptyAppCheckTokenProvider(), - ApplicationProvider.getApplicationContext(), - mockGrpcProvider); + Datastore datastore = createTestDatastore(testQueue, mockGrpcProvider); StreamStatusCallback streamCallback = new StreamStatusCallback() {}; final WatchStream watchStream = datastore.createWatchStream(streamCallback); @@ -169,14 +171,7 @@ public void testWatchStreamStopBeforeHandshake() throws Exception { @Test public void testWriteStreamStopAfterHandshake() throws Exception { AsyncQueue testQueue = new AsyncQueue(); - Datastore datastore = - new Datastore( - IntegrationTestUtil.testEnvDatabaseInfo(), - testQueue, - new EmptyCredentialsProvider(), - new EmptyAppCheckTokenProvider(), - ApplicationProvider.getApplicationContext(), - null); + Datastore datastore = createTestDatastore(testQueue, null); final WriteStream[] writeStreamWrapper = new WriteStream[1]; StreamStatusCallback streamCallback = new StreamStatusCallback() { @@ -217,14 +212,7 @@ public void onWriteResponse( @Test public void testWriteStreamStopPartial() throws Exception { AsyncQueue testQueue = new AsyncQueue(); - Datastore datastore = - new Datastore( - IntegrationTestUtil.testEnvDatabaseInfo(), - testQueue, - new EmptyCredentialsProvider(), - new EmptyAppCheckTokenProvider(), - ApplicationProvider.getApplicationContext(), - null); + Datastore datastore = createTestDatastore(testQueue, null); StreamStatusCallback streamCallback = new StreamStatusCallback() {}; final WriteStream writeStream = datastore.createWriteStream(streamCallback); @@ -298,14 +286,7 @@ public void testStreamStaysIdle() throws Exception { public void testStreamRefreshesTokenUponExpiration() throws Exception { AsyncQueue testQueue = new AsyncQueue(); MockCredentialsProvider mockCredentialsProvider = new MockCredentialsProvider(); - Datastore datastore = - new Datastore( - IntegrationTestUtil.testEnvDatabaseInfo(), - testQueue, - mockCredentialsProvider, - new EmptyAppCheckTokenProvider(), - ApplicationProvider.getApplicationContext(), - null); + Datastore datastore = createTestDatastore(testQueue, null); StreamStatusCallback callback = new StreamStatusCallback(); WriteStream writeStream = datastore.createWriteStream(callback); waitForWriteStreamOpen(testQueue, writeStream, callback); @@ -328,14 +309,7 @@ public void testStreamRefreshesTokenUponExpiration() throws Exception { public void testTokenIsNotInvalidatedOnceStreamIsHealthy() throws Exception { AsyncQueue testQueue = new AsyncQueue(); MockCredentialsProvider mockCredentialsProvider = new MockCredentialsProvider(); - Datastore datastore = - new Datastore( - IntegrationTestUtil.testEnvDatabaseInfo(), - testQueue, - mockCredentialsProvider, - new EmptyAppCheckTokenProvider(), - ApplicationProvider.getApplicationContext(), - null); + Datastore datastore = createTestDatastore(testQueue, null); StreamStatusCallback callback = new StreamStatusCallback(); WriteStream writeStream = datastore.createWriteStream(callback); waitForWriteStreamOpen(testQueue, writeStream, callback); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index b43b64cd121..455534e308d 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -42,10 +42,10 @@ import com.google.firebase.firestore.Source; import com.google.firebase.firestore.WriteBatch; import com.google.firebase.firestore.auth.User; +import com.google.firebase.firestore.core.ComponentProvider; import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.testutil.provider.FirestoreProvider; -import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Listener; import com.google.firebase.firestore.util.Logger; import com.google.firebase.firestore.util.Logger.Level; @@ -310,8 +310,9 @@ public static FirebaseFirestore testFirestore( context, databaseId, persistenceKey, - () -> MockCredentialsProvider.instance(), - () -> new EmptyAppCheckTokenProvider(), + MockCredentialsProvider::instance, + EmptyAppCheckTokenProvider::new, + ComponentProvider::defaultFactory, /*firebaseApp=*/ null, /*instanceRegistry=*/ (dbId) -> {}); waitFor(firestore.clearPersistence()); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 4e16aae92e0..ec912455edd 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -42,8 +42,6 @@ import com.google.firebase.firestore.core.ComponentProvider; import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.core.FirestoreClient; -import com.google.firebase.firestore.core.MemoryComponentProvider; -import com.google.firebase.firestore.core.SQLiteComponentProvider; import com.google.firebase.firestore.local.SQLitePersistence; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.model.FieldIndex; @@ -111,9 +109,12 @@ public interface InstanceRegistry { private final InstanceRegistry instanceRegistry; @Nullable private EmulatedServiceSettings emulatorSettings; private FirebaseFirestoreSettings settings; - private final FirestoreClientProvider clientProvider; + final FirestoreClientProvider clientProvider; private final GrpcMetadataProvider metadataProvider; + @VisibleForTesting + Function> clearPersistenceMethod; + @Nullable private PersistentCacheIndexManager persistentCacheIndexManager; @NonNull @@ -215,9 +216,7 @@ static FirebaseFirestore newInstance( persistenceKey, () -> new FirebaseAuthCredentialsProvider(deferredAuthProvider), () -> new FirebaseAppCheckTokenProvider(deferredAppCheckTokenProvider), - settings -> settings.isPersistenceEnabled() - ? new SQLiteComponentProvider() - : new MemoryComponentProvider(), + ComponentProvider::defaultFactory, app, instanceRegistry, metadataProvider); @@ -248,6 +247,21 @@ static FirebaseFirestore newInstance( this.firebaseApp = firebaseApp; this.instanceRegistry = instanceRegistry; this.metadataProvider = metadataProvider; + + this.settings = new FirebaseFirestoreSettings.Builder().build(); + + this.clearPersistenceMethod = executor -> { + final TaskCompletionSource source = new TaskCompletionSource<>(); + executor.execute(() -> { + try { + SQLitePersistence.clearPersistence(context, databaseId, persistenceKey); + source.setResult(null); + } catch (FirebaseFirestoreException e) { + source.setException(e); + } + }); + return source.getTask(); + }; } /** Returns the settings used by this {@code FirebaseFirestore} object. */ @@ -646,11 +660,6 @@ public Task waitForPendingWrites() { return clientProvider.call(FirestoreClient::waitForPendingWrites); } - @VisibleForTesting - AsyncQueue getAsyncQueue() { - return clientProvider.getAsyncQueue(); - } - /** * Re-enables network usage for this instance after a prior call to {@link #disableNetwork()}. * @@ -716,18 +725,7 @@ public static void setLoggingEnabled(boolean loggingEnabled) { */ @NonNull public Task clearPersistence() { - return clientProvider.executeWhileShutdown(executor -> { - final TaskCompletionSource source = new TaskCompletionSource<>(); - executor.execute(() -> { - try { - SQLitePersistence.clearPersistence(context, databaseId, persistenceKey); - source.setResult(null); - } catch (FirebaseFirestoreException e) { - source.setException(e); - } - }); - return source.getTask(); - }); + return clientProvider.executeWhileShutdown(clearPersistenceMethod); } /** @@ -874,7 +872,7 @@ private ListenerRegistration addSnapshotsInSyncListener( return clientProvider.call(client -> client.addSnapshotsInSyncListener(asyncListener, activity)); } - T callClient(com.google.common.base.Function call) { + T callClient(Function call) { return clientProvider.call(call); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java index c487f7d3fb6..6fbd68ed81a 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java @@ -1,14 +1,12 @@ package com.google.firebase.firestore; -import static com.google.firebase.firestore.util.Preconditions.checkNotNull; - import androidx.annotation.GuardedBy; import androidx.core.util.Consumer; import com.google.android.gms.tasks.Task; -import com.google.common.base.Function; import com.google.firebase.firestore.core.FirestoreClient; import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firebase.firestore.util.Function; import java.util.concurrent.Executor; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java index 3fde4140ca4..07cc8593726 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java @@ -14,11 +14,15 @@ package com.google.firebase.firestore.core; +import static com.google.firebase.firestore.util.Assert.hardAssert; import static com.google.firebase.firestore.util.Assert.hardAssertNonNull; import android.content.Context; + +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.firebase.firestore.FirebaseFirestoreSettings; +import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.User; import com.google.firebase.firestore.local.IndexBackfiller; import com.google.firebase.firestore.local.LocalStore; @@ -26,6 +30,9 @@ import com.google.firebase.firestore.local.Scheduler; import com.google.firebase.firestore.remote.ConnectivityMonitor; import com.google.firebase.firestore.remote.Datastore; +import com.google.firebase.firestore.remote.GrpcMetadataProvider; +import com.google.firebase.firestore.remote.RemoteComponenetProvider; +import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.remote.RemoteStore; import com.google.firebase.firestore.util.AsyncQueue; @@ -36,70 +43,76 @@ */ public abstract class ComponentProvider { + private RemoteComponenetProvider remoteProvider = new RemoteComponenetProvider(); private Persistence persistence; private LocalStore localStore; private SyncEngine syncEngine; private RemoteStore remoteStore; private EventManager eventManager; - private ConnectivityMonitor connectivityMonitor; @Nullable private IndexBackfiller indexBackfiller; @Nullable private Scheduler garbageCollectionScheduler; + private AsyncQueue asyncQueue; + + @NonNull + public static ComponentProvider defaultFactory(@NonNull FirebaseFirestoreSettings settings) { + return settings.isPersistenceEnabled() + ? new SQLiteComponentProvider() + : new MemoryComponentProvider(); + } /** Configuration options for the component provider. */ - public static class Configuration { + public static final class Configuration { - private final Context context; - private final AsyncQueue asyncQueue; - private final DatabaseInfo databaseInfo; - private final Datastore datastore; - private final User initialUser; - private final int maxConcurrentLimboResolutions; - private final FirebaseFirestoreSettings settings; + public final Context context; + public final AsyncQueue asyncQueue; + public final DatabaseInfo databaseInfo; + public final User initialUser; + public final int maxConcurrentLimboResolutions; + public final FirebaseFirestoreSettings settings; + public final CredentialsProvider authProvider; + public final CredentialsProvider appCheckProvider; + + @Nullable + public final GrpcMetadataProvider metadataProvider; public Configuration( Context context, AsyncQueue asyncQueue, DatabaseInfo databaseInfo, - Datastore datastore, User initialUser, int maxConcurrentLimboResolutions, - FirebaseFirestoreSettings settings) { + FirebaseFirestoreSettings settings, + CredentialsProvider authProvider, + CredentialsProvider appCheckProvider, + @Nullable GrpcMetadataProvider metadataProvider + ) { this.context = context; this.asyncQueue = asyncQueue; this.databaseInfo = databaseInfo; - this.datastore = datastore; this.initialUser = initialUser; this.maxConcurrentLimboResolutions = maxConcurrentLimboResolutions; this.settings = settings; + this.authProvider = authProvider; + this.appCheckProvider = appCheckProvider; + this.metadataProvider = metadataProvider; } + } - FirebaseFirestoreSettings getSettings() { - return settings; - } - - AsyncQueue getAsyncQueue() { - return asyncQueue; - } - - DatabaseInfo getDatabaseInfo() { - return databaseInfo; - } - - Datastore getDatastore() { - return datastore; - } + public void setRemoteProvider(RemoteComponenetProvider remoteProvider) { + hardAssert(remoteStore == null, "cannot set remoteProvider after initialize"); + this.remoteProvider = remoteProvider; + } - User getInitialUser() { - return initialUser; - } + public AsyncQueue getAsyncQueue() { + return hardAssertNonNull(asyncQueue, "asyncQueue not initialized yet"); + } - int getMaxConcurrentLimboResolutions() { - return maxConcurrentLimboResolutions; - } + public RemoteSerializer getRemoteSerializer() { + return remoteProvider.getRemoteSerializer(); + } - Context getContext() { - return context; - } + public Datastore getDatastore() { + return remoteProvider.getDatastore(); } public Persistence getPersistence() { @@ -133,7 +146,7 @@ public EventManager getEventManager() { } protected ConnectivityMonitor getConnectivityMonitor() { - return hardAssertNonNull(connectivityMonitor, "connectivityMonitor not initialized yet"); + return remoteProvider.getConnectivityMonitor(); } public void initialize(Configuration configuration) { @@ -146,10 +159,11 @@ public void initialize(Configuration configuration) { * *

    To catch incorrect order, all getX methods have runtime check for null. */ + asyncQueue = configuration.asyncQueue; + remoteProvider.initialize(configuration); persistence = createPersistence(configuration); persistence.start(); localStore = createLocalStore(configuration); - connectivityMonitor = createConnectivityMonitor(configuration); remoteStore = createRemoteStore(configuration); syncEngine = createSyncEngine(configuration); eventManager = createEventManager(configuration); @@ -166,8 +180,6 @@ public void initialize(Configuration configuration) { protected abstract LocalStore createLocalStore(Configuration configuration); - protected abstract ConnectivityMonitor createConnectivityMonitor(Configuration configuration); - protected abstract Persistence createPersistence(Configuration configuration); protected abstract RemoteStore createRemoteStore(Configuration configuration); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index 9ca432aabeb..1eaf05a9379 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -46,7 +46,6 @@ import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.FieldIndex; import com.google.firebase.firestore.model.mutation.Mutation; -import com.google.firebase.firestore.remote.Datastore; import com.google.firebase.firestore.remote.GrpcMetadataProvider; import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.remote.RemoteStore; @@ -77,8 +76,6 @@ public final class FirestoreClient { private final CredentialsProvider appCheckProvider; private final AsyncQueue asyncQueue; private final BundleSerializer bundleSerializer; - private final GrpcMetadataProvider metadataProvider; - private Persistence persistence; private LocalStore localStore; private RemoteStore remoteStore; @@ -101,7 +98,6 @@ public FirestoreClient( this.authProvider = authProvider; this.appCheckProvider = appCheckProvider; this.asyncQueue = asyncQueue; - this.metadataProvider = metadataProvider; this.bundleSerializer = new BundleSerializer(new RemoteSerializer(databaseInfo.getDatabaseId())); @@ -118,7 +114,7 @@ public FirestoreClient( try { // Block on initial user being available User initialUser = Tasks.await(firstUser.getTask()); - initialize(context, initialUser, settings, componentProvider); + initialize(context, initialUser, settings, componentProvider, metadataProvider); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException(e); } @@ -299,24 +295,28 @@ public Task waitForPendingWrites() { return source.getTask(); } - private void initialize(Context context, User user, FirebaseFirestoreSettings settings, ComponentProvider provider) { + private void initialize( + Context context, + User user, + FirebaseFirestoreSettings settings, + ComponentProvider provider, + GrpcMetadataProvider metadataProvider) { // Note: The initialization work must all be synchronous (we can't dispatch more work) since // external write/listen operations could get queued to run before that subsequent work // completes. Logger.debug(LOG_TAG, "Initializing. user=%s", user.getUid()); - Datastore datastore = - new Datastore( - databaseInfo, asyncQueue, authProvider, appCheckProvider, context, metadataProvider); ComponentProvider.Configuration configuration = new ComponentProvider.Configuration( context, asyncQueue, databaseInfo, - datastore, user, MAX_CONCURRENT_LIMBO_RESOLUTIONS, - settings); + settings, + authProvider, + appCheckProvider, + metadataProvider); provider.initialize(configuration); persistence = provider.getPersistence(); gcScheduler = provider.getGarbageCollectionScheduler(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java index 47d787ff12d..4e19674d0e6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java @@ -29,11 +29,9 @@ import com.google.firebase.firestore.local.Scheduler; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.mutation.MutationBatchResult; -import com.google.firebase.firestore.remote.AndroidConnectivityMonitor; +import com.google.firebase.firestore.remote.RemoteComponenetProvider; import com.google.firebase.firestore.remote.RemoteEvent; -import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.remote.RemoteStore; -import com.google.protobuf.ByteString; import io.grpc.Status; @@ -62,12 +60,7 @@ protected EventManager createEventManager(Configuration configuration) { @Override protected LocalStore createLocalStore(Configuration configuration) { - return new LocalStore(getPersistence(), new QueryEngine(), configuration.getInitialUser()); - } - - @Override - protected AndroidConnectivityMonitor createConnectivityMonitor(Configuration configuration) { - return new AndroidConnectivityMonitor(configuration.getContext()); + return new LocalStore(getPersistence(), new QueryEngine(), configuration.initialUser); } private boolean isMemoryLruGcEnabled(FirebaseFirestoreSettings settings) { @@ -82,13 +75,12 @@ private boolean isMemoryLruGcEnabled(FirebaseFirestoreSettings settings) { @Override protected Persistence createPersistence(Configuration configuration) { - if (isMemoryLruGcEnabled(configuration.getSettings())) { + if (isMemoryLruGcEnabled(configuration.settings)) { LocalSerializer serializer = - new LocalSerializer( - new RemoteSerializer(configuration.getDatabaseInfo().getDatabaseId())); + new LocalSerializer(getRemoteSerializer()); LruGarbageCollector.Params params = LruGarbageCollector.Params.WithCacheSizeBytes( - configuration.getSettings().getCacheSizeBytes()); + configuration.settings.getCacheSizeBytes()); return MemoryPersistence.createLruGcMemoryPersistence(params, serializer); } @@ -98,10 +90,11 @@ protected Persistence createPersistence(Configuration configuration) { @Override protected RemoteStore createRemoteStore(Configuration configuration) { return new RemoteStore( + configuration.databaseInfo.getDatabaseId(), new RemoteStoreCallback(), getLocalStore(), - configuration.getDatastore(), - configuration.getAsyncQueue(), + getDatastore(), + configuration.asyncQueue, getConnectivityMonitor()); } @@ -110,8 +103,8 @@ protected SyncEngine createSyncEngine(Configuration configuration) { return new SyncEngine( getLocalStore(), getRemoteStore(), - configuration.getInitialUser(), - configuration.getMaxConcurrentLimboResolutions()); + configuration.initialUser, + configuration.maxConcurrentLimboResolutions); } /** diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SQLiteComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SQLiteComponentProvider.java index e77d3a64a29..6c730ac33c7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SQLiteComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SQLiteComponentProvider.java @@ -30,25 +30,25 @@ public class SQLiteComponentProvider extends MemoryComponentProvider { protected Scheduler createGarbageCollectionScheduler(Configuration configuration) { LruDelegate lruDelegate = ((SQLitePersistence) getPersistence()).getReferenceDelegate(); LruGarbageCollector gc = lruDelegate.getGarbageCollector(); - return gc.newScheduler(configuration.getAsyncQueue(), getLocalStore()); + return gc.newScheduler(configuration.asyncQueue, getLocalStore()); } @Override protected IndexBackfiller createIndexBackfiller(Configuration configuration) { - return new IndexBackfiller(getPersistence(), configuration.getAsyncQueue(), getLocalStore()); + return new IndexBackfiller(getPersistence(), configuration.asyncQueue, getLocalStore()); } @Override protected Persistence createPersistence(Configuration configuration) { LocalSerializer serializer = - new LocalSerializer(new RemoteSerializer(configuration.getDatabaseInfo().getDatabaseId())); + new LocalSerializer(getRemoteSerializer()); LruGarbageCollector.Params params = LruGarbageCollector.Params.WithCacheSizeBytes( - configuration.getSettings().getCacheSizeBytes()); + configuration.settings.getCacheSizeBytes()); return new SQLitePersistence( - configuration.getContext(), - configuration.getDatabaseInfo().getPersistenceKey(), - configuration.getDatabaseInfo().getDatabaseId(), + configuration.context, + configuration.databaseInfo.getPersistenceKey(), + configuration.databaseInfo.getDatabaseId(), serializer, params); } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AndroidConnectivityMonitor.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AndroidConnectivityMonitor.java index e8077e46089..4a4384651b0 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AndroidConnectivityMonitor.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/AndroidConnectivityMonitor.java @@ -43,7 +43,7 @@ *

    Implementation note: Most of the code here was shamelessly stolen from * https://github.com/grpc/grpc-java/blob/master/android/src/main/java/io/grpc/android/AndroidChannelBuilder.java */ -public final class AndroidConnectivityMonitor implements ConnectivityMonitor { +final class AndroidConnectivityMonitor implements ConnectivityMonitor { private static final String LOG_TAG = "AndroidConnectivityMonitor"; @@ -52,7 +52,7 @@ public final class AndroidConnectivityMonitor implements ConnectivityMonitor { @Nullable private Runnable unregisterRunnable; private final List> callbacks = new ArrayList<>(); - public AndroidConnectivityMonitor(Context context) { + AndroidConnectivityMonitor(Context context) { // This notnull restriction could be eliminated... the pre-N method doesn't // require a Context, and we could use that even on N+ if necessary. hardAssert(context != null, "Context must be non-null"); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index 92e6ae85888..ab6fcd2b157 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -17,17 +17,13 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import static com.google.firebase.firestore.util.Util.exceptionFromStatus; -import android.content.Context; import android.os.Build; -import androidx.annotation.Nullable; + import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.firestore.AggregateField; import com.google.firebase.firestore.FirebaseFirestoreException; -import com.google.firebase.firestore.auth.CredentialsProvider; -import com.google.firebase.firestore.auth.User; -import com.google.firebase.firestore.core.DatabaseInfo; import com.google.firebase.firestore.core.Query; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.MutableDocument; @@ -90,36 +86,14 @@ public class Datastore { "x-google-service", "x-google-gfe-request-trace")); - private final DatabaseInfo databaseInfo; - private final RemoteSerializer serializer; + protected final RemoteSerializer serializer; private final AsyncQueue workerQueue; - private final FirestoreChannel channel; - public Datastore( - DatabaseInfo databaseInfo, - AsyncQueue workerQueue, - CredentialsProvider authProvider, - CredentialsProvider appCheckProvider, - Context context, - @Nullable GrpcMetadataProvider metadataProvider) { - this.databaseInfo = databaseInfo; + Datastore(AsyncQueue workerQueue, RemoteSerializer serializer, FirestoreChannel channel) { this.workerQueue = workerQueue; - this.serializer = new RemoteSerializer(databaseInfo.getDatabaseId()); - this.channel = - initializeChannel( - databaseInfo, workerQueue, authProvider, appCheckProvider, context, metadataProvider); - } - - FirestoreChannel initializeChannel( - DatabaseInfo databaseInfo, - AsyncQueue workerQueue, - CredentialsProvider authProvider, - CredentialsProvider appCheckProvider, - Context context, - @Nullable GrpcMetadataProvider metadataProvider) { - return new FirestoreChannel( - workerQueue, context, authProvider, appCheckProvider, databaseInfo, metadataProvider); + this.serializer = serializer; + this.channel = channel; } void shutdown() { @@ -131,10 +105,6 @@ AsyncQueue getWorkerQueue() { return workerQueue; } - DatabaseInfo getDatabaseInfo() { - return databaseInfo; - } - /** Creates a new WatchStream that is still unstarted but uses a common shared channel */ WatchStream createWatchStream(WatchStream.Callback listener) { return new WatchStream(channel, workerQueue, serializer, listener); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java index ec138e72976..9c41969b9a4 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java @@ -17,6 +17,7 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import android.content.Context; + import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.firestore.BuildConfig; @@ -86,22 +87,36 @@ public void onClose(Status status) {} CredentialsProvider appCheckProvider, DatabaseInfo databaseInfo, GrpcMetadataProvider metadataProvider) { + this(asyncQueue, authProvider, appCheckProvider, databaseInfo.getDatabaseId(), metadataProvider, + getGrpcCallProvider(asyncQueue, context, authProvider, appCheckProvider, databaseInfo)); + } + + FirestoreChannel( + AsyncQueue asyncQueue, + CredentialsProvider authProvider, + CredentialsProvider appCheckProvider, + DatabaseId databaseId, + GrpcMetadataProvider metadataProvider, + GrpcCallProvider grpcCallProvider) { this.asyncQueue = asyncQueue; this.metadataProvider = metadataProvider; this.authProvider = authProvider; this.appCheckProvider = appCheckProvider; - - FirestoreCallCredentials firestoreHeaders = - new FirestoreCallCredentials(authProvider, appCheckProvider); - this.callProvider = new GrpcCallProvider(asyncQueue, context, databaseInfo, firestoreHeaders); - - DatabaseId databaseId = databaseInfo.getDatabaseId(); + this.callProvider = grpcCallProvider; this.resourcePrefixValue = String.format( "projects/%s/databases/%s", databaseId.getProjectId(), databaseId.getDatabaseId()); } - /** + private static GrpcCallProvider getGrpcCallProvider( + AsyncQueue asyncQueue, Context context, CredentialsProvider authProvider, + CredentialsProvider appCheckProvider, DatabaseInfo databaseInfo) { + FirestoreCallCredentials firestoreHeaders = + new FirestoreCallCredentials(authProvider, appCheckProvider); + return new GrpcCallProvider(asyncQueue, context, databaseInfo, firestoreHeaders); + } + + /** * Shuts down the grpc channel. This is not reversible and renders the FirestoreChannel unusable. */ public void shutdown() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/GrpcCallProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/GrpcCallProvider.java index 4109f30ddee..4cb9f9347c3 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/GrpcCallProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/GrpcCallProvider.java @@ -117,7 +117,7 @@ private ManagedChannel initChannel(Context context, DatabaseInfo databaseInfo) { } /** Creates a new ClientCall. */ - Task> createClientCall( + public Task> createClientCall( MethodDescriptor methodDescriptor) { return channelTask.continueWithTask( asyncQueue.getExecutor(), @@ -125,7 +125,7 @@ Task> createClientCall( } /** Shuts down the gRPC channel and the internal worker queue. */ - void shutdown() { + public void shutdown() { // Handling shutdown synchronously to avoid re-enqueuing on the AsyncQueue after shutdown has // started. ManagedChannel channel; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java new file mode 100644 index 00000000000..b9a649a56ba --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java @@ -0,0 +1,72 @@ +package com.google.firebase.firestore.remote; + +import static com.google.firebase.firestore.util.Assert.hardAssertNonNull; + +import com.google.firebase.firestore.core.ComponentProvider; +import com.google.firebase.firestore.util.AsyncQueue; + +public class RemoteComponenetProvider { + + + private GrpcCallProvider grpcCallProvider; + private RemoteSerializer remoteSerializer; + private FirestoreChannel firestoreChannel; + private Datastore datastore; + private ConnectivityMonitor connectivityMonitor; + + public void initialize(ComponentProvider.Configuration configuration) { + remoteSerializer = createRemoteSerializer(configuration); + grpcCallProvider = createGrpcCallProvider(configuration); + firestoreChannel = createFirestoreChannel(configuration); + datastore = createDatastore(configuration); + connectivityMonitor = createConnectivityMonitor(configuration); + } + + public GrpcCallProvider getGrpcCallProvider() { + return hardAssertNonNull(grpcCallProvider, "grpcCallProvider not initialized yet"); + } + + public RemoteSerializer getRemoteSerializer() { + return hardAssertNonNull(remoteSerializer, "remoteSerializer not initialized yet"); + } + + public FirestoreChannel getFirestoreChannel() { + return hardAssertNonNull(firestoreChannel, "firestoreChannel not initialized yet"); + } + + public Datastore getDatastore() { + return hardAssertNonNull(datastore, "datastore not initialized yet"); + } + + public ConnectivityMonitor getConnectivityMonitor() { + return hardAssertNonNull(connectivityMonitor, "connectivityMonitor not initialized yet"); + } + + protected GrpcCallProvider createGrpcCallProvider(ComponentProvider.Configuration configuration) { + FirestoreCallCredentials firestoreHeaders = + new FirestoreCallCredentials(configuration.authProvider, configuration.appCheckProvider); + return new GrpcCallProvider(configuration.asyncQueue, configuration.context, configuration.databaseInfo, firestoreHeaders); + } + + protected RemoteSerializer createRemoteSerializer(ComponentProvider.Configuration configuration) { + return new RemoteSerializer(configuration.databaseInfo.getDatabaseId()); + } + + protected FirestoreChannel createFirestoreChannel(ComponentProvider.Configuration configuration) { + return new FirestoreChannel( + configuration.asyncQueue, + configuration.authProvider, + configuration.appCheckProvider, + configuration.databaseInfo.getDatabaseId(), + configuration.metadataProvider, + getGrpcCallProvider()); + } + + protected Datastore createDatastore(ComponentProvider.Configuration configuration) { + return new Datastore(configuration.asyncQueue, getRemoteSerializer(), getFirestoreChannel()); + } + + protected ConnectivityMonitor createConnectivityMonitor(ComponentProvider.Configuration configuration) { + return new AndroidConnectivityMonitor(configuration.context); + } +} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index 732647e11bf..75d0a4fde13 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -67,6 +67,7 @@ public class RemoteStore implements WatchChangeAggregator.TargetMetadataProvider /** The log tag to use for this class. */ private static final String LOG_TAG = "RemoteStore"; private Consumer clearPersistenceCallback; + private final DatabaseId datastoreId; /** A callback interface for events from RemoteStore. */ public interface RemoteStoreCallback { @@ -156,11 +157,13 @@ public interface RemoteStoreCallback { private final Deque writePipeline; public RemoteStore( + DatabaseId databaseId, RemoteStoreCallback remoteStoreCallback, LocalStore localStore, Datastore datastore, AsyncQueue workerQueue, ConnectivityMonitor connectivityMonitor) { + this.datastoreId = databaseId; this.remoteStoreCallback = remoteStoreCallback; this.localStore = localStore; this.datastore = datastore; @@ -811,7 +814,7 @@ public TargetData getTargetDataForTarget(int targetId) { @Override public DatabaseId getDatabaseId() { - return this.datastore.getDatabaseInfo().getDatabaseId(); + return this.datastoreId; } public Task> runAggregateQuery( diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java index 9ac80ffd0bf..5e3ab6d0568 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java @@ -36,6 +36,7 @@ * */ class SynchronizedShutdownAwareExecutor implements Executor { + public static final String ASYNC_QUEUE_IS_SHUTDOWN = "AsyncQueue is shutdown"; /** * The single threaded executor that is backing this Executor. This is also the executor used * when some tasks explicitly request to run after shutdown has been initiated. @@ -115,7 +116,7 @@ protected void afterExecute(Runnable r, Throwable t) { Thread.currentThread().interrupt(); } } - if (t != null) { + if (t != null && !ASYNC_QUEUE_IS_SHUTDOWN.equals(t.getMessage())) { shutdownNow(); AsyncQueue.halt(t); } @@ -134,7 +135,7 @@ private SynchronizedShutdownAwareExecutor(ScheduledThreadPoolExecutor internalEx synchronized void verifyNotShutdown() { if (shutdownTask != null) { - throw new RejectedExecutionException("AsyncQueue is shutdown"); + throw new RejectedExecutionException(ASYNC_QUEUE_IS_SHUTDOWN); } } @@ -188,7 +189,6 @@ Task executeAndReportResult(Callable task) { completionSource.setResult(task.call()); } catch (Exception e) { completionSource.setException(e); - throw new RuntimeException(e); } }); } catch (RejectedExecutionException e) { diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java new file mode 100644 index 00000000000..794aabaa859 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java @@ -0,0 +1,155 @@ +package com.google.firebase.firestore; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import androidx.test.core.app.ApplicationProvider; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.android.gms.tasks.Tasks; +import com.google.firebase.firestore.core.ComponentProvider; +import com.google.firebase.firestore.model.DatabaseId; +import com.google.firebase.firestore.remote.GrpcCallProvider; +import com.google.firebase.firestore.remote.RemoteComponenetProvider; +import com.google.firebase.firestore.testutil.EmptyAppCheckTokenProvider; +import com.google.firebase.firestore.testutil.EmptyCredentialsProvider; +import com.google.firebase.firestore.util.Function; +import com.google.firestore.v1.FirestoreGrpc; +import com.google.firestore.v1.ListenRequest; +import com.google.firestore.v1.ListenResponse; +import com.google.firestore.v1.WriteRequest; +import com.google.firestore.v1.WriteResponse; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +import io.grpc.ClientCall; +import io.grpc.MockClientCall; + +public final class FirebaseFirestoreTestFactory { + + public final DatabaseId databaseId; + + private TaskCompletionSource nextInstance; + public final List> instances = new ArrayList<>(); + + public static class Instance { + public ComponentProvider componentProvider; + public GrpcCallProvider mockGrpcCallProvider; + private TaskCompletionSource> nextListenClientCallback; + public final List>> listenClientCallbacks = new ArrayList<>(); + private TaskCompletionSource> nextWriteClientCallback; + public final List>> writeClientCallbacks = new ArrayList<>(); + + private final TaskCompletionSource initializeComplete; + public final Task initializeCompleteTask; + + public Task enqueue(Runnable runnable) { + return configuration.asyncQueue.enqueue(runnable); + } + + public ComponentProvider.Configuration configuration; + + public Instance() { + prepareListenClientCallbacks(); + prepareWriteClientCallbacks(); + initializeComplete = new TaskCompletionSource<>(); + initializeCompleteTask = initializeComplete.getTask(); + } + + private void prepareWriteClientCallbacks() { + nextWriteClientCallback = new TaskCompletionSource<>(); + writeClientCallbacks.add(nextWriteClientCallback.getTask()); + } + + private void prepareListenClientCallbacks() { + nextListenClientCallback = new TaskCompletionSource<>(); + listenClientCallbacks.add(nextListenClientCallback.getTask()); + } + + private Task> createListenCallback() { + synchronized (listenClientCallbacks) { + MockClientCall mock = new MockClientCall<>(); + nextListenClientCallback.setResult(mock); + prepareListenClientCallbacks(); + return Tasks.forResult(mock); + } + } + + private Task> createWriteCallback() { + synchronized (writeClientCallbacks) { + MockClientCall mock = new MockClientCall<>(); + nextWriteClientCallback.setResult(mock); + prepareWriteClientCallbacks(); + return Tasks.forResult(mock); + } + } + } + + public final FirebaseFirestore firestore; + public final FirebaseFirestore.InstanceRegistry instanceRegistry = mock(FirebaseFirestore.InstanceRegistry.class); + + public FirebaseFirestoreTestFactory() { + databaseId = DatabaseId.forDatabase("p", "d"); + prepareInstances(); + firestore = new FirebaseFirestore( + ApplicationProvider.getApplicationContext(), + databaseId, + "k", + EmptyCredentialsProvider::new, + EmptyAppCheckTokenProvider::new, + this::componentProvider, + null, + instanceRegistry, + null + ); + FirebaseFirestoreSettings.Builder builder = new FirebaseFirestoreSettings.Builder(firestore.getFirestoreSettings()); + builder.setLocalCacheSettings(MemoryCacheSettings.newBuilder().build()); + firestore.setFirestoreSettings(builder.build()); + } + + public void setClearPersistenceMethod(Function> clearPersistenceMethod) { + firestore.clearPersistenceMethod = clearPersistenceMethod; + } + + private void prepareInstances() { + nextInstance = new TaskCompletionSource<>(); + instances.add(nextInstance.getTask()); + } + + private GrpcCallProvider mockGrpcCallProvider(Instance instance) { + GrpcCallProvider mockGrpcCallProvider = mock(GrpcCallProvider.class); + when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getListenMethod()))) + .thenAnswer(invocation -> instance.createListenCallback()); + when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getWriteMethod()))) + .thenAnswer(invocation -> instance.createWriteCallback()); + instance.mockGrpcCallProvider = mockGrpcCallProvider; + return mockGrpcCallProvider; + } + + private ComponentProvider componentProvider(FirebaseFirestoreSettings settings) { + Instance instance = new Instance(); + instance.componentProvider = ComponentProvider.defaultFactory(settings); + instance.componentProvider.setRemoteProvider(new RemoteComponenetProvider() { +// @Override +// protected Datastore createDatastore(ComponentProvider.Configuration configuration) { +// instance.configuration = configuration; +// return mockDatastore(instance); +// } +// + @Override + protected GrpcCallProvider createGrpcCallProvider(ComponentProvider.Configuration configuration) { + instance.configuration = configuration; + configuration.asyncQueue.enqueueAndForget(() -> instance.initializeComplete.setResult(null)); + return mockGrpcCallProvider(instance); + } + }); + TaskCompletionSource nextInstance = this.nextInstance; + prepareInstances(); + nextInstance.setResult(instance); + return instance.componentProvider; + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java index e987d58541a..cf296db703d 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/core/EventManagerTest.java @@ -170,80 +170,80 @@ public void testWillForwardOnOnlineStateChangedCalls() { assertEquals(Arrays.asList(OnlineState.UNKNOWN, OnlineState.ONLINE), events); } - @Test - public void xxx() { - Query query = Query.atPath(path("foo/bar")); - - Consumer clearPersistenceCallback = spy(new Consumer() { - @Override - public void accept(ByteString value) { - - } - }); - EventListener eventListener1 = mock(EventListener.class); - EventListener eventListener2 = mock(EventListener.class); - - QueryListener listener1 = new QueryListener(query, new ListenOptions(), eventListener1); - QueryListener listener2 = new QueryListener(query, new ListenOptions(), eventListener2); - - SyncEngine syncEngine; - EventManager eventManager; - RemoteStore remoteStore = mockRemoteStore(); - LocalStore localStore = createLruGcMemoryLocalStore(); - syncEngine = spy(new SyncEngine(localStore, remoteStore, User.UNAUTHENTICATED, 100, clearPersistenceCallback)); - eventManager = new EventManager(syncEngine); - eventManager.abortAllTargets(); - - eventManager.addQueryListener(listener1); - eventManager.addQueryListener(listener2); - - syncEngine.handleClearPersistence(ByteString.copyFromUtf8("sessionToken")); - - verify(syncEngine, times(1)) - .listen( - query, - /** shouldListenToRemote= */ - true); - - ArgumentMatcher abortedExceptionMatcher = e -> e.getCode() == Code.ABORTED; - verify(eventListener1, times(1)) - .onEvent(isNull(), argThat(abortedExceptionMatcher)); - - verify(eventListener2, times(1)) - .onEvent(isNull(), argThat(abortedExceptionMatcher)); - - verify(clearPersistenceCallback, times(1)).accept(ByteString.copyFromUtf8("sessionToken")); - - verify(remoteStore, times(1)).listen(any(TargetData.class)); - verify(remoteStore, atLeastOnce()).canUseNetwork(); - verify(remoteStore, times(1)).disableNetwork(); - verify(remoteStore, times(1)).enableNetwork(); - verifyNoMoreInteractions(remoteStore); - } - - @NonNull - private static RemoteStore mockRemoteStore() { - AtomicBoolean online = new AtomicBoolean(true); - RemoteStore remoteStore = mock(RemoteStore.class); - when(remoteStore.canUseNetwork()).thenAnswer(invocation -> online.get()); - doAnswer((Answer) invocation -> { - online.set(true); - return null; - }).when(remoteStore).enableNetwork(); - doAnswer((Answer) invocation -> { - online.set(false); - return null; - }).when(remoteStore).disableNetwork(); - return remoteStore; - } - - @NonNull - private static LocalStore createLruGcMemoryLocalStore() { - DatabaseId databaseId = DatabaseId.forProject("projectId"); - LocalSerializer serializer = new LocalSerializer(new RemoteSerializer(databaseId)); - Persistence persistence = MemoryPersistence.createLruGcMemoryPersistence( - LruGarbageCollector.Params.Default(), serializer); - persistence.start(); - return new LocalStore(persistence, new QueryEngine(), User.UNAUTHENTICATED); - } +// @Test +// public void xxx() { +// Query query = Query.atPath(path("foo/bar")); +// +// Consumer clearPersistenceCallback = spy(new Consumer() { +// @Override +// public void accept(ByteString value) { +// +// } +// }); +// EventListener eventListener1 = mock(EventListener.class); +// EventListener eventListener2 = mock(EventListener.class); +// +// QueryListener listener1 = new QueryListener(query, new ListenOptions(), eventListener1); +// QueryListener listener2 = new QueryListener(query, new ListenOptions(), eventListener2); +// +// SyncEngine syncEngine; +// EventManager eventManager; +// RemoteStore remoteStore = mockRemoteStore(); +// LocalStore localStore = createLruGcMemoryLocalStore(); +// syncEngine = spy(new SyncEngine(localStore, remoteStore, User.UNAUTHENTICATED, 100, clearPersistenceCallback)); +// eventManager = new EventManager(syncEngine); +// eventManager.abortAllTargets(); +// +// eventManager.addQueryListener(listener1); +// eventManager.addQueryListener(listener2); +// +// syncEngine.handleClearPersistence(ByteString.copyFromUtf8("sessionToken")); +// +// verify(syncEngine, times(1)) +// .listen( +// query, +// /** shouldListenToRemote= */ +// true); +// +// ArgumentMatcher abortedExceptionMatcher = e -> e.getCode() == Code.ABORTED; +// verify(eventListener1, times(1)) +// .onEvent(isNull(), argThat(abortedExceptionMatcher)); +// +// verify(eventListener2, times(1)) +// .onEvent(isNull(), argThat(abortedExceptionMatcher)); +// +// verify(clearPersistenceCallback, times(1)).accept(ByteString.copyFromUtf8("sessionToken")); +// +// verify(remoteStore, times(1)).listen(any(TargetData.class)); +// verify(remoteStore, atLeastOnce()).canUseNetwork(); +// verify(remoteStore, times(1)).disableNetwork(); +// verify(remoteStore, times(1)).enableNetwork(); +// verifyNoMoreInteractions(remoteStore); +// } +// +// @NonNull +// private static RemoteStore mockRemoteStore() { +// AtomicBoolean online = new AtomicBoolean(true); +// RemoteStore remoteStore = mock(RemoteStore.class); +// when(remoteStore.canUseNetwork()).thenAnswer(invocation -> online.get()); +// doAnswer((Answer) invocation -> { +// online.set(true); +// return null; +// }).when(remoteStore).enableNetwork(); +// doAnswer((Answer) invocation -> { +// online.set(false); +// return null; +// }).when(remoteStore).disableNetwork(); +// return remoteStore; +// } +// +// @NonNull +// private static LocalStore createLruGcMemoryLocalStore() { +// DatabaseId databaseId = DatabaseId.forProject("projectId"); +// LocalSerializer serializer = new LocalSerializer(new RemoteSerializer(databaseId)); +// Persistence persistence = MemoryPersistence.createLruGcMemoryPersistence( +// LruGarbageCollector.Params.Default(), serializer); +// persistence.start(); +// return new LocalStore(persistence, new QueryEngine(), User.UNAUTHENTICATED); +// } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java new file mode 100644 index 00000000000..9513a27e58d --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java @@ -0,0 +1,264 @@ +package com.google.firebase.firestore.integration; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.firestore.util.Executors.BACKGROUND_EXECUTOR; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import androidx.annotation.NonNull; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.FirebaseFirestoreException; +import com.google.firebase.firestore.FirebaseFirestoreTestFactory; +import com.google.firebase.firestore.QuerySnapshot; +import com.google.firebase.firestore.TestAccessHelper; +import com.google.firebase.firestore.model.DatabaseId; +import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firestore.v1.InitRequest; +import com.google.firestore.v1.InitResponse; +import com.google.firestore.v1.ListenRequest; +import com.google.firestore.v1.ListenResponse; +import com.google.protobuf.ByteString; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; + +import io.grpc.ClientCall; +import io.grpc.Metadata; +import io.grpc.MockClientCall; +import io.grpc.Status; + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class FirebaseFirestoreTest { + + private FirebaseFirestore firestore; + private FirebaseFirestoreTestFactory factory; + + private static void waitForSuccess(Task task) throws InterruptedException { + waitFor(task).getResult(); + } + + private static T waitForResult(Task task) throws InterruptedException { + return waitFor(task).getResult(); + } + + private static Exception waitForException(Task task) throws InterruptedException { + return waitFor(task).getException(); + } + + private T waitForException(Task task, Class clazz) throws InterruptedException { + return clazz.cast(waitForException(task)); + } + + @NonNull + public static String getResourcePrefixValue(DatabaseId databaseId) { + return String.format( + "projects/%s/databases/%s", databaseId.getProjectId(), databaseId.getDatabaseId()); + } + + private static Task waitFor(Task task) throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(1); + task.addOnSuccessListener(BACKGROUND_EXECUTOR, t -> countDownLatch.countDown()); + task.addOnFailureListener(BACKGROUND_EXECUTOR, e -> countDownLatch.countDown()); + task.addOnCanceledListener(BACKGROUND_EXECUTOR, () -> countDownLatch.countDown()); + countDownLatch.await(900, TimeUnit.SECONDS); + return task; + } + + @Before + public void before() { + factory = new FirebaseFirestoreTestFactory(); + firestore = factory.firestore; + } + + @After + public void after() throws Exception { + waitForSuccess(firestore.terminate()); + verify(factory.instanceRegistry, Mockito.atLeastOnce()).remove(factory.databaseId.getDatabaseId()); + Mockito.verifyNoMoreInteractions(factory.instanceRegistry); + + factory = null; + firestore = null; + } + + @Test() + public void clearPersistanceAfterStartupShouldRestartFirestoreClient() throws Exception { + // Trigger instantiation of FirestoreClient + firestore.collection("col"); + + FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); + waitForSuccess(first.initializeCompleteTask); + + AsyncQueue firstAsyncQueue = first.configuration.asyncQueue; + + assertFalse(firstAsyncQueue.isShuttingDown()); + + // Clearing persistence will require restarting FirestoreClient. + waitForSuccess(firestore.clearPersistence()); + + // Now we have a history of 2 instances. + FirebaseFirestoreTestFactory.Instance second = waitForResult(factory.instances.get(1)); + AsyncQueue secondAsyncQueue = second.configuration.asyncQueue; + + assertEquals(firstAsyncQueue.getExecutor(), secondAsyncQueue.getExecutor()); + + assertTrue(firstAsyncQueue.isShuttingDown()); + assertFalse(secondAsyncQueue.isShuttingDown()); + + // AsyncQueue of first instance should reject tasks. + Exception firstTask = waitForException(firstAsyncQueue.enqueue(() -> "Hi")); + assertThat(firstTask).isInstanceOf(RejectedExecutionException.class); + assertThat(firstTask).hasMessageThat().isEqualTo("AsyncQueue is shutdown"); + + // AsyncQueue of second instance should be functional. + assertThat(waitFor(secondAsyncQueue.enqueue(() -> "Hello")).getResult()).isEqualTo("Hello"); + + waitForSuccess(firestore.terminate()); + + // After terminate the second instance should also reject tasks. + Exception afterTerminate = waitForException(secondAsyncQueue.enqueue(() -> "Uh oh")); + assertThat(afterTerminate).isInstanceOf(RejectedExecutionException.class); + assertThat(afterTerminate).hasMessageThat().isEqualTo("AsyncQueue is shutdown"); + } + + @Test + public void clearPersistenceDueToInitResponse() throws Exception { + // Create a snapshot listener that will be active during handshake clearing of cache. + TaskCompletionSource snapshotTask1 = new TaskCompletionSource<>(); + firestore.collection("col") + .addSnapshotListener(BACKGROUND_EXECUTOR, (value, error) -> { + if (error == null) { + // Skip cached results. + if (value.getMetadata().isFromCache()) return; + snapshotTask1.setResult(value); + } else { + snapshotTask1.setException(error); + } + }); + + // Wait for first FirestoreClient to instantiate + FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); + + // Wait for Listen CallClient to be created. + MockClientCall callbackTask1 = waitForResult(first.listenClientCallbacks.get(0)); + + // Wait for Listen CallClient to have start called by FirestoreClient. + // This gives us a response callback to simulate response from server. + ClientCall.Listener responseListener1 = waitForResult(callbackTask1.getStart()).first; + + // Wait for ListenRequest. + // We expect an empty init request because the database is fresh. + assertThat(waitForResult(callbackTask1.getSent(0))) + .isEqualTo(listenRequestWith(InitRequest.getDefaultInstance())); + + // Simulate a successful InitResponse from server. + waitForSuccess(first.enqueue(() -> responseListener1.onMessage(ListenResponse.newBuilder() + .setInitResponse(InitResponse.newBuilder() + .setSessionToken(ByteString.copyFromUtf8("token1"))) + .build()))); + + // We expect previous addSnapshotListener to cause a, AddTarget request. + assertTrue(waitForResult(callbackTask1.getSent(1)).hasAddTarget()); + + // Simulate Database deletion by closing connection with NOT_FOUND. + waitForSuccess(first.enqueue(() -> responseListener1.onClose(Status.NOT_FOUND, new Metadata()))); + + // We expect client to reconnect Listen stream. + MockClientCall callbackTask2 = waitForResult(first.listenClientCallbacks.get(1)); + + // Wait for Listen CallClient to have start called by FirestoreClient. + // This gives us a response callback to simulate response from server. + ClientCall.Listener responseListener2 = waitForResult(callbackTask2.getStart()).first; + + // Wait for ListenRequest. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(callbackTask2.getSent(0))) + .isEqualTo(listenRequestWith(InitRequest.newBuilder() + .setSessionToken(ByteString.copyFromUtf8("token1")) + .build())); + + + // This task will complete when clearPersistence is invoked on FirebaseFirestore. + Task clearPersistenceTask = setupClearPersistenceTask(); + + // Simulate a clear cache InitResponse from server. + waitForSuccess(first.enqueue(() -> responseListener2.onMessage(ListenResponse.newBuilder() + .setInitResponse(InitResponse.newBuilder() + .setSessionToken(ByteString.copyFromUtf8("token2")) + .setClearCache(true)) + .build()))); + + // Wait for cleanPersistence to be run. + waitForSuccess(clearPersistenceTask); + + // Verify that the first FirestoreClient was shutdown. If the GrpcCallProvider component has + // has it's shutdown method called, then we know shutdown was triggered. + verify(first.mockGrpcCallProvider, times(1)).shutdown(); + + // Snapshot listeners should fail with ABORTED + FirebaseFirestoreException exception = waitForException(snapshotTask1.getTask(), FirebaseFirestoreException.class); + assertThat(exception.getCode()).isEqualTo(FirebaseFirestoreException.Code.ABORTED); + + // Start another snapshot listener + TaskCompletionSource snapshotTask2 = new TaskCompletionSource<>(); + firestore.collection("col") + .addSnapshotListener(BACKGROUND_EXECUTOR, (value, error) -> { + if (error == null) { + // Skip cached results. + if (value.getMetadata().isFromCache()) return; + snapshotTask2.setResult(value); + } else { + snapshotTask2.setException(error); + } + }); + + // Wait for first FirestoreClient to instantiate + FirebaseFirestoreTestFactory.Instance second = waitForResult(factory.instances.get(1)); + + // Wait for Listen CallClient to be created. + MockClientCall callbackTask3 = waitForResult(second.listenClientCallbacks.get(0)); + + // Wait for Listen CallClient to have start called by FirestoreClient. + // This gives us a response callback to simulate response from server. + ClientCall.Listener responseListener3 = waitForResult(callbackTask3.getStart()).first; + + // Wait for ListenRequest. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(callbackTask3.getSent(0))) + .isEqualTo(listenRequestWith(InitRequest.newBuilder() + .setSessionToken(ByteString.copyFromUtf8("token2")) + .build())); + } + + private ListenRequest listenRequestWith(InitRequest initRequest) { + return ListenRequest.newBuilder() + .setDatabase(getResourcePrefixValue(factory.databaseId)) + .setInitRequest(initRequest) + .build(); + } + + @NonNull + private Task setupClearPersistenceTask() { + TaskCompletionSource clearPersistenceTask = new TaskCompletionSource<>(); + factory.setClearPersistenceMethod(executor -> { + executor.execute(() -> clearPersistenceTask.setResult(null)); + return clearPersistenceTask.getTask(); + }); + return clearPersistenceTask.getTask(); + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java index b33ebb8af7c..e5f5e65ecfc 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java @@ -237,31 +237,14 @@ int getWritesSent() { private MockWatchStream watchStream; private MockWriteStream writeStream; - private final RemoteSerializer serializer; - private int writeStreamRequestCount; private int watchStreamRequestCount; - public MockDatastore(DatabaseInfo databaseInfo, AsyncQueue workerQueue, Context context) { + public MockDatastore(DatabaseInfo databaseInfo, AsyncQueue workerQueue) { super( - databaseInfo, workerQueue, - new EmptyCredentialsProvider(), - new EmptyAppCheckTokenProvider(), - context, + new RemoteSerializer(databaseInfo.getDatabaseId()), null); - this.serializer = new RemoteSerializer(getDatabaseInfo().getDatabaseId()); - } - - @Override - FirestoreChannel initializeChannel( - DatabaseInfo databaseInfo, - AsyncQueue workerQueue, - CredentialsProvider authCredentialsProvider, - CredentialsProvider appCheckTokenProvider, - Context context, - @Nullable GrpcMetadataProvider metadataProvider) { - return null; } @Override diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/MemorySpecTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/MemorySpecTest.java index 35383d7a6a6..71de6c389c6 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/MemorySpecTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/MemorySpecTest.java @@ -20,8 +20,9 @@ import com.google.firebase.firestore.local.LruGarbageCollector; import com.google.firebase.firestore.local.MemoryPersistence; import com.google.firebase.firestore.local.Persistence; -import com.google.firebase.firestore.model.DatabaseId; -import com.google.firebase.firestore.remote.RemoteSerializer; +import com.google.firebase.firestore.remote.Datastore; +import com.google.firebase.firestore.remote.MockDatastore; +import com.google.firebase.firestore.remote.RemoteComponenetProvider; import java.util.Set; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -40,7 +41,7 @@ protected boolean isExcluded(Set tags) { @Override protected MemoryComponentProvider initializeComponentProvider( - ComponentProvider.Configuration configuration, boolean useEagerGc) { + RemoteComponenetProvider remoteProvider, ComponentProvider.Configuration configuration, boolean useEagerGc) { MemoryComponentProvider provider = new MemoryComponentProvider() { @Override @@ -48,13 +49,18 @@ protected Persistence createPersistence(Configuration configuration) { if (useEagerGc) { return MemoryPersistence.createEagerGcMemoryPersistence(); } else { - DatabaseId databaseId = DatabaseId.forProject("projectId"); - LocalSerializer serializer = new LocalSerializer(new RemoteSerializer(databaseId)); + LocalSerializer serializer = new LocalSerializer(getRemoteSerializer()); return MemoryPersistence.createLruGcMemoryPersistence( LruGarbageCollector.Params.Default(), serializer); } } }; + provider.setRemoteProvider(new RemoteComponenetProvider() { + @Override + protected Datastore createDatastore(ComponentProvider.Configuration configuration) { + return new MockDatastore(configuration.databaseInfo, configuration.asyncQueue); + } + }); provider.initialize(configuration); return provider; } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SQLiteSpecTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SQLiteSpecTest.java index 9cc38490ea0..dea563fcd38 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SQLiteSpecTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SQLiteSpecTest.java @@ -16,6 +16,8 @@ import com.google.firebase.firestore.core.ComponentProvider; import com.google.firebase.firestore.core.SQLiteComponentProvider; +import com.google.firebase.firestore.remote.RemoteComponenetProvider; + import java.util.Set; import org.json.JSONObject; import org.junit.runner.RunWith; @@ -35,8 +37,9 @@ protected void specSetUp(JSONObject config) { @Override protected SQLiteComponentProvider initializeComponentProvider( - ComponentProvider.Configuration configuration, boolean garbageCollectionEnabled) { + RemoteComponenetProvider remoteProvider, ComponentProvider.Configuration configuration, boolean garbageCollectionEnabled) { SQLiteComponentProvider provider = new SQLiteComponentProvider(); + provider.setRemoteProvider(remoteProvider); provider.initialize(configuration); return provider; } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index cedc0395a97..3b231948e4a 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -71,8 +71,10 @@ import com.google.firebase.firestore.model.mutation.Mutation; import com.google.firebase.firestore.model.mutation.MutationBatchResult; import com.google.firebase.firestore.model.mutation.MutationResult; +import com.google.firebase.firestore.remote.Datastore; import com.google.firebase.firestore.remote.ExistenceFilter; import com.google.firebase.firestore.remote.MockDatastore; +import com.google.firebase.firestore.remote.RemoteComponenetProvider; import com.google.firebase.firestore.remote.RemoteEvent; import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.remote.RemoteStore; @@ -83,6 +85,8 @@ import com.google.firebase.firestore.remote.WatchChange.WatchTargetChange; import com.google.firebase.firestore.remote.WatchChange.WatchTargetChangeType; import com.google.firebase.firestore.remote.WatchStream; +import com.google.firebase.firestore.testutil.EmptyAppCheckTokenProvider; +import com.google.firebase.firestore.testutil.EmptyCredentialsProvider; import com.google.firebase.firestore.testutil.TestUtil; import com.google.firebase.firestore.util.Assert; import com.google.firebase.firestore.util.AsyncQueue; @@ -259,7 +263,7 @@ public static void log(String line) { // protected abstract ComponentProvider initializeComponentProvider( - ComponentProvider.Configuration configuration, boolean garbageCollectionEnabled); + RemoteComponenetProvider remoteProvider, ComponentProvider.Configuration configuration, boolean garbageCollectionEnabled); private boolean shouldRun(Set tags) { for (String tag : tags) { @@ -315,19 +319,28 @@ protected void specTearDown() throws Exception { */ private void initClient() { queue = new AsyncQueue(); - datastore = new MockDatastore(databaseInfo, queue, ApplicationProvider.getApplicationContext()); + datastore = new MockDatastore(databaseInfo, queue); ComponentProvider.Configuration configuration = new ComponentProvider.Configuration( ApplicationProvider.getApplicationContext(), queue, databaseInfo, - datastore, currentUser, maxConcurrentLimboResolutions, - new FirebaseFirestoreSettings.Builder().build()); - - ComponentProvider provider = initializeComponentProvider(configuration, useEagerGcForMemory); + new FirebaseFirestoreSettings.Builder().build(), + new EmptyCredentialsProvider(), + new EmptyAppCheckTokenProvider(), + null + ); + + RemoteComponenetProvider remoteProvider = new RemoteComponenetProvider() { + @Override + protected Datastore createDatastore(ComponentProvider.Configuration configuration) { + return datastore; + } + }; + ComponentProvider provider = initializeComponentProvider(remoteProvider, configuration, useEagerGcForMemory); localPersistence = provider.getPersistence(); if (localPersistence.getReferenceDelegate() instanceof LruDelegate) { lruGarbageCollector = diff --git a/firebase-firestore/src/test/java/io/grpc/MockClientCall.java b/firebase-firestore/src/test/java/io/grpc/MockClientCall.java new file mode 100644 index 00000000000..b2411cc7367 --- /dev/null +++ b/firebase-firestore/src/test/java/io/grpc/MockClientCall.java @@ -0,0 +1,74 @@ +package io.grpc; + +import android.util.Pair; + +import androidx.annotation.Nullable; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; + +import java.util.ArrayList; +import java.util.List; + +public class MockClientCall extends ClientCall { + + private int sentIndex = -1; + private List> sent = new ArrayList<>(); + private TaskCompletionSource, Metadata>> startTask = new TaskCompletionSource<>(); + private TaskCompletionSource> cancelTask = new TaskCompletionSource<>(); + + @Override + public void start(Listener responseListener, Metadata headers) { + startTask.setResult(Pair.create(responseListener, headers)); + System.out.println(">>> start"); + } + + @Override + public void request(int numMessages) { + System.out.println(">>> request"); + } + + @Override + public void cancel(@Nullable String message, @Nullable Throwable cause) { + cancelTask.setResult(Pair.create(message, cause)); + System.out.println(">>> cancel"); + } + + @Override + public void halfClose() { + System.out.println(">>> halfClose"); + } + + @Override + public void sendMessage(ReqT message) { + System.out.println(">>> sendMessage"); + final TaskCompletionSource sourceTask; + synchronized (sent) { + sentIndex++; + if (sent.size() > sentIndex) { + sourceTask = sent.get(sentIndex); + } else { + sourceTask = new TaskCompletionSource<>(); + sent.add(sourceTask); + } + } + sourceTask.setResult(message); + } + + public Task, Metadata>> getStart() { + return startTask.getTask(); + } + + public Task> getCancel() { + return cancelTask.getTask(); + } + + public Task getSent(int index) { + synchronized (sent) { + while (sent.size() <= index) { + sent.add(new TaskCompletionSource<>()); + } + return sent.get(index).getTask(); + } + } +} From e58f8fca9d7ae557669aece126133033847ca040 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 12 Jun 2024 10:44:08 -0400 Subject: [PATCH 24/30] Comments and tests --- .../firestore/FirestoreClientProvider.java | 14 ++ .../remote/RemoteComponenetProvider.java | 14 ++ .../SynchronizedShutdownAwareExecutor.java | 14 ++ .../FirebaseFirestoreTestFactory.java | 56 +++--- .../integration/FirebaseFirestoreTest.java | 171 +++++++++++------- .../firestore/integration/TestClientCall.java | 97 ++++++++++ .../integration/TestEventListener.java | 113 ++++++++++++ .../src/test/java/io/grpc/MockClientCall.java | 74 -------- 8 files changed, 386 insertions(+), 167 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java delete mode 100644 firebase-firestore/src/test/java/io/grpc/MockClientCall.java diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java index 6fbd68ed81a..c70f1eb41ad 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java @@ -1,3 +1,17 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package com.google.firebase.firestore; import androidx.annotation.GuardedBy; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java index b9a649a56ba..3048532a169 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java @@ -1,3 +1,17 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package com.google.firebase.firestore.remote; import static com.google.firebase.firestore.util.Assert.hardAssertNonNull; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java index 5e3ab6d0568..320c8ec9557 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java @@ -1,3 +1,17 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package com.google.firebase.firestore.util; import static com.google.firebase.firestore.util.Assert.fail; diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java index 794aabaa859..27fbe5007b1 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java @@ -1,3 +1,17 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package com.google.firebase.firestore; import static org.mockito.ArgumentMatchers.eq; @@ -27,7 +41,7 @@ import java.util.concurrent.Executor; import io.grpc.ClientCall; -import io.grpc.MockClientCall; +import com.google.firebase.firestore.integration.TestClientCall; public final class FirebaseFirestoreTestFactory { @@ -39,10 +53,10 @@ public final class FirebaseFirestoreTestFactory { public static class Instance { public ComponentProvider componentProvider; public GrpcCallProvider mockGrpcCallProvider; - private TaskCompletionSource> nextListenClientCallback; - public final List>> listenClientCallbacks = new ArrayList<>(); - private TaskCompletionSource> nextWriteClientCallback; - public final List>> writeClientCallbacks = new ArrayList<>(); + private TaskCompletionSource> nextListen; + public final List>> listens = new ArrayList<>(); + private TaskCompletionSource> nextWrite; + public final List>> writes = new ArrayList<>(); private final TaskCompletionSource initializeComplete; public final Task initializeCompleteTask; @@ -54,36 +68,28 @@ public Task enqueue(Runnable runnable) { public ComponentProvider.Configuration configuration; public Instance() { - prepareListenClientCallbacks(); - prepareWriteClientCallbacks(); + nextListen = new TaskCompletionSource<>(); + listens.add(nextListen.getTask()); + nextWrite = new TaskCompletionSource<>(); + writes.add(nextWrite.getTask()); initializeComplete = new TaskCompletionSource<>(); initializeCompleteTask = initializeComplete.getTask(); } - private void prepareWriteClientCallbacks() { - nextWriteClientCallback = new TaskCompletionSource<>(); - writeClientCallbacks.add(nextWriteClientCallback.getTask()); - } - - private void prepareListenClientCallbacks() { - nextListenClientCallback = new TaskCompletionSource<>(); - listenClientCallbacks.add(nextListenClientCallback.getTask()); - } - private Task> createListenCallback() { - synchronized (listenClientCallbacks) { - MockClientCall mock = new MockClientCall<>(); - nextListenClientCallback.setResult(mock); - prepareListenClientCallbacks(); + synchronized (listens) { + TestClientCall mock = new TestClientCall<>(nextListen); + nextListen = new TaskCompletionSource<>(); + listens.add(nextListen.getTask()); return Tasks.forResult(mock); } } private Task> createWriteCallback() { - synchronized (writeClientCallbacks) { - MockClientCall mock = new MockClientCall<>(); - nextWriteClientCallback.setResult(mock); - prepareWriteClientCallbacks(); + synchronized (writes) { + TestClientCall mock = new TestClientCall<>(nextWrite); + nextWrite = new TaskCompletionSource<>(); + writes.add(nextWrite.getTask()); return Tasks.forResult(mock); } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java index 9513a27e58d..18ab2acffc5 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java @@ -1,6 +1,21 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package com.google.firebase.firestore.integration; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.firestore.testutil.TestUtil.map; import static com.google.firebase.firestore.util.Executors.BACKGROUND_EXECUTOR; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -12,17 +27,20 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.firebase.firestore.CollectionReference; +import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.FirebaseFirestoreTestFactory; import com.google.firebase.firestore.QuerySnapshot; -import com.google.firebase.firestore.TestAccessHelper; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firestore.v1.InitRequest; import com.google.firestore.v1.InitResponse; import com.google.firestore.v1.ListenRequest; import com.google.firestore.v1.ListenResponse; +import com.google.firestore.v1.WriteRequest; +import com.google.firestore.v1.WriteResponse; import com.google.protobuf.ByteString; import org.junit.After; @@ -33,13 +51,12 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.Iterator; import java.util.concurrent.CountDownLatch; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; -import io.grpc.ClientCall; import io.grpc.Metadata; -import io.grpc.MockClientCall; import io.grpc.Status; @RunWith(RobolectricTestRunner.class) @@ -61,10 +78,6 @@ private static Exception waitForException(Task task) throws InterruptedExcept return waitFor(task).getException(); } - private T waitForException(Task task, Class clazz) throws InterruptedException { - return clazz.cast(waitForException(task)); - } - @NonNull public static String getResourcePrefixValue(DatabaseId databaseId) { return String.format( @@ -80,6 +93,10 @@ private static Task waitFor(Task task) throws InterruptedException { return task; } + private T waitForException(Task task, Class clazz) throws InterruptedException { + return clazz.cast(waitForException(task)); + } + @Before public void before() { factory = new FirebaseFirestoreTestFactory(); @@ -139,69 +156,48 @@ public void clearPersistanceAfterStartupShouldRestartFirestoreClient() throws Ex @Test public void clearPersistenceDueToInitResponse() throws Exception { // Create a snapshot listener that will be active during handshake clearing of cache. - TaskCompletionSource snapshotTask1 = new TaskCompletionSource<>(); - firestore.collection("col") - .addSnapshotListener(BACKGROUND_EXECUTOR, (value, error) -> { - if (error == null) { - // Skip cached results. - if (value.getMetadata().isFromCache()) return; - snapshotTask1.setResult(value); - } else { - snapshotTask1.setException(error); - } - }); + TestEventListener snapshotListener1 = new TestEventListener<>(); + firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener1); + Iterator> snapshots1 = snapshotListener1.iterator(); + + // First snapshot will be from cache. + assertTrue(waitForResult(snapshots1.next()).getMetadata().isFromCache()); // Wait for first FirestoreClient to instantiate FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); // Wait for Listen CallClient to be created. - MockClientCall callbackTask1 = waitForResult(first.listenClientCallbacks.get(0)); + TestClientCall callback1 = waitForResult(first.listens.get(0)); - // Wait for Listen CallClient to have start called by FirestoreClient. - // This gives us a response callback to simulate response from server. - ClientCall.Listener responseListener1 = waitForResult(callbackTask1.getStart()).first; - - // Wait for ListenRequest. + // Wait for ListenRequest handshake. // We expect an empty init request because the database is fresh. - assertThat(waitForResult(callbackTask1.getSent(0))) - .isEqualTo(listenRequestWith(InitRequest.getDefaultInstance())); + assertThat(waitForResult(callback1.getRequest(0))) + .isEqualTo(listenRequest(InitRequest.getDefaultInstance())); // Simulate a successful InitResponse from server. - waitForSuccess(first.enqueue(() -> responseListener1.onMessage(ListenResponse.newBuilder() - .setInitResponse(InitResponse.newBuilder() - .setSessionToken(ByteString.copyFromUtf8("token1"))) - .build()))); + waitForSuccess(first.enqueue(() -> callback1.listener.onMessage(listenResponse(initResponse("token1"))))); // We expect previous addSnapshotListener to cause a, AddTarget request. - assertTrue(waitForResult(callbackTask1.getSent(1)).hasAddTarget()); + assertTrue(waitForResult(callback1.getRequest(1)).hasAddTarget()); // Simulate Database deletion by closing connection with NOT_FOUND. - waitForSuccess(first.enqueue(() -> responseListener1.onClose(Status.NOT_FOUND, new Metadata()))); + waitForSuccess(first.enqueue(() -> callback1.listener.onClose(Status.NOT_FOUND, new Metadata()))); // We expect client to reconnect Listen stream. - MockClientCall callbackTask2 = waitForResult(first.listenClientCallbacks.get(1)); - - // Wait for Listen CallClient to have start called by FirestoreClient. - // This gives us a response callback to simulate response from server. - ClientCall.Listener responseListener2 = waitForResult(callbackTask2.getStart()).first; + TestClientCall callback2 = waitForResult(first.listens.get(1)); // Wait for ListenRequest. // We expect FirestoreClient to send InitRequest with previous token. - assertThat(waitForResult(callbackTask2.getSent(0))) - .isEqualTo(listenRequestWith(InitRequest.newBuilder() - .setSessionToken(ByteString.copyFromUtf8("token1")) - .build())); + assertThat(waitForResult(callback2.getRequest(0))) + .isEqualTo(listenRequest(initRequest("token1"))); // This task will complete when clearPersistence is invoked on FirebaseFirestore. Task clearPersistenceTask = setupClearPersistenceTask(); // Simulate a clear cache InitResponse from server. - waitForSuccess(first.enqueue(() -> responseListener2.onMessage(ListenResponse.newBuilder() - .setInitResponse(InitResponse.newBuilder() - .setSessionToken(ByteString.copyFromUtf8("token2")) - .setClearCache(true)) - .build()))); + waitForSuccess(first.enqueue(() -> callback2.listener.onMessage( + listenResponse(initResponse("token2", true))))); // Wait for cleanPersistence to be run. waitForSuccess(clearPersistenceTask); @@ -211,47 +207,86 @@ public void clearPersistenceDueToInitResponse() throws Exception { verify(first.mockGrpcCallProvider, times(1)).shutdown(); // Snapshot listeners should fail with ABORTED - FirebaseFirestoreException exception = waitForException(snapshotTask1.getTask(), FirebaseFirestoreException.class); + FirebaseFirestoreException exception = waitForException(snapshots1.next(), FirebaseFirestoreException.class); assertThat(exception.getCode()).isEqualTo(FirebaseFirestoreException.Code.ABORTED); // Start another snapshot listener - TaskCompletionSource snapshotTask2 = new TaskCompletionSource<>(); - firestore.collection("col") - .addSnapshotListener(BACKGROUND_EXECUTOR, (value, error) -> { - if (error == null) { - // Skip cached results. - if (value.getMetadata().isFromCache()) return; - snapshotTask2.setResult(value); - } else { - snapshotTask2.setException(error); - } - }); + TestEventListener snapshotListener2 = new TestEventListener<>(); + firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener2); // Wait for first FirestoreClient to instantiate FirebaseFirestoreTestFactory.Instance second = waitForResult(factory.instances.get(1)); // Wait for Listen CallClient to be created. - MockClientCall callbackTask3 = waitForResult(second.listenClientCallbacks.get(0)); - - // Wait for Listen CallClient to have start called by FirestoreClient. - // This gives us a response callback to simulate response from server. - ClientCall.Listener responseListener3 = waitForResult(callbackTask3.getStart()).first; + TestClientCall callback3 = waitForResult(second.listens.get(0)); // Wait for ListenRequest. // We expect FirestoreClient to send InitRequest with previous token. - assertThat(waitForResult(callbackTask3.getSent(0))) - .isEqualTo(listenRequestWith(InitRequest.newBuilder() - .setSessionToken(ByteString.copyFromUtf8("token2")) - .build())); + assertThat(waitForResult(callback3.getRequest(0))) + .isEqualTo(listenRequest(initRequest("token2"))); + } + + @Test + public void preserveWritesWhenDisconnectedWithNotFound() throws Exception { + CollectionReference col = firestore.collection("col"); + DocumentReference doc1 = col.document(); + DocumentReference doc2 = col.document(); + DocumentReference doc3 = col.document(); + doc1.set(map("foo", "A")); + doc2.set(map("foo", "B")); + doc3.set(map("foo", "C")); + + // Wait for first FirestoreClient to instantiate + FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); + + // Wait for Listen CallClient to be created. + TestClientCall callback1 = waitForResult(first.writes.get(0)); + + // Wait for WriteRequest handshake. + // We expect an empty init request because the database is fresh. + assertThat(waitForResult(callback1.getRequest(0))) + .isEqualTo(writeRequest(InitRequest.getDefaultInstance())); + } + + private static ListenResponse listenResponse(InitResponse initResponse) { + return ListenResponse.newBuilder() + .setInitResponse(initResponse) + .build(); } - private ListenRequest listenRequestWith(InitRequest initRequest) { + private ListenRequest listenRequest(InitRequest initRequest) { return ListenRequest.newBuilder() .setDatabase(getResourcePrefixValue(factory.databaseId)) .setInitRequest(initRequest) .build(); } + private WriteRequest writeRequest(InitRequest initRequest) { + return WriteRequest.newBuilder() + .setDatabase(getResourcePrefixValue(factory.databaseId)) + .setInitRequest(initRequest) + .build(); + } + + private static InitResponse initResponse(String token) { + return InitResponse.newBuilder() + .setSessionToken(ByteString.copyFromUtf8(token)) + .build(); + } + + private static InitResponse initResponse(String token, boolean clearCache) { + return InitResponse.newBuilder() + .setSessionToken(ByteString.copyFromUtf8(token)) + .setClearCache(clearCache) + .build(); + } + + private static InitRequest initRequest(String token) { + return InitRequest.newBuilder() + .setSessionToken(ByteString.copyFromUtf8(token)) + .build(); + } + @NonNull private Task setupClearPersistenceTask() { TaskCompletionSource clearPersistenceTask = new TaskCompletionSource<>(); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java new file mode 100644 index 00000000000..38eb8d96eb2 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java @@ -0,0 +1,97 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.integration; + +import android.util.Pair; + +import androidx.annotation.Nullable; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; + +import java.util.ArrayList; +import java.util.List; + +import io.grpc.ClientCall; +import io.grpc.Metadata; + +/** + * Construct a ClientCall test harness. + */ +public class TestClientCall extends ClientCall { + + public Metadata headers; + public Listener listener; + private int sentIndex = -1; + private List> requests = new ArrayList<>(); + private TaskCompletionSource> startTask = new TaskCompletionSource<>(); + private TaskCompletionSource> cancelTask = new TaskCompletionSource<>(); + + /** + * Construct a ClientCall test harness. + * + * The {@code #headers} and {@code #listener} will be populated when {@code #startTask} + * completes. + * + * @param startTask Will complete when ClientCall has start callback invoked. + */ + public TestClientCall(TaskCompletionSource> startTask) { + this.startTask = startTask; + } + + @Override + public void start(Listener responseListener, Metadata headers) { + this.listener = responseListener; + this.headers = headers; + startTask.setResult(this); + } + + @Override + public void request(int numMessages) { + } + + @Override + public void cancel(@Nullable String message, @Nullable Throwable cause) { + cancelTask.setResult(Pair.create(message, cause)); + } + + @Override + public void halfClose() { + } + + @Override + public void sendMessage(ReqT message) { + final TaskCompletionSource sourceTask; + synchronized (requests) { + sentIndex++; + if (requests.size() > sentIndex) { + sourceTask = requests.get(sentIndex); + } else { + sourceTask = new TaskCompletionSource<>(); + requests.add(sourceTask); + } + } + sourceTask.setResult(message); + } + + public Task getRequest(int index) { + synchronized (requests) { + while (requests.size() <= index) { + requests.add(new TaskCompletionSource<>()); + } + return requests.get(index).getTask(); + } + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java new file mode 100644 index 00000000000..ae5f3a970bf --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java @@ -0,0 +1,113 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.firestore.integration; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; +import com.google.firebase.firestore.EventListener; +import com.google.firebase.firestore.FirebaseFirestoreException; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * EventListener test harness. + */ +class TestEventListener implements EventListener, Iterable> { + + private int eventCount = 0; + private TaskCompletionSource next; + private List> events = new ArrayList<>(); + + public TestEventListener() { + next = computeIfAbsentIndex(eventCount); + } + + private TaskCompletionSource computeIfAbsentIndex(int i) { + while (events.size() <= i) { + events.add(new TaskCompletionSource<>()); + } + return events.get(i); + } + + @Override + public synchronized void onEvent(@Nullable T value, @Nullable FirebaseFirestoreException error) { + if (error == null) { + next.setResult(value); + } else { + next.setException(error); + } + eventCount++; + next = computeIfAbsentIndex(eventCount); + } + + /** + * Get result from {@code #onEvent}. Task that that completes with event result. + * @param index 0 indexed sequence of {@code #onEvent} invocations. + * @return Task that completes when {@code #onEvent} is called. + */ + @NonNull + public synchronized Task get(int index) { + return computeIfAbsentIndex(index).getTask(); + } + + /** + * Iterates over {@code #onEvent}. + * + * This will return the same as calling {@code #get(0..n)}. + * The Iterator is thread safe. + * Iteration will stop upon event task that fails or cancels. + * + * A loop that waits for event task to complete before getting next event task will continue to + * iterate indefinitely. The next event task is not available until previous task is successful. + * Attempting to iterate past an event task that is not yet successful will throw + * {#code NoSuchElementException} and {@code #hasNext()} will be false. In this way, iteration + * can complete without blocking for already received events. + * + * @return Iterator of Tasks that complete when {@code #onEvent} is called. + */ + @NonNull + @Override + public Iterator> iterator() { + return new Iterator>() { + + private int i = -1; + private Task current; + + @Override + public synchronized boolean hasNext() { + // We always return first, and continue to return tasks so long as previous + // is successful. A task that hasn't completed, will also mark the end of + // iteration. + return i < 0 || current.isSuccessful(); + } + + @Override + public synchronized Task next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + i++; + current = get(i); + return current; + } + }; + } +} diff --git a/firebase-firestore/src/test/java/io/grpc/MockClientCall.java b/firebase-firestore/src/test/java/io/grpc/MockClientCall.java deleted file mode 100644 index b2411cc7367..00000000000 --- a/firebase-firestore/src/test/java/io/grpc/MockClientCall.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.grpc; - -import android.util.Pair; - -import androidx.annotation.Nullable; - -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; - -import java.util.ArrayList; -import java.util.List; - -public class MockClientCall extends ClientCall { - - private int sentIndex = -1; - private List> sent = new ArrayList<>(); - private TaskCompletionSource, Metadata>> startTask = new TaskCompletionSource<>(); - private TaskCompletionSource> cancelTask = new TaskCompletionSource<>(); - - @Override - public void start(Listener responseListener, Metadata headers) { - startTask.setResult(Pair.create(responseListener, headers)); - System.out.println(">>> start"); - } - - @Override - public void request(int numMessages) { - System.out.println(">>> request"); - } - - @Override - public void cancel(@Nullable String message, @Nullable Throwable cause) { - cancelTask.setResult(Pair.create(message, cause)); - System.out.println(">>> cancel"); - } - - @Override - public void halfClose() { - System.out.println(">>> halfClose"); - } - - @Override - public void sendMessage(ReqT message) { - System.out.println(">>> sendMessage"); - final TaskCompletionSource sourceTask; - synchronized (sent) { - sentIndex++; - if (sent.size() > sentIndex) { - sourceTask = sent.get(sentIndex); - } else { - sourceTask = new TaskCompletionSource<>(); - sent.add(sourceTask); - } - } - sourceTask.setResult(message); - } - - public Task, Metadata>> getStart() { - return startTask.getTask(); - } - - public Task> getCancel() { - return cancelTask.getTask(); - } - - public Task getSent(int index) { - synchronized (sent) { - while (sent.size() <= index) { - sent.add(new TaskCompletionSource<>()); - } - return sent.get(index).getTask(); - } - } -} From 2262fdcdb4c6ddb21bafed3b075d6b0b18cb0be2 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 12 Jun 2024 16:11:14 -0400 Subject: [PATCH 25/30] Simplify --- .../firestore/core/ComponentProvider.java | 9 +- .../FirebaseFirestoreTestFactory.java | 65 ++--------- .../integration/AsyncTaskAccumulator.java | 106 ++++++++++++++++++ .../integration/FirebaseFirestoreTest.java | 9 +- .../firestore/integration/TestClientCall.java | 33 ++---- .../integration/TestEventListener.java | 74 +----------- 6 files changed, 139 insertions(+), 157 deletions(-) create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/integration/AsyncTaskAccumulator.java diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java index 07cc8593726..2319097c831 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/ComponentProvider.java @@ -21,6 +21,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + import com.google.firebase.firestore.FirebaseFirestoreSettings; import com.google.firebase.firestore.auth.CredentialsProvider; import com.google.firebase.firestore.auth.User; @@ -51,7 +53,6 @@ public abstract class ComponentProvider { private EventManager eventManager; @Nullable private IndexBackfiller indexBackfiller; @Nullable private Scheduler garbageCollectionScheduler; - private AsyncQueue asyncQueue; @NonNull public static ComponentProvider defaultFactory(@NonNull FirebaseFirestoreSettings settings) { @@ -98,15 +99,12 @@ public Configuration( } } + @VisibleForTesting public void setRemoteProvider(RemoteComponenetProvider remoteProvider) { hardAssert(remoteStore == null, "cannot set remoteProvider after initialize"); this.remoteProvider = remoteProvider; } - public AsyncQueue getAsyncQueue() { - return hardAssertNonNull(asyncQueue, "asyncQueue not initialized yet"); - } - public RemoteSerializer getRemoteSerializer() { return remoteProvider.getRemoteSerializer(); } @@ -159,7 +157,6 @@ public void initialize(Configuration configuration) { * *

    To catch incorrect order, all getX methods have runtime check for null. */ - asyncQueue = configuration.asyncQueue; remoteProvider.initialize(configuration); persistence = createPersistence(configuration); persistence.start(); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java index 27fbe5007b1..e84eaf39d9d 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java @@ -24,6 +24,7 @@ import com.google.android.gms.tasks.TaskCompletionSource; import com.google.android.gms.tasks.Tasks; import com.google.firebase.firestore.core.ComponentProvider; +import com.google.firebase.firestore.integration.AsyncTaskAccumulator; import com.google.firebase.firestore.model.DatabaseId; import com.google.firebase.firestore.remote.GrpcCallProvider; import com.google.firebase.firestore.remote.RemoteComponenetProvider; @@ -36,63 +37,35 @@ import com.google.firestore.v1.WriteRequest; import com.google.firestore.v1.WriteResponse; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.Executor; -import io.grpc.ClientCall; import com.google.firebase.firestore.integration.TestClientCall; public final class FirebaseFirestoreTestFactory { public final DatabaseId databaseId; - - private TaskCompletionSource nextInstance; - public final List> instances = new ArrayList<>(); + public final AsyncTaskAccumulator instances = new AsyncTaskAccumulator<>(); public static class Instance { public ComponentProvider componentProvider; public GrpcCallProvider mockGrpcCallProvider; - private TaskCompletionSource> nextListen; - public final List>> listens = new ArrayList<>(); - private TaskCompletionSource> nextWrite; - public final List>> writes = new ArrayList<>(); - - private final TaskCompletionSource initializeComplete; - public final Task initializeCompleteTask; + private final AsyncTaskAccumulator> listens = new AsyncTaskAccumulator<>(); + private final AsyncTaskAccumulator> writes = new AsyncTaskAccumulator<>(); public Task enqueue(Runnable runnable) { return configuration.asyncQueue.enqueue(runnable); } public ComponentProvider.Configuration configuration; - public Instance() { - nextListen = new TaskCompletionSource<>(); - listens.add(nextListen.getTask()); - nextWrite = new TaskCompletionSource<>(); - writes.add(nextWrite.getTask()); - initializeComplete = new TaskCompletionSource<>(); - initializeCompleteTask = initializeComplete.getTask(); + public Task> getListenClient(int i) { + return listens.get(i); } - private Task> createListenCallback() { - synchronized (listens) { - TestClientCall mock = new TestClientCall<>(nextListen); - nextListen = new TaskCompletionSource<>(); - listens.add(nextListen.getTask()); - return Tasks.forResult(mock); - } + public Task> getWriteClient(int i) { + return writes.get(i); } - private Task> createWriteCallback() { - synchronized (writes) { - TestClientCall mock = new TestClientCall<>(nextWrite); - nextWrite = new TaskCompletionSource<>(); - writes.add(nextWrite.getTask()); - return Tasks.forResult(mock); - } - } } public final FirebaseFirestore firestore; @@ -100,7 +73,6 @@ private Task> createWriteCallback() { public FirebaseFirestoreTestFactory() { databaseId = DatabaseId.forDatabase("p", "d"); - prepareInstances(); firestore = new FirebaseFirestore( ApplicationProvider.getApplicationContext(), databaseId, @@ -121,41 +93,28 @@ public void setClearPersistenceMethod(Function> clearPersis firestore.clearPersistenceMethod = clearPersistenceMethod; } - private void prepareInstances() { - nextInstance = new TaskCompletionSource<>(); - instances.add(nextInstance.getTask()); - } - private GrpcCallProvider mockGrpcCallProvider(Instance instance) { GrpcCallProvider mockGrpcCallProvider = mock(GrpcCallProvider.class); when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getListenMethod()))) - .thenAnswer(invocation -> instance.createListenCallback()); + .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.listens.next()))); when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getWriteMethod()))) - .thenAnswer(invocation -> instance.createWriteCallback()); + .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.writes.next()))); instance.mockGrpcCallProvider = mockGrpcCallProvider; return mockGrpcCallProvider; } private ComponentProvider componentProvider(FirebaseFirestoreSettings settings) { + TaskCompletionSource next = instances.next(); Instance instance = new Instance(); instance.componentProvider = ComponentProvider.defaultFactory(settings); instance.componentProvider.setRemoteProvider(new RemoteComponenetProvider() { -// @Override -// protected Datastore createDatastore(ComponentProvider.Configuration configuration) { -// instance.configuration = configuration; -// return mockDatastore(instance); -// } -// @Override protected GrpcCallProvider createGrpcCallProvider(ComponentProvider.Configuration configuration) { instance.configuration = configuration; - configuration.asyncQueue.enqueueAndForget(() -> instance.initializeComplete.setResult(null)); + next.setResult(instance); return mockGrpcCallProvider(instance); } }); - TaskCompletionSource nextInstance = this.nextInstance; - prepareInstances(); - nextInstance.setResult(instance); return instance.componentProvider; } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/AsyncTaskAccumulator.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/AsyncTaskAccumulator.java new file mode 100644 index 00000000000..5675e58149f --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/AsyncTaskAccumulator.java @@ -0,0 +1,106 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package com.google.firebase.firestore.integration; + +import androidx.annotation.NonNull; + +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.TaskCompletionSource; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + + +public class AsyncTaskAccumulator implements Iterable> { + + private int eventCount; + private List> events; + + public AsyncTaskAccumulator() { + eventCount = 0; + events = new ArrayList<>(); + } + + public synchronized TaskCompletionSource next() { + return computeIfAbsentIndex(eventCount++); + } + + public void onResult(T result) { + next().setResult(result); + } + + public void onException(Exception e) { + next().setException(e); + } + + private TaskCompletionSource computeIfAbsentIndex(int i) { + while (events.size() <= i) { + events.add(new TaskCompletionSource<>()); + } + return events.get(i); + } + + /** + * Get task that completes when result arrives. + * + * @param index 0 indexed arrival sequence of results. + * @return Task. + */ + @NonNull + public synchronized Task get(int index) { + return computeIfAbsentIndex(index).getTask(); + } + + /** + * Iterates over results. + *

    + * The Iterator is thread safe. + * Iteration will stop upon task that is failed, cancelled or incomplete. + *

    + * A loop that waits for task to complete before getting next task will continue to iterate + * indefinitely. Attempting to iterate past a task that is not yet successful will throw + * {#code NoSuchElementException} and {@code #hasNext()} will be false. In this way, iteration + * in nonblocking. Last element will be failed, cancelled or awaiting result. + * + * @return Iterator of Tasks that complete. + */ + @NonNull + @Override + public Iterator> iterator() { + return new Iterator>() { + + private int i = -1; + private Task current; + + @Override + public synchronized boolean hasNext() { + // We always return first, and continue to return tasks so long as previous + // is successful. A task that hasn't completed, will also mark the end of + // iteration. + return i < 0 || current.isSuccessful(); + } + + @Override + public synchronized Task next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + i++; + return current = get(i); + } + }; + } +} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java index 18ab2acffc5..c349bb906ae 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java @@ -119,7 +119,6 @@ public void clearPersistanceAfterStartupShouldRestartFirestoreClient() throws Ex firestore.collection("col"); FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); - waitForSuccess(first.initializeCompleteTask); AsyncQueue firstAsyncQueue = first.configuration.asyncQueue; @@ -167,7 +166,7 @@ public void clearPersistenceDueToInitResponse() throws Exception { FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); // Wait for Listen CallClient to be created. - TestClientCall callback1 = waitForResult(first.listens.get(0)); + TestClientCall callback1 = waitForResult(first.getListenClient(0)); // Wait for ListenRequest handshake. // We expect an empty init request because the database is fresh. @@ -184,7 +183,7 @@ public void clearPersistenceDueToInitResponse() throws Exception { waitForSuccess(first.enqueue(() -> callback1.listener.onClose(Status.NOT_FOUND, new Metadata()))); // We expect client to reconnect Listen stream. - TestClientCall callback2 = waitForResult(first.listens.get(1)); + TestClientCall callback2 = waitForResult(first.getListenClient(1)); // Wait for ListenRequest. // We expect FirestoreClient to send InitRequest with previous token. @@ -218,7 +217,7 @@ public void clearPersistenceDueToInitResponse() throws Exception { FirebaseFirestoreTestFactory.Instance second = waitForResult(factory.instances.get(1)); // Wait for Listen CallClient to be created. - TestClientCall callback3 = waitForResult(second.listens.get(0)); + TestClientCall callback3 = waitForResult(second.getListenClient(0)); // Wait for ListenRequest. // We expect FirestoreClient to send InitRequest with previous token. @@ -240,7 +239,7 @@ public void preserveWritesWhenDisconnectedWithNotFound() throws Exception { FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); // Wait for Listen CallClient to be created. - TestClientCall callback1 = waitForResult(first.writes.get(0)); + TestClientCall callback1 = waitForResult(first.getWriteClient(0)); // Wait for WriteRequest handshake. // We expect an empty init request because the database is fresh. diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java index 38eb8d96eb2..310a58f4c84 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java @@ -21,23 +21,20 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; -import java.util.ArrayList; -import java.util.List; - import io.grpc.ClientCall; import io.grpc.Metadata; /** - * Construct a ClientCall test harness. + * ClientCall test harness. */ public class TestClientCall extends ClientCall { public Metadata headers; public Listener listener; - private int sentIndex = -1; - private List> requests = new ArrayList<>(); - private TaskCompletionSource> startTask = new TaskCompletionSource<>(); - private TaskCompletionSource> cancelTask = new TaskCompletionSource<>(); + + private final AsyncTaskAccumulator requests; + private final TaskCompletionSource> startTask; + private final TaskCompletionSource> cancelTask = new TaskCompletionSource<>(); /** * Construct a ClientCall test harness. @@ -49,6 +46,7 @@ public class TestClientCall extends ClientCall { */ public TestClientCall(TaskCompletionSource> startTask) { this.startTask = startTask; + this.requests = new AsyncTaskAccumulator<>(); } @Override @@ -73,25 +71,10 @@ public void halfClose() { @Override public void sendMessage(ReqT message) { - final TaskCompletionSource sourceTask; - synchronized (requests) { - sentIndex++; - if (requests.size() > sentIndex) { - sourceTask = requests.get(sentIndex); - } else { - sourceTask = new TaskCompletionSource<>(); - requests.add(sourceTask); - } - } - sourceTask.setResult(message); + requests.onResult(message); } public Task getRequest(int index) { - synchronized (requests) { - while (requests.size() <= index) { - requests.add(new TaskCompletionSource<>()); - } - return requests.get(index).getTask(); - } + return requests.get(index); } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java index ae5f3a970bf..01a3b604bd9 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java @@ -18,96 +18,34 @@ import androidx.annotation.Nullable; import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.FirebaseFirestoreException; -import java.util.ArrayList; import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; /** * EventListener test harness. */ -class TestEventListener implements EventListener, Iterable> { +class TestEventListenerimplements EventListener { - private int eventCount = 0; - private TaskCompletionSource next; - private List> events = new ArrayList<>(); - - public TestEventListener() { - next = computeIfAbsentIndex(eventCount); - } - - private TaskCompletionSource computeIfAbsentIndex(int i) { - while (events.size() <= i) { - events.add(new TaskCompletionSource<>()); - } - return events.get(i); - } + AsyncTaskAccumulator events = new AsyncTaskAccumulator<>(); @Override public synchronized void onEvent(@Nullable T value, @Nullable FirebaseFirestoreException error) { if (error == null) { - next.setResult(value); + events.onResult(value); } else { - next.setException(error); + events.onException(error); } - eventCount++; - next = computeIfAbsentIndex(eventCount); } - /** - * Get result from {@code #onEvent}. Task that that completes with event result. - * @param index 0 indexed sequence of {@code #onEvent} invocations. - * @return Task that completes when {@code #onEvent} is called. - */ @NonNull public synchronized Task get(int index) { - return computeIfAbsentIndex(index).getTask(); + return events.get(index); } - /** - * Iterates over {@code #onEvent}. - * - * This will return the same as calling {@code #get(0..n)}. - * The Iterator is thread safe. - * Iteration will stop upon event task that fails or cancels. - * - * A loop that waits for event task to complete before getting next event task will continue to - * iterate indefinitely. The next event task is not available until previous task is successful. - * Attempting to iterate past an event task that is not yet successful will throw - * {#code NoSuchElementException} and {@code #hasNext()} will be false. In this way, iteration - * can complete without blocking for already received events. - * - * @return Iterator of Tasks that complete when {@code #onEvent} is called. - */ @NonNull - @Override public Iterator> iterator() { - return new Iterator>() { - - private int i = -1; - private Task current; - - @Override - public synchronized boolean hasNext() { - // We always return first, and continue to return tasks so long as previous - // is successful. A task that hasn't completed, will also mark the end of - // iteration. - return i < 0 || current.isSuccessful(); - } - - @Override - public synchronized Task next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - i++; - current = get(i); - return current; - } - }; + return events.iterator(); } } From 2c678e3f4a7c74c2d790e4c788734a2da61f0347 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Thu, 13 Jun 2024 18:03:03 -0400 Subject: [PATCH 26/30] Add tests --- .../integration/FirebaseFirestoreTest.java | 293 ++++++++++++++++-- .../firestore/integration/TestClientCall.java | 7 + 2 files changed, 280 insertions(+), 20 deletions(-) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java index c349bb906ae..2fd7ed68c89 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java @@ -33,16 +33,26 @@ import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.FirebaseFirestoreTestFactory; import com.google.firebase.firestore.QuerySnapshot; +import com.google.firebase.firestore.UserDataReader; +import com.google.firebase.firestore.core.UserData; import com.google.firebase.firestore.model.DatabaseId; +import com.google.firebase.firestore.model.DocumentKey; +import com.google.firebase.firestore.model.mutation.FieldTransform; +import com.google.firebase.firestore.model.mutation.Precondition; +import com.google.firebase.firestore.model.mutation.SetMutation; +import com.google.firebase.firestore.remote.RemoteSerializer; import com.google.firebase.firestore.util.AsyncQueue; import com.google.firestore.v1.InitRequest; import com.google.firestore.v1.InitResponse; import com.google.firestore.v1.ListenRequest; import com.google.firestore.v1.ListenResponse; +import com.google.firestore.v1.Write; import com.google.firestore.v1.WriteRequest; import com.google.firestore.v1.WriteResponse; +import com.google.firestore.v1.WriteResult; import com.google.protobuf.ByteString; +import org.bouncycastle.util.Arrays; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -51,11 +61,15 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; +import io.grpc.ClientCall; import io.grpc.Metadata; import io.grpc.Status; @@ -75,7 +89,9 @@ private static T waitForResult(Task task) throws InterruptedException { } private static Exception waitForException(Task task) throws InterruptedException { - return waitFor(task).getException(); + Exception exception = waitFor(task).getException(); + assertThat(exception).isNotNull(); + return exception; } @NonNull @@ -89,7 +105,7 @@ private static Task waitFor(Task task) throws InterruptedException { task.addOnSuccessListener(BACKGROUND_EXECUTOR, t -> countDownLatch.countDown()); task.addOnFailureListener(BACKGROUND_EXECUTOR, e -> countDownLatch.countDown()); task.addOnCanceledListener(BACKGROUND_EXECUTOR, () -> countDownLatch.countDown()); - countDownLatch.await(900, TimeUnit.SECONDS); + countDownLatch.await(15, TimeUnit.SECONDS); return task; } @@ -157,10 +173,8 @@ public void clearPersistenceDueToInitResponse() throws Exception { // Create a snapshot listener that will be active during handshake clearing of cache. TestEventListener snapshotListener1 = new TestEventListener<>(); firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener1); - Iterator> snapshots1 = snapshotListener1.iterator(); - - // First snapshot will be from cache. - assertTrue(waitForResult(snapshots1.next()).getMetadata().isFromCache()); + Iterator> snapshots = snapshotListener1.iterator(); + Task firstSnapshot = snapshots.next(); // Wait for first FirestoreClient to instantiate FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); @@ -179,9 +193,16 @@ public void clearPersistenceDueToInitResponse() throws Exception { // We expect previous addSnapshotListener to cause a, AddTarget request. assertTrue(waitForResult(callback1.getRequest(1)).hasAddTarget()); + // TODO(does this make sense?) + // We have a 10 second timeout on raising snapshot from Cache, that is triggered when Listen connection is closed. + assertFalse(firstSnapshot.isComplete()); + // Simulate Database deletion by closing connection with NOT_FOUND. waitForSuccess(first.enqueue(() -> callback1.listener.onClose(Status.NOT_FOUND, new Metadata()))); + // First snapshot is raised from cache immediately after connection is closed. + assertTrue(waitForResult(firstSnapshot).getMetadata().isFromCache()); + // We expect client to reconnect Listen stream. TestClientCall callback2 = waitForResult(first.getListenClient(1)); @@ -190,7 +211,6 @@ public void clearPersistenceDueToInitResponse() throws Exception { assertThat(waitForResult(callback2.getRequest(0))) .isEqualTo(listenRequest(initRequest("token1"))); - // This task will complete when clearPersistence is invoked on FirebaseFirestore. Task clearPersistenceTask = setupClearPersistenceTask(); @@ -206,14 +226,14 @@ public void clearPersistenceDueToInitResponse() throws Exception { verify(first.mockGrpcCallProvider, times(1)).shutdown(); // Snapshot listeners should fail with ABORTED - FirebaseFirestoreException exception = waitForException(snapshots1.next(), FirebaseFirestoreException.class); + FirebaseFirestoreException exception = waitForException(snapshots.next(), FirebaseFirestoreException.class); assertThat(exception.getCode()).isEqualTo(FirebaseFirestoreException.Code.ABORTED); // Start another snapshot listener TestEventListener snapshotListener2 = new TestEventListener<>(); firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener2); - // Wait for first FirestoreClient to instantiate + // Wait for second FirestoreClient to instantiate FirebaseFirestoreTestFactory.Instance second = waitForResult(factory.instances.get(1)); // Wait for Listen CallClient to be created. @@ -235,24 +255,226 @@ public void preserveWritesWhenDisconnectedWithNotFound() throws Exception { doc2.set(map("foo", "B")); doc3.set(map("foo", "C")); - // Wait for first FirestoreClient to instantiate - FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); + // 1st FirestoreClient instance. + { + // Wait for first FirestoreClient to instantiate + FirebaseFirestoreTestFactory.Instance instance = waitForResult(factory.instances.get(0)); + RemoteSerializer serializer = instance.componentProvider.getRemoteSerializer(); + + // First Write stream connection + { + // Wait for Write CallClient to be created. + TestClientCall callback = waitForResult(instance.getWriteClient(0)); + Iterator> requests = callback.requestIterator(); + + // Wait for WriteRequest handshake. + // We expect an empty init request because the database is fresh. + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(InitRequest.getDefaultInstance())); + + // Simulate a successful InitResponse from server. + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token1"))))); + + // Expect first write request. + Write write1 = serializer.encodeMutation(setMutation(doc1, map("foo", "A"))); + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(write1)); + + // Simulate write acknowledgement. + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); + + // Expect second write request. + Write write2 = serializer.encodeMutation(setMutation(doc2, map("foo", "B"))); + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(write2)); + + // Simulate NOT_FOUND error that was NOT due to database name reuse. ( + waitForSuccess(instance.enqueue(() -> callback.listener.onClose(Status.NOT_FOUND, new Metadata()))); + } + + // Second Write Stream connection + // Previous connection was closed by server with NOT_FOUND error. + { + // Wait for Write CallClient to be created. + TestClientCall callback = waitForResult(instance.getWriteClient(1)); + Iterator> requests = callback.requestIterator(); + + // Wait for WriteRequest handshake. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(initRequest("token1"))); + + // Simulate a successful InitResponse from server. + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token2"))))); + + // Expect second write to be retried. + Write write2 = serializer.encodeMutation(setMutation(doc2, map("foo", "B"))); + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(write2)); + + // Simulate write acknowledgement. + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); + + // Simulate NOT_FOUND error. This time we will clear cache. + waitForSuccess(instance.enqueue(() -> callback.listener.onClose(Status.NOT_FOUND, new Metadata()))); + } + + + // Third Write Stream connection + // Previous connection was closed by server with NOT_FOUND error. + { + // Wait for Write CallClient to be created. + TestClientCall callback = waitForResult(instance.getWriteClient(2)); + Iterator> requests = callback.requestIterator(); + + // Wait for WriteRequest. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(initRequest("token2"))); + + // Simulate a clear cache InitResponse from server. + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token3", true))))); + } + } + + // Interaction with 2nd FirestoreClient instance. + // Previous instance was shutdown due to clear cache command from server. + { + // Wait for second FirestoreClient to instantiate + FirebaseFirestoreTestFactory.Instance instance = waitForResult(factory.instances.get(1)); + RemoteSerializer serializer = instance.componentProvider.getRemoteSerializer(); + + // The writes should have been cleared, so we will have to create a new one. + DocumentReference doc4 = col.document(); + doc4.set(map("foo", "D")); + + // Wait for Write CallClient to be created. + TestClientCall callback = waitForResult(instance.getWriteClient(0)); + Iterator> requests = callback.requestIterator(); + + // Wait for WriteRequest. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(initRequest("token3"))); + + // Simulate a successful InitResponse from server. + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token4"))))); + + // Expect the new write request. + Write write4 = serializer.encodeMutation(setMutation(doc4, map("foo", "D"))); + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(write4)); + + // Simulate write acknowledgement. + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); + } + } - // Wait for Listen CallClient to be created. - TestClientCall callback1 = waitForResult(first.getWriteClient(0)); + @Test + public void listenHandshakeMustWaitForWriteHandshakeToComplete() throws Exception { + CollectionReference col = firestore.collection("col"); - // Wait for WriteRequest handshake. - // We expect an empty init request because the database is fresh. - assertThat(waitForResult(callback1.getRequest(0))) + // Wait for FirestoreClient to instantiate + FirebaseFirestoreTestFactory.Instance instance = waitForResult(factory.instances.get(0)); + + // Trigger Write Stream First + col.document().set(map("foo", "A")); + + TestClientCall write = waitForResult(instance.getWriteClient(0)); + ClientCall.Listener writeResponses = write.listener; + Iterator> writeRequests = write.requestIterator(); + + // Then Trigger Listen Stream; + TestEventListener snapshotListener = new TestEventListener<>(); + firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener); + Iterator> snapshots = snapshotListener.iterator(); + + TestClientCall listen = waitForResult(instance.getListenClient(0)); + Iterator> listenRequests = listen.requestIterator(); + ClientCall.Listener listenResponses = listen.listener; + + // Prepare + Task writeInitRequest = writeRequests.next(); + Task listenInitRequest = listenRequests.next(); + + // Expect empty InitRequest from Write stream. + assertThat(waitForResult(writeInitRequest)) .isEqualTo(writeRequest(InitRequest.getDefaultInstance())); + + // No request should have come from Listen stream yet. + assertFalse(listenInitRequest.isComplete()); + + // Simulate a successful InitResponse from server. + waitForSuccess(instance.enqueue(() -> writeResponses.onMessage(writeResponse(initResponse("token1"))))); + + // Now that Write handshake is complete, the Listen stream should send a InitRequest with token from Write handshake. + assertThat(waitForResult(listenInitRequest)) + .isEqualTo(listenRequest(initRequest("token1"))); } - private static ListenResponse listenResponse(InitResponse initResponse) { - return ListenResponse.newBuilder() - .setInitResponse(initResponse) - .build(); + @Test + public void writeHandshakeMustWaitForListenHandshakeToComplete() throws Exception { + CollectionReference col = firestore.collection("col"); + + // Wait for FirestoreClient to instantiate + FirebaseFirestoreTestFactory.Instance instance = waitForResult(factory.instances.get(0)); + + // Trigger Listen Stream First + TestEventListener snapshotListener = new TestEventListener<>(); + firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener); + Iterator> snapshots = snapshotListener.iterator(); + + TestClientCall listen = waitForResult(instance.getListenClient(0)); + Iterator> listenRequests = listen.requestIterator(); + ClientCall.Listener listenResponses = listen.listener; + + // Then Trigger Write Stream; + col.document().set(map("foo", "A")); + + TestClientCall write = waitForResult(instance.getWriteClient(0)); + ClientCall.Listener writeResponses = write.listener; + Iterator> writeRequests = write.requestIterator(); + + // Prepare + Task writeInitRequest = writeRequests.next(); + Task listenInitRequest = listenRequests.next(); + + // Expect empty InitRequest from Listen stream. + assertThat(waitForResult(listenInitRequest)) + .isEqualTo(listenRequest(InitRequest.getDefaultInstance())); + + // No request should have come from Listen stream yet. + assertFalse(writeInitRequest.isComplete()); + + // Simulate a successful InitResponse from server. + waitForSuccess(instance.enqueue(() -> listenResponses.onMessage(listenResponse(initResponse("token1"))))); + + // Now that Write handshake is complete, the Listen stream should send a InitRequest with token from Write handshake. + assertThat(waitForResult(writeInitRequest)) + .isEqualTo(writeRequest(initRequest("token1"))); + + } + + @NonNull + private DocumentKey key(DocumentReference doc) { + return DocumentKey.fromPathString(doc.getPath()); + } + + @NonNull + public SetMutation setMutation(DocumentReference doc, Map values) { + UserDataReader dataReader = new UserDataReader(factory.databaseId); + UserData.ParsedSetData parsed = dataReader.parseSetData(values); + + // The order of the transforms doesn't matter, but we sort them so tests can assume a particular + // order. + ArrayList fieldTransforms = new ArrayList<>(parsed.getFieldTransforms()); + Collections.sort( + fieldTransforms, (ft1, ft2) -> ft1.getFieldPath().compareTo(ft2.getFieldPath())); + + return new SetMutation(key(doc), parsed.getData(), Precondition.NONE, fieldTransforms); } + @NonNull private ListenRequest listenRequest(InitRequest initRequest) { return ListenRequest.newBuilder() .setDatabase(getResourcePrefixValue(factory.databaseId)) @@ -260,6 +482,14 @@ private ListenRequest listenRequest(InitRequest initRequest) { .build(); } + @NonNull + private static ListenResponse listenResponse(InitResponse initResponse) { + return ListenResponse.newBuilder() + .setInitResponse(initResponse) + .build(); + } + + @NonNull private WriteRequest writeRequest(InitRequest initRequest) { return WriteRequest.newBuilder() .setDatabase(getResourcePrefixValue(factory.databaseId)) @@ -267,12 +497,34 @@ private WriteRequest writeRequest(InitRequest initRequest) { .build(); } + @NonNull + private WriteRequest writeRequest(Write... writes) { + return WriteRequest.newBuilder() + .addAllWrites(() -> new Arrays.Iterator<>(writes)) + .build(); + } + + @NonNull + private static WriteResponse writeResponse(InitResponse initResponse) { + return WriteResponse.newBuilder() + .setInitResponse(initResponse) + .build(); + } + + @NonNull + private static WriteResponse writeResponse(WriteResult... writeResults) { + return WriteResponse.newBuilder() + .addAllWriteResults(() -> new Arrays.Iterator<>(writeResults)) + .build(); + } + @NonNull private static InitResponse initResponse(String token) { return InitResponse.newBuilder() .setSessionToken(ByteString.copyFromUtf8(token)) .build(); } + @NonNull private static InitResponse initResponse(String token, boolean clearCache) { return InitResponse.newBuilder() .setSessionToken(ByteString.copyFromUtf8(token)) @@ -280,6 +532,7 @@ private static InitResponse initResponse(String token, boolean clearCache) { .build(); } + @NonNull private static InitRequest initRequest(String token) { return InitRequest.newBuilder() .setSessionToken(ByteString.copyFromUtf8(token)) diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java index 310a58f4c84..78e0399b568 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestClientCall.java @@ -21,6 +21,8 @@ import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; +import java.util.Iterator; + import io.grpc.ClientCall; import io.grpc.Metadata; @@ -67,6 +69,7 @@ public void cancel(@Nullable String message, @Nullable Throwable cause) { @Override public void halfClose() { + requests.onException(new RuntimeException("halfClose")); } @Override @@ -77,4 +80,8 @@ public void sendMessage(ReqT message) { public Task getRequest(int index) { return requests.get(index); } + + public Iterator> requestIterator() { + return requests.iterator(); + } } From 7c049f08d5162b3b102dabc936d54536488cde4f Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 17 Jun 2024 12:15:28 -0400 Subject: [PATCH 27/30] Fix after merge --- .../firestore/remote/RemoteStoreTest.java | 17 +-- .../testutil/IntegrationTestUtil.java | 8 +- .../firestore/core/FirestoreClient.java | 16 +-- .../core/MemoryComponentProvider.java | 5 +- .../firebase/firestore/remote/Datastore.java | 1 + .../firestore/remote/FirestoreChannel.java | 17 ++- .../remote/RemoteComponenetProvider.java | 8 +- .../firestore/remote/RemoteStore.java | 15 +-- .../firebase/firestore/util/AsyncQueue.java | 1 + .../firestore/core/EventManagerTest.java | 105 ------------------ .../firestore/spec/MemorySpecTest.java | 9 +- .../firestore/spec/SQLiteSpecTest.java | 4 +- .../firebase/firestore/spec/SpecTestCase.java | 6 +- 13 files changed, 51 insertions(+), 161 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java index 3d1fe8be163..851b503c980 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/RemoteStoreTest.java @@ -41,16 +41,8 @@ public class RemoteStoreTest { @Test public void testRemoteStoreStreamStopsWhenNetworkUnreachable() { AsyncQueue testQueue = new AsyncQueue(); - DatabaseInfo databaseInfo = IntegrationTestUtil.testEnvDatabaseInfo(); - RemoteSerializer serializer = new RemoteSerializer(databaseInfo.getDatabaseId()); - FirestoreChannel channel = new FirestoreChannel( - testQueue, - ApplicationProvider.getApplicationContext(), - null, - null, - databaseInfo, - null); - Datastore datastore = new Datastore(testQueue, serializer, channel); + RemoteSerializer serializer = new RemoteSerializer(IntegrationTestUtil.testEnvDatabaseId()); + Datastore datastore = new Datastore(testQueue, serializer, null); Semaphore networkChangeSemaphore = new Semaphore(0); RemoteStore.RemoteStoreCallback callback = new RemoteStore.RemoteStoreCallback() { @@ -78,12 +70,11 @@ public ImmutableSortedSet getRemoteKeysForTarget(int targetId) { }; FakeConnectivityMonitor connectivityMonitor = new FakeConnectivityMonitor(); - QueryEngine queryEngine = new QueryEngine(); Persistence persistence = MemoryPersistence.createEagerGcMemoryPersistence(); persistence.start(); - LocalStore localStore = new LocalStore(persistence, queryEngine, User.UNAUTHENTICATED); + LocalStore localStore = new LocalStore(persistence, new QueryEngine(), User.UNAUTHENTICATED); RemoteStore remoteStore = - new RemoteStore(databaseInfo.getDatabaseId(), callback, localStore, datastore, testQueue, connectivityMonitor); + new RemoteStore(IntegrationTestUtil.testEnvDatabaseId(), callback, localStore, datastore, testQueue, connectivityMonitor); waitFor(testQueue.enqueue(remoteStore::forceEnableNetwork)); drain(testQueue); diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java index 455534e308d..94584e90e5c 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/testutil/IntegrationTestUtil.java @@ -170,14 +170,20 @@ public static TargetBackend getTargetBackend() { } } + @NonNull public static DatabaseInfo testEnvDatabaseInfo() { return new DatabaseInfo( - DatabaseId.forProject(provider.projectId()), + testEnvDatabaseId(), "test-persistenceKey", getFirestoreHost(), getSslEnabled()); } + @NonNull + public static DatabaseId testEnvDatabaseId() { + return DatabaseId.forProject(provider.projectId()); + } + public static FirebaseFirestoreSettings newTestSettings() { Logger.debug("IntegrationTestUtil", "target backend is: %s", backend.name()); FirebaseFirestoreSettings.Builder settings = new FirebaseFirestoreSettings.Builder(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index 1eaf05a9379..5542c620e12 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -86,14 +86,14 @@ public final class FirestoreClient { @Nullable private Scheduler gcScheduler; public FirestoreClient( - final Context context, - DatabaseInfo databaseInfo, - FirebaseFirestoreSettings settings, - CredentialsProvider authProvider, - CredentialsProvider appCheckProvider, - AsyncQueue asyncQueue, - @Nullable GrpcMetadataProvider metadataProvider, - ComponentProvider componentProvider) { + final Context context, + DatabaseInfo databaseInfo, + FirebaseFirestoreSettings settings, + CredentialsProvider authProvider, + CredentialsProvider appCheckProvider, + AsyncQueue asyncQueue, + @Nullable GrpcMetadataProvider metadataProvider, + ComponentProvider componentProvider) { this.databaseInfo = databaseInfo; this.authProvider = authProvider; this.appCheckProvider = appCheckProvider; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java index 4e19674d0e6..6781fad65de 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/MemoryComponentProvider.java @@ -29,10 +29,8 @@ import com.google.firebase.firestore.local.Scheduler; import com.google.firebase.firestore.model.DocumentKey; import com.google.firebase.firestore.model.mutation.MutationBatchResult; -import com.google.firebase.firestore.remote.RemoteComponenetProvider; import com.google.firebase.firestore.remote.RemoteEvent; import com.google.firebase.firestore.remote.RemoteStore; - import io.grpc.Status; /** @@ -76,8 +74,7 @@ private boolean isMemoryLruGcEnabled(FirebaseFirestoreSettings settings) { @Override protected Persistence createPersistence(Configuration configuration) { if (isMemoryLruGcEnabled(configuration.settings)) { - LocalSerializer serializer = - new LocalSerializer(getRemoteSerializer()); + LocalSerializer serializer = new LocalSerializer(getRemoteSerializer()); LruGarbageCollector.Params params = LruGarbageCollector.Params.WithCacheSizeBytes( configuration.settings.getCacheSizeBytes()); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java index ab6fcd2b157..b9f22ed74cb 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/Datastore.java @@ -88,6 +88,7 @@ public class Datastore { protected final RemoteSerializer serializer; private final AsyncQueue workerQueue; + private final FirestoreChannel channel; Datastore(AsyncQueue workerQueue, RemoteSerializer serializer, FirestoreChannel channel) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java index 9c41969b9a4..51b2ea26a66 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/FirestoreChannel.java @@ -17,7 +17,6 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import android.content.Context; - import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.firestore.BuildConfig; @@ -108,15 +107,15 @@ public void onClose(Status status) {} "projects/%s/databases/%s", databaseId.getProjectId(), databaseId.getDatabaseId()); } - private static GrpcCallProvider getGrpcCallProvider( - AsyncQueue asyncQueue, Context context, CredentialsProvider authProvider, - CredentialsProvider appCheckProvider, DatabaseInfo databaseInfo) { - FirestoreCallCredentials firestoreHeaders = - new FirestoreCallCredentials(authProvider, appCheckProvider); - return new GrpcCallProvider(asyncQueue, context, databaseInfo, firestoreHeaders); - } + private static GrpcCallProvider getGrpcCallProvider( + AsyncQueue asyncQueue, Context context, CredentialsProvider authProvider, + CredentialsProvider appCheckProvider, DatabaseInfo databaseInfo) { + FirestoreCallCredentials firestoreHeaders = + new FirestoreCallCredentials(authProvider, appCheckProvider); + return new GrpcCallProvider(asyncQueue, context, databaseInfo, firestoreHeaders); + } - /** + /** * Shuts down the grpc channel. This is not reversible and renders the FirestoreChannel unusable. */ public void shutdown() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java index 3048532a169..c99ad9b2dd6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteComponenetProvider.java @@ -17,8 +17,14 @@ import static com.google.firebase.firestore.util.Assert.hardAssertNonNull; import com.google.firebase.firestore.core.ComponentProvider; -import com.google.firebase.firestore.util.AsyncQueue; +/** + * Initializes and wires up remote components for Firestore. + * + *

    Implementations provide custom components by overriding the `createX()` methods. + *

    The RemoteComponentProvider is located in the same package as the components in order to have + * package-private access to the components. + */ public class RemoteComponenetProvider { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index 75d0a4fde13..8f85eb0767b 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -67,7 +67,9 @@ public class RemoteStore implements WatchChangeAggregator.TargetMetadataProvider /** The log tag to use for this class. */ private static final String LOG_TAG = "RemoteStore"; private Consumer clearPersistenceCallback; - private final DatabaseId datastoreId; + + /** The database ID of the Firestore instance. */ + private final DatabaseId databaseId; /** A callback interface for events from RemoteStore. */ public interface RemoteStoreCallback { @@ -115,7 +117,7 @@ public interface RemoteStoreCallback { *

    Returns an empty set of document keys for unknown targets. */ ImmutableSortedSet getRemoteKeysForTarget(int targetId); -} + } private final RemoteStoreCallback remoteStoreCallback; private final LocalStore localStore; @@ -163,7 +165,7 @@ public RemoteStore( Datastore datastore, AsyncQueue workerQueue, ConnectivityMonitor connectivityMonitor) { - this.datastoreId = databaseId; + this.databaseId = databaseId; this.remoteStoreCallback = remoteStoreCallback; this.localStore = localStore; this.datastore = datastore; @@ -469,7 +471,7 @@ private void startWatchStream() { hardAssert( shouldStartWatchStream(), "startWatchStream() called when shouldStartWatchStream() is false."); - watchChangeAggregator = new WatchChangeAggregator(this); + watchChangeAggregator = new WatchChangeAggregator(databaseId, this); watchStream.start(); onlineStateTracker.handleWatchStreamStart(); @@ -812,11 +814,6 @@ public TargetData getTargetDataForTarget(int targetId) { return this.listenTargets.get(targetId); } - @Override - public DatabaseId getDatabaseId() { - return this.datastoreId; - } - public Task> runAggregateQuery( Query query, List aggregateFields) { if (canUseNetwork()) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java index 678e607efb6..93101f54c45 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java @@ -197,6 +197,7 @@ public static Task callTask(Executor executor, Callable clearPersistenceCallback = spy(new Consumer() { -// @Override -// public void accept(ByteString value) { -// -// } -// }); -// EventListener eventListener1 = mock(EventListener.class); -// EventListener eventListener2 = mock(EventListener.class); -// -// QueryListener listener1 = new QueryListener(query, new ListenOptions(), eventListener1); -// QueryListener listener2 = new QueryListener(query, new ListenOptions(), eventListener2); -// -// SyncEngine syncEngine; -// EventManager eventManager; -// RemoteStore remoteStore = mockRemoteStore(); -// LocalStore localStore = createLruGcMemoryLocalStore(); -// syncEngine = spy(new SyncEngine(localStore, remoteStore, User.UNAUTHENTICATED, 100, clearPersistenceCallback)); -// eventManager = new EventManager(syncEngine); -// eventManager.abortAllTargets(); -// -// eventManager.addQueryListener(listener1); -// eventManager.addQueryListener(listener2); -// -// syncEngine.handleClearPersistence(ByteString.copyFromUtf8("sessionToken")); -// -// verify(syncEngine, times(1)) -// .listen( -// query, -// /** shouldListenToRemote= */ -// true); -// -// ArgumentMatcher abortedExceptionMatcher = e -> e.getCode() == Code.ABORTED; -// verify(eventListener1, times(1)) -// .onEvent(isNull(), argThat(abortedExceptionMatcher)); -// -// verify(eventListener2, times(1)) -// .onEvent(isNull(), argThat(abortedExceptionMatcher)); -// -// verify(clearPersistenceCallback, times(1)).accept(ByteString.copyFromUtf8("sessionToken")); -// -// verify(remoteStore, times(1)).listen(any(TargetData.class)); -// verify(remoteStore, atLeastOnce()).canUseNetwork(); -// verify(remoteStore, times(1)).disableNetwork(); -// verify(remoteStore, times(1)).enableNetwork(); -// verifyNoMoreInteractions(remoteStore); -// } -// -// @NonNull -// private static RemoteStore mockRemoteStore() { -// AtomicBoolean online = new AtomicBoolean(true); -// RemoteStore remoteStore = mock(RemoteStore.class); -// when(remoteStore.canUseNetwork()).thenAnswer(invocation -> online.get()); -// doAnswer((Answer) invocation -> { -// online.set(true); -// return null; -// }).when(remoteStore).enableNetwork(); -// doAnswer((Answer) invocation -> { -// online.set(false); -// return null; -// }).when(remoteStore).disableNetwork(); -// return remoteStore; -// } -// -// @NonNull -// private static LocalStore createLruGcMemoryLocalStore() { -// DatabaseId databaseId = DatabaseId.forProject("projectId"); -// LocalSerializer serializer = new LocalSerializer(new RemoteSerializer(databaseId)); -// Persistence persistence = MemoryPersistence.createLruGcMemoryPersistence( -// LruGarbageCollector.Params.Default(), serializer); -// persistence.start(); -// return new LocalStore(persistence, new QueryEngine(), User.UNAUTHENTICATED); -// } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/MemorySpecTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/MemorySpecTest.java index 71de6c389c6..75fe3f4503f 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/MemorySpecTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/MemorySpecTest.java @@ -20,8 +20,6 @@ import com.google.firebase.firestore.local.LruGarbageCollector; import com.google.firebase.firestore.local.MemoryPersistence; import com.google.firebase.firestore.local.Persistence; -import com.google.firebase.firestore.remote.Datastore; -import com.google.firebase.firestore.remote.MockDatastore; import com.google.firebase.firestore.remote.RemoteComponenetProvider; import java.util.Set; import org.junit.runner.RunWith; @@ -55,12 +53,7 @@ protected Persistence createPersistence(Configuration configuration) { } } }; - provider.setRemoteProvider(new RemoteComponenetProvider() { - @Override - protected Datastore createDatastore(ComponentProvider.Configuration configuration) { - return new MockDatastore(configuration.databaseInfo, configuration.asyncQueue); - } - }); + provider.setRemoteProvider(remoteProvider); provider.initialize(configuration); return provider; } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SQLiteSpecTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SQLiteSpecTest.java index dea563fcd38..969774cb33c 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SQLiteSpecTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SQLiteSpecTest.java @@ -37,7 +37,9 @@ protected void specSetUp(JSONObject config) { @Override protected SQLiteComponentProvider initializeComponentProvider( - RemoteComponenetProvider remoteProvider, ComponentProvider.Configuration configuration, boolean garbageCollectionEnabled) { + RemoteComponenetProvider remoteProvider, + ComponentProvider.Configuration configuration, + boolean garbageCollectionEnabled) { SQLiteComponentProvider provider = new SQLiteComponentProvider(); provider.setRemoteProvider(remoteProvider); provider.initialize(configuration); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index 3b231948e4a..0fab592252a 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -263,7 +263,9 @@ public static void log(String line) { // protected abstract ComponentProvider initializeComponentProvider( - RemoteComponenetProvider remoteProvider, ComponentProvider.Configuration configuration, boolean garbageCollectionEnabled); + RemoteComponenetProvider remoteProvider, + ComponentProvider.Configuration configuration, + boolean garbageCollectionEnabled); private boolean shouldRun(Set tags) { for (String tag : tags) { @@ -331,7 +333,7 @@ private void initClient() { new FirebaseFirestoreSettings.Builder().build(), new EmptyCredentialsProvider(), new EmptyAppCheckTokenProvider(), - null + null ); RemoteComponenetProvider remoteProvider = new RemoteComponenetProvider() { From 92c73ee9cbb49921519bd96a876298d5087c82da Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 17 Jun 2024 16:36:05 -0400 Subject: [PATCH 28/30] fix after merge --- ...rebaseFirestoreIntegrationTestFactory.java | 18 ++- .../FirebaseFirestoreTestFactory.java | 120 ------------------ .../integration/FirebaseFirestoreTest.java | 29 ++--- 3 files changed, 28 insertions(+), 139 deletions(-) delete mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreIntegrationTestFactory.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreIntegrationTestFactory.java index 91836cce99d..cc694fe117c 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreIntegrationTestFactory.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreIntegrationTestFactory.java @@ -31,6 +31,7 @@ import com.google.firebase.firestore.testutil.EmptyAppCheckTokenProvider; import com.google.firebase.firestore.testutil.EmptyCredentialsProvider; import com.google.firebase.firestore.util.AsyncQueue; +import com.google.firebase.firestore.util.Function; import com.google.firestore.v1.FirestoreGrpc; import com.google.firestore.v1.ListenRequest; import com.google.firestore.v1.ListenResponse; @@ -39,6 +40,8 @@ import com.google.firebase.firestore.integration.TestClientCall; +import java.util.concurrent.Executor; + /** * Factory for producing FirebaseFirestore instances that has mocked gRPC layer. * @@ -72,6 +75,9 @@ public static class Instance { /** Every write stream created is captured here. */ private final AsyncTaskAccumulator> writes = new AsyncTaskAccumulator<>(); + /** Mockito mock of GrpcCallProvider. */ + public GrpcCallProvider mockGrpcCallProvider; + private Instance(ComponentProvider componentProvider) { this.componentProvider = componentProvider; } @@ -92,7 +98,7 @@ public Task enqueue(Runnable runnable) { * `FirebaseFirestoreIntegrationTestFactory` will set `Instance.configuration` from within * the ComponentProvider override. */ - private ComponentProvider.Configuration configuration; + public ComponentProvider.Configuration configuration; /** Every listen stream created */ public Task> getListenClient(int i) { @@ -122,9 +128,8 @@ public FirebaseFirestoreIntegrationTestFactory(DatabaseId databaseId) { ApplicationProvider.getApplicationContext(), databaseId, "k", - new EmptyCredentialsProvider(), - new EmptyAppCheckTokenProvider(), - new AsyncQueue(), + EmptyCredentialsProvider::new, + EmptyAppCheckTokenProvider::new, this::componentProvider, null, instanceRegistry, @@ -138,12 +143,17 @@ public void useMemoryCache() { firestore.setFirestoreSettings(builder.build()); } + public void setClearPersistenceMethod(Function> clearPersistenceMethod) { + firestore.clearPersistenceMethod = clearPersistenceMethod; + } + private GrpcCallProvider mockGrpcCallProvider(Instance instance) { GrpcCallProvider mockGrpcCallProvider = mock(GrpcCallProvider.class); when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getListenMethod()))) .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.listens.next()))); when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getWriteMethod()))) .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.writes.next()))); + instance.mockGrpcCallProvider = mockGrpcCallProvider; return mockGrpcCallProvider; } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java deleted file mode 100644 index e84eaf39d9d..00000000000 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreTestFactory.java +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.firestore; - -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import androidx.test.core.app.ApplicationProvider; - -import com.google.android.gms.tasks.Task; -import com.google.android.gms.tasks.TaskCompletionSource; -import com.google.android.gms.tasks.Tasks; -import com.google.firebase.firestore.core.ComponentProvider; -import com.google.firebase.firestore.integration.AsyncTaskAccumulator; -import com.google.firebase.firestore.model.DatabaseId; -import com.google.firebase.firestore.remote.GrpcCallProvider; -import com.google.firebase.firestore.remote.RemoteComponenetProvider; -import com.google.firebase.firestore.testutil.EmptyAppCheckTokenProvider; -import com.google.firebase.firestore.testutil.EmptyCredentialsProvider; -import com.google.firebase.firestore.util.Function; -import com.google.firestore.v1.FirestoreGrpc; -import com.google.firestore.v1.ListenRequest; -import com.google.firestore.v1.ListenResponse; -import com.google.firestore.v1.WriteRequest; -import com.google.firestore.v1.WriteResponse; - -import java.util.concurrent.Executor; - -import com.google.firebase.firestore.integration.TestClientCall; - -public final class FirebaseFirestoreTestFactory { - - public final DatabaseId databaseId; - public final AsyncTaskAccumulator instances = new AsyncTaskAccumulator<>(); - - public static class Instance { - public ComponentProvider componentProvider; - public GrpcCallProvider mockGrpcCallProvider; - - private final AsyncTaskAccumulator> listens = new AsyncTaskAccumulator<>(); - private final AsyncTaskAccumulator> writes = new AsyncTaskAccumulator<>(); - public Task enqueue(Runnable runnable) { - return configuration.asyncQueue.enqueue(runnable); - } - - public ComponentProvider.Configuration configuration; - - public Task> getListenClient(int i) { - return listens.get(i); - } - - public Task> getWriteClient(int i) { - return writes.get(i); - } - - } - - public final FirebaseFirestore firestore; - public final FirebaseFirestore.InstanceRegistry instanceRegistry = mock(FirebaseFirestore.InstanceRegistry.class); - - public FirebaseFirestoreTestFactory() { - databaseId = DatabaseId.forDatabase("p", "d"); - firestore = new FirebaseFirestore( - ApplicationProvider.getApplicationContext(), - databaseId, - "k", - EmptyCredentialsProvider::new, - EmptyAppCheckTokenProvider::new, - this::componentProvider, - null, - instanceRegistry, - null - ); - FirebaseFirestoreSettings.Builder builder = new FirebaseFirestoreSettings.Builder(firestore.getFirestoreSettings()); - builder.setLocalCacheSettings(MemoryCacheSettings.newBuilder().build()); - firestore.setFirestoreSettings(builder.build()); - } - - public void setClearPersistenceMethod(Function> clearPersistenceMethod) { - firestore.clearPersistenceMethod = clearPersistenceMethod; - } - - private GrpcCallProvider mockGrpcCallProvider(Instance instance) { - GrpcCallProvider mockGrpcCallProvider = mock(GrpcCallProvider.class); - when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getListenMethod()))) - .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.listens.next()))); - when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getWriteMethod()))) - .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.writes.next()))); - instance.mockGrpcCallProvider = mockGrpcCallProvider; - return mockGrpcCallProvider; - } - - private ComponentProvider componentProvider(FirebaseFirestoreSettings settings) { - TaskCompletionSource next = instances.next(); - Instance instance = new Instance(); - instance.componentProvider = ComponentProvider.defaultFactory(settings); - instance.componentProvider.setRemoteProvider(new RemoteComponenetProvider() { - @Override - protected GrpcCallProvider createGrpcCallProvider(ComponentProvider.Configuration configuration) { - instance.configuration = configuration; - next.setResult(instance); - return mockGrpcCallProvider(instance); - } - }); - return instance.componentProvider; - } -} diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java index 40501906aa7..5059b2f5a2b 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java @@ -32,7 +32,6 @@ import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.FirebaseFirestoreIntegrationTestFactory; -import com.google.firebase.firestore.FirebaseFirestoreTestFactory; import com.google.firebase.firestore.QuerySnapshot; import com.google.firebase.firestore.UserDataReader; import com.google.firebase.firestore.core.UserData; @@ -156,10 +155,10 @@ public void preserveWritesWhenDisconnectedWithInternalError() throws Exception { // Wait for WriteRequest handshake. // We expect an empty init request because the database is fresh. - assertThat(waitForResult(requests.next())).isEqualTo(writeRequestHandshake()); + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(InitRequest.getDefaultInstance())); // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse()))); + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token1"))))); // Expect first write request. Write write1 = serializer.encodeMutation(setMutation(doc1, map("foo", "A"))); @@ -185,10 +184,10 @@ public void preserveWritesWhenDisconnectedWithInternalError() throws Exception { // Wait for WriteRequest handshake. // We expect FirestoreClient to send InitRequest with previous token. - assertThat(waitForResult(requests.next())).isEqualTo(writeRequestHandshake()); + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(initRequest("token1"))); // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse()))); + waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token2"))))); // Expect second write to be retried. Write write2 = serializer.encodeMutation(setMutation(doc2, map("foo", "B"))); @@ -210,7 +209,7 @@ public void clearPersistanceAfterStartupShouldRestartFirestoreClient() throws Ex // Trigger instantiation of FirestoreClient firestore.collection("col"); - FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); + FirebaseFirestoreIntegrationTestFactory.Instance first = waitForResult(factory.instances.get(0)); AsyncQueue firstAsyncQueue = first.configuration.asyncQueue; @@ -220,7 +219,7 @@ public void clearPersistanceAfterStartupShouldRestartFirestoreClient() throws Ex waitForSuccess(firestore.clearPersistence()); // Now we have a history of 2 instances. - FirebaseFirestoreTestFactory.Instance second = waitForResult(factory.instances.get(1)); + FirebaseFirestoreIntegrationTestFactory.Instance second = waitForResult(factory.instances.get(1)); AsyncQueue secondAsyncQueue = second.configuration.asyncQueue; assertEquals(firstAsyncQueue.getExecutor(), secondAsyncQueue.getExecutor()); @@ -253,7 +252,7 @@ public void clearPersistenceDueToInitResponse() throws Exception { Task firstSnapshot = snapshots.next(); // Wait for first FirestoreClient to instantiate - FirebaseFirestoreTestFactory.Instance first = waitForResult(factory.instances.get(0)); + FirebaseFirestoreIntegrationTestFactory.Instance first = waitForResult(factory.instances.get(0)); // Wait for Listen CallClient to be created. TestClientCall callback1 = waitForResult(first.getListenClient(0)); @@ -310,7 +309,7 @@ public void clearPersistenceDueToInitResponse() throws Exception { firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener2); // Wait for second FirestoreClient to instantiate - FirebaseFirestoreTestFactory.Instance second = waitForResult(factory.instances.get(1)); + FirebaseFirestoreIntegrationTestFactory.Instance second = waitForResult(factory.instances.get(1)); // Wait for Listen CallClient to be created. TestClientCall callback3 = waitForResult(second.getListenClient(0)); @@ -334,7 +333,7 @@ public void preserveWritesWhenDisconnectedWithNotFound() throws Exception { // 1st FirestoreClient instance. { // Wait for first FirestoreClient to instantiate - FirebaseFirestoreTestFactory.Instance instance = waitForResult(factory.instances.get(0)); + FirebaseFirestoreIntegrationTestFactory.Instance instance = waitForResult(factory.instances.get(0)); RemoteSerializer serializer = instance.componentProvider.getRemoteSerializer(); // First Write stream connection @@ -417,7 +416,7 @@ public void preserveWritesWhenDisconnectedWithNotFound() throws Exception { // Previous instance was shutdown due to clear cache command from server. { // Wait for second FirestoreClient to instantiate - FirebaseFirestoreTestFactory.Instance instance = waitForResult(factory.instances.get(1)); + FirebaseFirestoreIntegrationTestFactory.Instance instance = waitForResult(factory.instances.get(1)); RemoteSerializer serializer = instance.componentProvider.getRemoteSerializer(); // The writes should have been cleared, so we will have to create a new one. @@ -451,7 +450,7 @@ public void listenHandshakeMustWaitForWriteHandshakeToComplete() throws Exceptio CollectionReference col = firestore.collection("col"); // Wait for FirestoreClient to instantiate - FirebaseFirestoreTestFactory.Instance instance = waitForResult(factory.instances.get(0)); + FirebaseFirestoreIntegrationTestFactory.Instance instance = waitForResult(factory.instances.get(0)); // Trigger Write Stream First col.document().set(map("foo", "A")); @@ -493,7 +492,7 @@ public void writeHandshakeMustWaitForListenHandshakeToComplete() throws Exceptio CollectionReference col = firestore.collection("col"); // Wait for FirestoreClient to instantiate - FirebaseFirestoreTestFactory.Instance instance = waitForResult(factory.instances.get(0)); + FirebaseFirestoreIntegrationTestFactory.Instance instance = waitForResult(factory.instances.get(0)); // Trigger Listen Stream First TestEventListener snapshotListener = new TestEventListener<>(); @@ -552,7 +551,7 @@ public SetMutation setMutation(DocumentReference doc, Map values @NonNull private ListenRequest listenRequest(InitRequest initRequest) { return ListenRequest.newBuilder() - .setDatabase(getResourcePrefixValue(factory.databaseId)) + .setDatabase(getResourcePrefixValue(databaseId)) .setInitRequest(initRequest) .build(); } @@ -567,7 +566,7 @@ private static ListenResponse listenResponse(InitResponse initResponse) { @NonNull private WriteRequest writeRequest(InitRequest initRequest) { return WriteRequest.newBuilder() - .setDatabase(getResourcePrefixValue(factory.databaseId)) + .setDatabase(getResourcePrefixValue(databaseId)) .setInitRequest(initRequest) .build(); } From e61b1ed87f6ff8a8b26ea7b2f8b45460b93e1f3d Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Wed, 19 Jun 2024 12:37:43 -0400 Subject: [PATCH 29/30] Fix after merge --- .../java/com/google/firebase/firestore/FirebaseFirestore.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 165e725ab11..db8aa1ca831 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -347,8 +347,6 @@ private FirestoreClient newClient(AsyncQueue asyncQueue) { } return client; - - return client; } } From f68934249ee106170b6d617121ca26690bc8eef0 Mon Sep 17 00:00:00 2001 From: Tom Andersen Date: Mon, 29 Jul 2024 15:26:52 -0400 Subject: [PATCH 30/30] Fix after merge --- .../firestore/CompositeIndexQueryTest.java | 1 - .../FirestoreClientProviderTest.java | 7 +- .../firebase/firestore/ValidationTest.java | 1 - .../firebase/firestore/remote/StreamTest.java | 5 +- .../firebase/firestore/FirebaseFirestore.java | 56 +- .../firestore/FirestoreClientProvider.java | 17 +- .../firebase/firestore/core/EventManager.java | 4 +- .../firestore/core/FirestoreClient.java | 4 +- .../firebase/firestore/core/SyncEngine.java | 3 - .../firestore/local/SQLitePersistence.java | 2 +- .../firestore/remote/RemoteStore.java | 1 + .../firestore/remote/WatchStream.java | 3 +- .../firestore/remote/WriteStream.java | 5 +- .../firebase/firestore/util/AsyncQueue.java | 2 - .../SynchronizedShutdownAwareExecutor.java | 415 +++++----- ...rebaseFirestoreIntegrationTestFactory.java | 45 +- .../integration/FirebaseFirestoreTest.java | 718 +++++++++--------- .../integration/TestEventListener.java | 36 +- .../firestore/remote/MockDatastore.java | 7 +- .../firebase/firestore/spec/SpecTestCase.java | 6 +- 20 files changed, 681 insertions(+), 657 deletions(-) diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java index 80a2e5c23f1..aa9be3bcf01 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/CompositeIndexQueryTest.java @@ -39,7 +39,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.firebase.firestore.Query.Direction; -import com.google.firebase.firestore.core.FirestoreClient; import com.google.firebase.firestore.testutil.CompositeIndexTestHelper; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import java.util.Map; diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreClientProviderTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreClientProviderTest.java index 9a55bc5931b..afac6732375 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreClientProviderTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/FirestoreClientProviderTest.java @@ -1,12 +1,11 @@ package com.google.firebase.firestore; -import org.junit.runner.RunWith; - import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class FirestoreClientProviderTest { - //TODO(requires backend/emulator support) + // TODO(requires backend/emulator support) -} \ No newline at end of file +} diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java index 07df2823209..0fd261fefe3 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/ValidationTest.java @@ -45,7 +45,6 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.firestore.Transaction.Function; -import com.google.firebase.firestore.core.FirestoreClient; import com.google.firebase.firestore.testutil.IntegrationTestUtil; import com.google.firebase.firestore.util.Consumer; import java.util.Arrays; diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java index 7770dc6774a..7c24fc3b01a 100644 --- a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/remote/StreamTest.java @@ -40,14 +40,12 @@ import com.google.firebase.firestore.util.AsyncQueue.TimerId; import com.google.firestore.v1.InitResponse; import com.google.protobuf.ByteString; - import io.grpc.Status; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; - import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; @@ -76,8 +74,7 @@ public List observedStates() { @RunWith(AndroidJUnit4.class) public class StreamTest { - @Rule - public Timeout timeout = new Timeout(10, TimeUnit.SECONDS); + @Rule public Timeout timeout = new Timeout(10, TimeUnit.SECONDS); /** Single mutation to send to the write stream. */ private static final List mutations = diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java index 5b93c24713f..ff26f7af047 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirebaseFirestore.java @@ -59,7 +59,6 @@ import com.google.firebase.firestore.util.Preconditions; import com.google.firebase.inject.Deferred; import com.google.protobuf.ByteString; - import java.io.ByteArrayInputStream; import java.io.InputStream; import java.nio.ByteBuffer; @@ -113,8 +112,7 @@ public interface InstanceRegistry { final FirestoreClientProvider clientProvider; private final GrpcMetadataProvider metadataProvider; - @VisibleForTesting - Function> clearPersistenceMethod; + @VisibleForTesting Function> clearPersistenceMethod; @Nullable private PersistentCacheIndexManager persistentCacheIndexManager; @@ -251,18 +249,20 @@ static FirebaseFirestore newInstance( this.settings = new FirebaseFirestoreSettings.Builder().build(); - this.clearPersistenceMethod = executor -> { - final TaskCompletionSource source = new TaskCompletionSource<>(); - executor.execute(() -> { - try { - SQLitePersistence.clearPersistence(context, databaseId, persistenceKey); - source.setResult(null); - } catch (FirebaseFirestoreException e) { - source.setException(e); - } - }); - return source.getTask(); - }; + this.clearPersistenceMethod = + executor -> { + final TaskCompletionSource source = new TaskCompletionSource<>(); + executor.execute( + () -> { + try { + SQLitePersistence.clearPersistence(context, databaseId, persistenceKey); + source.setResult(null); + } catch (FirebaseFirestoreException e) { + source.setException(e); + } + }); + return source.getTask(); + }; } /** Returns the settings used by this {@code FirebaseFirestore} object. */ @@ -316,26 +316,26 @@ public void useEmulator(@NonNull String host, int port) { private FirestoreClient newClient(AsyncQueue asyncQueue) { synchronized (clientProvider) { DatabaseInfo databaseInfo = - new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled()); + new DatabaseInfo(databaseId, persistenceKey, settings.getHost(), settings.isSslEnabled()); - FirestoreClient client = new FirestoreClient( + FirestoreClient client = + new FirestoreClient( context, databaseInfo, - settings, authProviderFactory.get(), appCheckTokenProviderFactory.get(), asyncQueue, metadataProvider, - componentProviderFactory.apply(settings) - ); - - client.setClearPersistenceCallback(sessionToken -> { - synchronized (clientProvider) { - if (client.isTerminated()) return; - this.sessionToken = sessionToken; - clearPersistence(); - } - }); + componentProviderFactory.apply(settings)); + + client.setClearPersistenceCallback( + sessionToken -> { + synchronized (clientProvider) { + if (client.isTerminated()) return; + this.sessionToken = sessionToken; + clearPersistence(); + } + }); // Session token must be set before we enable network, since it is part of stream handshake. if (sessionToken != null) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java index 3eaccc9dcbe..4c3037eaad2 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/FirestoreClientProvider.java @@ -120,24 +120,23 @@ synchronized void procedure(Consumer call) { */ synchronized T executeIfShutdown( Function callIf, Function callElse) { - Executor executor = command -> asyncQueue.enqueueAndForgetEvenAfterShutdown(command); if (client == null || client.isTerminated()) { - return callIf.apply(executor); + return callIf.apply(asyncQueue.getExecutor()); } else { - return callElse.apply(executor); + return callElse.apply(asyncQueue.getExecutor()); } } synchronized T executeWhileShutdown(Function call) { // This will block asyncQueue, prevent a new client from being started. if (client == null || client.isTerminated()) { - return call.apply(asyncQueue.getExecutor()); + return call.apply(asyncQueue.getExecutor()); } else { - client.shutdown(); - asyncQueue = asyncQueue.reincarnate(); - T result = call.apply(asyncQueue.getExecutor()); - client = clientFactory.apply(asyncQueue); - return result; + client.shutdown(); + asyncQueue = asyncQueue.reincarnate(); + T result = call.apply(asyncQueue.getExecutor()); + client = clientFactory.apply(asyncQueue); + return result; } } diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java index dea399b82a1..f8819a86781 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/EventManager.java @@ -17,6 +17,7 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import com.google.firebase.firestore.EventListener; +import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.ListenSource; import com.google.firebase.firestore.core.SyncEngine.SyncEngineCallback; import com.google.firebase.firestore.util.Util; @@ -267,9 +268,10 @@ public void handleOnlineStateChange(OnlineState onlineState) { } public void abortAllTargets() { + FirebaseFirestoreException error = Util.exceptionFromStatus(Status.ABORTED); for (QueryListenersInfo info : queries.values()) { for (QueryListener listener : info.listeners) { - listener.onError(Util.exceptionFromStatus(Status.ABORTED)); + listener.onError(error); } } queries.clear(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java index a2619ae876e..4ee4ef5c806 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/FirestoreClient.java @@ -52,7 +52,6 @@ import com.google.firebase.firestore.util.Logger; import com.google.firestore.v1.Value; import com.google.protobuf.ByteString; - import java.io.InputStream; import java.util.List; import java.util.Map; @@ -140,7 +139,8 @@ public FirestoreClient( public void setClearPersistenceCallback(Consumer clearPersistenceCallback) { this.verifyNotTerminated(); - asyncQueue.enqueueAndForget(() -> remoteStore.setClearPersistenceCallback(clearPersistenceCallback)); + asyncQueue.enqueueAndForget( + () -> remoteStore.setClearPersistenceCallback(clearPersistenceCallback)); } private void onAsyncQueueShutdown() { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java index 1abf77e35f8..9c51418fae7 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/core/SyncEngine.java @@ -19,15 +19,12 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; import com.google.firebase.database.collection.ImmutableSortedMap; import com.google.firebase.database.collection.ImmutableSortedSet; -import com.google.firebase.firestore.AggregateField; import com.google.firebase.firestore.FirebaseFirestoreException; import com.google.firebase.firestore.LoadBundleTask; import com.google.firebase.firestore.LoadBundleTaskProgress; -import com.google.firebase.firestore.TransactionOptions; import com.google.firebase.firestore.auth.User; import com.google.firebase.firestore.bundle.BundleElement; import com.google.firebase.firestore.bundle.BundleLoader; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java index 77d7bc9ed78..f222f8f6236 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/local/SQLitePersistence.java @@ -242,7 +242,7 @@ T runTransaction(String action, Supplier operation) { public static void clearPersistence(Context context, DatabaseId databaseId, String persistenceKey) throws FirebaseFirestoreException { - //TODO Could we change this with SQLiteDatabase.deleteDatabase(). + // TODO Could we change this with SQLiteDatabase.deleteDatabase(). String databaseName = SQLitePersistence.databaseName(persistenceKey, databaseId); String sqLitePath = context.getDatabasePath(databaseName).getPath(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java index 4fcfd67bf86..9a775b3f55e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/RemoteStore.java @@ -66,6 +66,7 @@ public class RemoteStore implements WatchChangeAggregator.TargetMetadataProvider /** The log tag to use for this class. */ private static final String LOG_TAG = "RemoteStore"; + private Consumer clearPersistenceCallback; /** The database ID of the Firestore instance. */ diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java index 3b3ba0c7520..868055757ed 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WatchStream.java @@ -90,7 +90,8 @@ void sendHandshake(ByteString sessionToken) { InitRequest.Builder initRequest = InitRequest.newBuilder(); if (sessionToken != null) initRequest.setSessionToken(sessionToken); - ListenRequest.Builder request = ListenRequest.newBuilder() + ListenRequest.Builder request = + ListenRequest.newBuilder() .setDatabase(serializer.databaseName()) .setInitRequest(initRequest); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java index 02b40ac42a5..94b00d0bea6 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/remote/WriteStream.java @@ -146,7 +146,8 @@ void sendHandshake(ByteString sessionToken) { InitRequest.Builder initRequest = InitRequest.newBuilder(); if (sessionToken != null) initRequest.setSessionToken(sessionToken); - WriteRequest.Builder request = WriteRequest.newBuilder() + WriteRequest.Builder request = + WriteRequest.newBuilder() .setDatabase(serializer.databaseName()) .setInitRequest(initRequest); @@ -173,7 +174,7 @@ void writeMutations(List mutations) { @Override public void onFirst(WriteResponse response) { - hardAssert(response.hasInitResponse(),"InitResponse expected as part of Handshake response"); + hardAssert(response.hasInitResponse(), "InitResponse expected as part of Handshake response"); lastStreamToken = response.getStreamToken(); // The first response is the handshake response diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java index 53fd946c513..71307945d03 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/AsyncQueue.java @@ -14,13 +14,11 @@ package com.google.firebase.firestore.util; -import static com.google.firebase.firestore.util.Assert.fail; import static com.google.firebase.firestore.util.Assert.hardAssert; import android.annotation.SuppressLint; import android.os.Handler; import android.os.Looper; - import androidx.annotation.VisibleForTesting; import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java index 320c8ec9557..39a78cc5e0e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/SynchronizedShutdownAwareExecutor.java @@ -18,12 +18,9 @@ import static com.google.firebase.firestore.util.Assert.hardAssert; import android.annotation.SuppressLint; - import androidx.annotation.NonNull; - import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.TaskCompletionSource; - import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CountDownLatch; @@ -50,220 +47,224 @@ * */ class SynchronizedShutdownAwareExecutor implements Executor { - public static final String ASYNC_QUEUE_IS_SHUTDOWN = "AsyncQueue is shutdown"; - /** - * The single threaded executor that is backing this Executor. This is also the executor used - * when some tasks explicitly request to run after shutdown has been initiated. - */ - final ScheduledThreadPoolExecutor internalExecutor; - - /** - * Task ss assigned when the shutdown process has been initiated, once it is started, it is not revertable. - */ - private Task shutdownTask; - - private Runnable onShutdown = null; - - /** - * The single thread that will be used by the executor. This is created early and managed - * directly so that it's possible later to make assertions about executing on the correct - * thread. - */ - private final Thread thread; - - /** - * A ThreadFactory for a single, pre-created thread. - */ - private class DelayedStartFactory implements Runnable, ThreadFactory { - private final CountDownLatch latch = new CountDownLatch(1); - private Runnable delegate; - - @Override - public void run() { - try { - latch.await(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - delegate.run(); - } - - @Override - public Thread newThread(@NonNull Runnable runnable) { - hardAssert(delegate == null, "Only one thread may be created in an AsyncQueue."); - delegate = runnable; - latch.countDown(); - return thread; - } - } + public static final String ASYNC_QUEUE_IS_SHUTDOWN = "AsyncQueue is shutdown"; + + /** + * The single threaded executor that is backing this Executor. This is also the executor used + * when some tasks explicitly request to run after shutdown has been initiated. + */ + final ScheduledThreadPoolExecutor internalExecutor; + + /** + * Task ss assigned when the shutdown process has been initiated, once it is started, it is not revertable. + */ + private Task shutdownTask; + + private Runnable onShutdown = null; + + /** + * The single thread that will be used by the executor. This is created early and managed + * directly so that it's possible later to make assertions about executing on the correct + * thread. + */ + private final Thread thread; + + /** + * A ThreadFactory for a single, pre-created thread. + */ + private class DelayedStartFactory implements Runnable, ThreadFactory { + private final CountDownLatch latch = new CountDownLatch(1); + private Runnable delegate; - // TODO(b/258277574): Migrate to go/firebase-android-executors - @SuppressLint("ThreadPoolCreation") - public SynchronizedShutdownAwareExecutor() { - DelayedStartFactory threadFactory = new DelayedStartFactory(); - - thread = Executors.defaultThreadFactory().newThread(threadFactory); - thread.setName("FirestoreWorker"); - thread.setDaemon(true); - thread.setUncaughtExceptionHandler((crashingThread, throwable) -> { - shutdownNow(); - AsyncQueue.halt(throwable); - }); - - internalExecutor = - new ScheduledThreadPoolExecutor(1, threadFactory) { - @Override - protected void afterExecute(Runnable r, Throwable t) { - super.afterExecute(r, t); - if (t == null && r instanceof Future) { - Future future = (Future) r; - try { - // Not all Futures will be done, for example when used with scheduledAtFixedRate. - if (future.isDone()) { - future.get(); - } - } catch (CancellationException ce) { - // Cancellation exceptions are okay, we expect them to happen sometimes - } catch (ExecutionException ee) { - t = ee.getCause(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - if (t != null && !ASYNC_QUEUE_IS_SHUTDOWN.equals(t.getMessage())) { - shutdownNow(); - AsyncQueue.halt(t); - } - } - }; - - // Core threads don't time out, this only takes effect when we drop the number of required - // core threads - internalExecutor.setKeepAliveTime(3, TimeUnit.SECONDS); - } - - private SynchronizedShutdownAwareExecutor(ScheduledThreadPoolExecutor internalExecutor, Thread thread) { - this.internalExecutor = internalExecutor; - this.thread = thread; - } - - synchronized void verifyNotShutdown() { - if (shutdownTask != null) { - throw new RejectedExecutionException(ASYNC_QUEUE_IS_SHUTDOWN); - } - } - - void setOnShutdown(Runnable onShutdown) { - verifyNotShutdown(); - hardAssert(this.onShutdown == null, "setOnShutdown can only be called once."); - this.onShutdown = onShutdown; + @Override + public void run() { + try { + latch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + delegate.run(); } - /** - * Synchronized access to isShuttingDown - */ - synchronized boolean isShuttingDown() { - return shutdownTask != null; + @Override + public Thread newThread(@NonNull Runnable runnable) { + hardAssert(delegate == null, "Only one thread may be created in an AsyncQueue."); + delegate = runnable; + latch.countDown(); + return thread; } + } + + // TODO(b/258277574): Migrate to go/firebase-android-executors + @SuppressLint("ThreadPoolCreation") + public SynchronizedShutdownAwareExecutor() { + DelayedStartFactory threadFactory = new DelayedStartFactory(); + + thread = Executors.defaultThreadFactory().newThread(threadFactory); + thread.setName("FirestoreWorker"); + thread.setDaemon(true); + thread.setUncaughtExceptionHandler( + (crashingThread, throwable) -> { + shutdownNow(); + AsyncQueue.halt(throwable); + }); - /** - * Check if shutdown is initiated before scheduling. If it is initiated, the command will not be - * executed. - */ - - void verifyIsCurrentThread() { - Thread current = Thread.currentThread(); - if (thread != current) { - throw fail( - "We are running on the wrong thread. Expected to be on the AsyncQueue " - + "thread %s/%d but was %s/%d", - thread.getName(), thread.getId(), current.getName(), current.getId()); - } + internalExecutor = + new ScheduledThreadPoolExecutor(1, threadFactory) { + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + if (t == null && r instanceof Future) { + Future future = (Future) r; + try { + // Not all Futures will be done, for example when used with scheduledAtFixedRate. + if (future.isDone()) { + future.get(); + } + } catch (CancellationException ce) { + // Cancellation exceptions are okay, we expect them to happen sometimes + } catch (ExecutionException ee) { + t = ee.getCause(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + if (t != null && !ASYNC_QUEUE_IS_SHUTDOWN.equals(t.getMessage())) { + shutdownNow(); + AsyncQueue.halt(t); + } + } + }; + + // Core threads don't time out, this only takes effect when we drop the number of required + // core threads + internalExecutor.setKeepAliveTime(3, TimeUnit.SECONDS); + } + + private SynchronizedShutdownAwareExecutor( + ScheduledThreadPoolExecutor internalExecutor, Thread thread) { + this.internalExecutor = internalExecutor; + this.thread = thread; + } + + synchronized void verifyNotShutdown() { + if (shutdownTask != null) { + throw new RejectedExecutionException(ASYNC_QUEUE_IS_SHUTDOWN); } - - @Override - public synchronized void execute(Runnable command) { - verifyNotShutdown(); - internalExecutor.execute(command); + } + + void setOnShutdown(Runnable onShutdown) { + verifyNotShutdown(); + hardAssert(this.onShutdown == null, "setOnShutdown can only be called once."); + this.onShutdown = onShutdown; + } + + /** + * Synchronized access to isShuttingDown + */ + synchronized boolean isShuttingDown() { + return shutdownTask != null; + } + + /** + * Check if shutdown is initiated before scheduling. If it is initiated, the command will not be + * executed. + */ + void verifyIsCurrentThread() { + Thread current = Thread.currentThread(); + if (thread != current) { + throw fail( + "We are running on the wrong thread. Expected to be on the AsyncQueue " + + "thread %s/%d but was %s/%d", + thread.getName(), thread.getId(), current.getName(), current.getId()); } - - /** - * Run a given `Callable` on this executor, and report the result of the `Callable` in a {@link - * Task}. The `Callable` will not be run if the executor started shutting down already. - * - * @return A {@link Task} resolves when the requested `Callable` completes, or reports error - * when the `Callable` runs into exceptions. - */ - Task executeAndReportResult(Callable task) { - final TaskCompletionSource completionSource = new TaskCompletionSource<>(); - try { - this.execute( - () -> { - try { - completionSource.setResult(task.call()); - } catch (Exception e) { - completionSource.setException(e); - } - }); - } catch (RejectedExecutionException e) { - // The only way we can get here is if the AsyncQueue has panicked and we're now racing with - // the post to the main looper that will crash the app. - Logger.warn(AsyncQueue.class.getSimpleName(), "Refused to enqueue task after panic"); - completionSource.setException(e); - } - return completionSource.getTask(); + } + + @Override + public synchronized void execute(Runnable command) { + verifyNotShutdown(); + internalExecutor.execute(command); + } + + /** + * Run a given `Callable` on this executor, and report the result of the `Callable` in a {@link + * Task}. The `Callable` will not be run if the executor started shutting down already. + * + * @return A {@link Task} resolves when the requested `Callable` completes, or reports error + * when the `Callable` runs into exceptions. + */ + Task executeAndReportResult(Callable task) { + final TaskCompletionSource completionSource = new TaskCompletionSource<>(); + try { + this.execute( + () -> { + try { + completionSource.setResult(task.call()); + } catch (Exception e) { + completionSource.setException(e); + } + }); + } catch (RejectedExecutionException e) { + // The only way we can get here is if the AsyncQueue has panicked and we're now racing with + // the post to the main looper that will crash the app. + Logger.warn(AsyncQueue.class.getSimpleName(), "Refused to enqueue task after panic"); + completionSource.setException(e); } - - /** - * Initiate the shutdown process. Once called, the only possible way to run `Runnable`s are by - * holding the `internalExecutor` reference. - */ - synchronized Task shutdown() { - if (shutdownTask == null) { - shutdownTask = executeAndReportResult(() -> { + return completionSource.getTask(); + } + + /** + * Initiate the shutdown process. Once called, the only possible way to run `Runnable`s are by + * holding the `internalExecutor` reference. + */ + synchronized Task shutdown() { + if (shutdownTask == null) { + shutdownTask = + executeAndReportResult( + () -> { if (this.onShutdown != null) { - this.onShutdown.run(); + this.onShutdown.run(); } return null; - }); - } - return shutdownTask; - } - - /** - * Initiate the shutdown process and reduce thread pool to 0. - */ - synchronized void terminate() { - shutdown(); - - // Will cause the executor to de-reference all threads, the best we can do - internalExecutor.setCorePoolSize(0); - } - - - synchronized SynchronizedShutdownAwareExecutor reincarnate() { - hardAssert(isShuttingDown(), "Executor must be shutting down to be eligible for reincarnation."); - hardAssert(!isTerminated(), "Cannot reincarnate executor that is terminated."); - return new SynchronizedShutdownAwareExecutor(internalExecutor, thread); - } - - private boolean isTerminated() { - return internalExecutor.getCorePoolSize() == 0; - } - - /** - * Wraps {@link ScheduledThreadPoolExecutor#schedule(Runnable, long, TimeUnit)} and provides - * shutdown state check: the command will not be scheduled if the shutdown has been initiated. - */ - synchronized ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { - verifyNotShutdown(); - return internalExecutor.schedule(command, delay, unit); - } - - /** - * Wraps around {@link ScheduledThreadPoolExecutor#shutdownNow()}. - */ - void shutdownNow() { - internalExecutor.shutdownNow(); + }); } + return shutdownTask; + } + + /** + * Initiate the shutdown process and reduce thread pool to 0. + */ + synchronized void terminate() { + shutdown(); + + // Will cause the executor to de-reference all threads, the best we can do + internalExecutor.setCorePoolSize(0); + } + + synchronized SynchronizedShutdownAwareExecutor reincarnate() { + hardAssert( + isShuttingDown(), "Executor must be shutting down to be eligible for reincarnation."); + hardAssert(!isTerminated(), "Cannot reincarnate executor that is terminated."); + return new SynchronizedShutdownAwareExecutor(internalExecutor, thread); + } + + private boolean isTerminated() { + return internalExecutor.getCorePoolSize() == 0; + } + + /** + * Wraps {@link ScheduledThreadPoolExecutor#schedule(Runnable, long, TimeUnit)} and provides + * shutdown state check: the command will not be scheduled if the shutdown has been initiated. + */ + synchronized ScheduledFuture schedule(Runnable command, long delay, TimeUnit unit) { + verifyNotShutdown(); + return internalExecutor.schedule(command, delay, unit); + } + + /** + * Wraps around {@link ScheduledThreadPoolExecutor#shutdownNow()}. + */ + void shutdownNow() { + internalExecutor.shutdownNow(); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreIntegrationTestFactory.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreIntegrationTestFactory.java index d746f0a7476..c94c372c147 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreIntegrationTestFactory.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/FirebaseFirestoreIntegrationTestFactory.java @@ -30,16 +30,12 @@ import com.google.firebase.firestore.remote.RemoteComponenetProvider; import com.google.firebase.firestore.testutil.EmptyAppCheckTokenProvider; import com.google.firebase.firestore.testutil.EmptyCredentialsProvider; -import com.google.firebase.firestore.util.AsyncQueue; import com.google.firebase.firestore.util.Function; import com.google.firestore.v1.FirestoreGrpc; import com.google.firestore.v1.ListenRequest; import com.google.firestore.v1.ListenResponse; import com.google.firestore.v1.WriteRequest; import com.google.firestore.v1.WriteResponse; - -import com.google.firebase.firestore.integration.TestClientCall; - import java.util.concurrent.Executor; /** @@ -78,11 +74,11 @@ public static class Instance { new AsyncTaskAccumulator<>(); /** Mockito mock of GrpcCallProvider. */ - public GrpcCallProvider mockGrpcCallProvider; + public GrpcCallProvider mockGrpcCallProvider; - private Instance(ComponentProvider componentProvider) { - this.componentProvider = componentProvider; - } + private Instance(ComponentProvider componentProvider) { + this.componentProvider = componentProvider; + } /** * Queues work on AsyncQueue. This is required when faking responses from server since they @@ -125,13 +121,14 @@ public Task> getWriteClient(int i) { public final FirebaseFirestore.InstanceRegistry instanceRegistry = mock(FirebaseFirestore.InstanceRegistry.class); - public FirebaseFirestoreIntegrationTestFactory(DatabaseId databaseId) { - firestore = new FirebaseFirestore( - ApplicationProvider.getApplicationContext(), - databaseId, - "k", - EmptyCredentialsProvider::new, - EmptyAppCheckTokenProvider::new, + public FirebaseFirestoreIntegrationTestFactory(DatabaseId databaseId) { + firestore = + new FirebaseFirestore( + ApplicationProvider.getApplicationContext(), + databaseId, + "k", + EmptyCredentialsProvider::new, + EmptyAppCheckTokenProvider::new, this::componentProvider, null, instanceRegistry, @@ -146,16 +143,16 @@ public void useMemoryCache() { } public void setClearPersistenceMethod(Function> clearPersistenceMethod) { - firestore.clearPersistenceMethod = clearPersistenceMethod; - } + firestore.clearPersistenceMethod = clearPersistenceMethod; + } - private GrpcCallProvider mockGrpcCallProvider(Instance instance) { - GrpcCallProvider mockGrpcCallProvider = mock(GrpcCallProvider.class); - when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getListenMethod()))) - .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.listens.next()))); - when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getWriteMethod()))) - .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.writes.next()))); - instance.mockGrpcCallProvider = mockGrpcCallProvider; + private GrpcCallProvider mockGrpcCallProvider(Instance instance) { + GrpcCallProvider mockGrpcCallProvider = mock(GrpcCallProvider.class); + when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getListenMethod()))) + .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.listens.next()))); + when(mockGrpcCallProvider.createClientCall(eq(FirestoreGrpc.getWriteMethod()))) + .thenAnswer(invocation -> Tasks.forResult(new TestClientCall<>(instance.writes.next()))); + instance.mockGrpcCallProvider = mockGrpcCallProvider; return mockGrpcCallProvider; } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java index e5e942b4cf1..0dd96d74620 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/FirebaseFirestoreTest.java @@ -49,6 +49,8 @@ import com.google.firestore.v1.WriteRequest; import com.google.firestore.v1.WriteResponse; import com.google.firestore.v1.WriteResult; +import com.google.protobuf.ByteString; +import io.grpc.ClientCall; import io.grpc.Metadata; import io.grpc.Status; import java.util.ArrayList; @@ -84,16 +86,16 @@ private static T waitForResult(Task task) throws InterruptedException { } private static Exception waitForException(Task task) throws InterruptedException { - Exception exception = waitFor(task).getException(); - assertThat(exception).isNotNull(); - return exception; - } + Exception exception = waitFor(task).getException(); + assertThat(exception).isNotNull(); + return exception; + } - @NonNull - public static String getResourcePrefixValue(DatabaseId databaseId) { - return String.format( - "projects/%s/databases/%s", databaseId.getProjectId(), databaseId.getDatabaseId()); - } + @NonNull + public static String getResourcePrefixValue(DatabaseId databaseId) { + return String.format( + "projects/%s/databases/%s", databaseId.getProjectId(), databaseId.getDatabaseId()); + } private static Task waitFor(Task task) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); @@ -104,17 +106,18 @@ private static Task waitFor(Task task) throws InterruptedException { return task; } - private T waitForException(Task task, Class clazz) throws InterruptedException { - return clazz.cast(waitForException(task)); - } + private T waitForException(Task task, Class clazz) + throws InterruptedException { + return clazz.cast(waitForException(task)); + } - @Before - public void before() { - databaseId = DatabaseId.forDatabase("p", "d"); - factory = new FirebaseFirestoreIntegrationTestFactory(databaseId); - factory.useMemoryCache(); - firestore = factory.firestore; - } + @Before + public void before() { + databaseId = DatabaseId.forDatabase("p", "d"); + factory = new FirebaseFirestoreIntegrationTestFactory(databaseId); + factory.useMemoryCache(); + firestore = factory.firestore; + } @After public void after() throws Exception { @@ -150,10 +153,13 @@ public void preserveWritesWhenDisconnectedWithInternalError() throws Exception { // Wait for WriteRequest handshake. // We expect an empty init request because the database is fresh. - assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(InitRequest.getDefaultInstance())); + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(InitRequest.getDefaultInstance())); // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token1"))))); + waitForSuccess( + instance.enqueue( + () -> callback.listener.onMessage(writeResponse(initResponse("token1"))))); // Expect first write request. Write write1 = serializer.encodeMutation(setMutation(doc1, map("foo", "A"))); @@ -186,7 +192,9 @@ public void preserveWritesWhenDisconnectedWithInternalError() throws Exception { assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(initRequest("token1"))); // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token2"))))); + waitForSuccess( + instance.enqueue( + () -> callback.listener.onMessage(writeResponse(initResponse("token2"))))); // Expect second write to be retried. Write write2 = serializer.encodeMutation(setMutation(doc2, map("foo", "B"))); @@ -203,338 +211,369 @@ public void preserveWritesWhenDisconnectedWithInternalError() throws Exception { } } + @Test() + public void clearPersistanceAfterStartupShouldRestartFirestoreClient() throws Exception { + // Trigger instantiation of FirestoreClient + firestore.collection("col"); + FirebaseFirestoreIntegrationTestFactory.Instance first = + waitForResult(factory.instances.get(0)); - @Test() - public void clearPersistanceAfterStartupShouldRestartFirestoreClient() throws Exception { - // Trigger instantiation of FirestoreClient - firestore.collection("col"); + AsyncQueue firstAsyncQueue = first.configuration.asyncQueue; - FirebaseFirestoreIntegrationTestFactory.Instance first = waitForResult(factory.instances.get(0)); + assertFalse(firstAsyncQueue.isShuttingDown()); - AsyncQueue firstAsyncQueue = first.configuration.asyncQueue; + // Clearing persistence will require restarting FirestoreClient. + waitForSuccess(firestore.clearPersistence()); - assertFalse(firstAsyncQueue.isShuttingDown()); + // Now we have a history of 2 instances. + FirebaseFirestoreIntegrationTestFactory.Instance second = + waitForResult(factory.instances.get(1)); + AsyncQueue secondAsyncQueue = second.configuration.asyncQueue; - // Clearing persistence will require restarting FirestoreClient. - waitForSuccess(firestore.clearPersistence()); + assertEquals(firstAsyncQueue.getExecutor(), secondAsyncQueue.getExecutor()); - // Now we have a history of 2 instances. - FirebaseFirestoreIntegrationTestFactory.Instance second = waitForResult(factory.instances.get(1)); - AsyncQueue secondAsyncQueue = second.configuration.asyncQueue; + assertTrue(firstAsyncQueue.isShuttingDown()); + assertFalse(secondAsyncQueue.isShuttingDown()); - assertEquals(firstAsyncQueue.getExecutor(), secondAsyncQueue.getExecutor()); + // AsyncQueue of first instance should reject tasks. + Exception firstTask = waitForException(firstAsyncQueue.enqueue(() -> "Hi")); + assertThat(firstTask).isInstanceOf(RejectedExecutionException.class); + assertThat(firstTask).hasMessageThat().isEqualTo("AsyncQueue is shutdown"); - assertTrue(firstAsyncQueue.isShuttingDown()); - assertFalse(secondAsyncQueue.isShuttingDown()); + // AsyncQueue of second instance should be functional. + assertThat(waitFor(secondAsyncQueue.enqueue(() -> "Hello")).getResult()).isEqualTo("Hello"); - // AsyncQueue of first instance should reject tasks. - Exception firstTask = waitForException(firstAsyncQueue.enqueue(() -> "Hi")); - assertThat(firstTask).isInstanceOf(RejectedExecutionException.class); - assertThat(firstTask).hasMessageThat().isEqualTo("AsyncQueue is shutdown"); + waitForSuccess(firestore.terminate()); - // AsyncQueue of second instance should be functional. - assertThat(waitFor(secondAsyncQueue.enqueue(() -> "Hello")).getResult()).isEqualTo("Hello"); + // After terminate the second instance should also reject tasks. + Exception afterTerminate = waitForException(secondAsyncQueue.enqueue(() -> "Uh oh")); + assertThat(afterTerminate).isInstanceOf(RejectedExecutionException.class); + assertThat(afterTerminate).hasMessageThat().isEqualTo("AsyncQueue is shutdown"); + } - waitForSuccess(firestore.terminate()); + @Test + public void clearPersistenceDueToInitResponse() throws Exception { + // Create a snapshot listener that will be active during handshake clearing of cache. + TestEventListener snapshotListener1 = new TestEventListener<>(); + firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener1); + Iterator> snapshots = snapshotListener1.iterator(); + Task firstSnapshot = snapshots.next(); - // After terminate the second instance should also reject tasks. - Exception afterTerminate = waitForException(secondAsyncQueue.enqueue(() -> "Uh oh")); - assertThat(afterTerminate).isInstanceOf(RejectedExecutionException.class); - assertThat(afterTerminate).hasMessageThat().isEqualTo("AsyncQueue is shutdown"); - } + // Wait for first FirestoreClient to instantiate + FirebaseFirestoreIntegrationTestFactory.Instance first = + waitForResult(factory.instances.get(0)); - @Test - public void clearPersistenceDueToInitResponse() throws Exception { - // Create a snapshot listener that will be active during handshake clearing of cache. - TestEventListener snapshotListener1 = new TestEventListener<>(); - firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener1); - Iterator> snapshots = snapshotListener1.iterator(); - Task firstSnapshot = snapshots.next(); + // Wait for Listen CallClient to be created. + TestClientCall callback1 = + waitForResult(first.getListenClient(0)); - // Wait for first FirestoreClient to instantiate - FirebaseFirestoreIntegrationTestFactory.Instance first = waitForResult(factory.instances.get(0)); + // Wait for ListenRequest handshake. + // We expect an empty init request because the database is fresh. + assertThat(waitForResult(callback1.getRequest(0))) + .isEqualTo(listenRequest(InitRequest.getDefaultInstance())); - // Wait for Listen CallClient to be created. - TestClientCall callback1 = waitForResult(first.getListenClient(0)); + // Simulate a successful InitResponse from server. + waitForSuccess( + first.enqueue(() -> callback1.listener.onMessage(listenResponse(initResponse("token1"))))); - // Wait for ListenRequest handshake. - // We expect an empty init request because the database is fresh. - assertThat(waitForResult(callback1.getRequest(0))) - .isEqualTo(listenRequest(InitRequest.getDefaultInstance())); + // We expect previous addSnapshotListener to cause a, AddTarget request. + assertTrue(waitForResult(callback1.getRequest(1)).hasAddTarget()); - // Simulate a successful InitResponse from server. - waitForSuccess(first.enqueue(() -> callback1.listener.onMessage(listenResponse(initResponse("token1"))))); + // TODO(does this make sense?) + // We have a 10 second timeout on raising snapshot from Cache, that is triggered when Listen + // connection is closed. + assertFalse(firstSnapshot.isComplete()); - // We expect previous addSnapshotListener to cause a, AddTarget request. - assertTrue(waitForResult(callback1.getRequest(1)).hasAddTarget()); + // Simulate Database deletion by closing connection with NOT_FOUND. + waitForSuccess( + first.enqueue(() -> callback1.listener.onClose(Status.NOT_FOUND, new Metadata()))); - // TODO(does this make sense?) - // We have a 10 second timeout on raising snapshot from Cache, that is triggered when Listen connection is closed. - assertFalse(firstSnapshot.isComplete()); + // First snapshot is raised from cache immediately after connection is closed. + assertTrue(waitForResult(firstSnapshot).getMetadata().isFromCache()); - // Simulate Database deletion by closing connection with NOT_FOUND. - waitForSuccess(first.enqueue(() -> callback1.listener.onClose(Status.NOT_FOUND, new Metadata()))); + // We expect client to reconnect Listen stream. + TestClientCall callback2 = + waitForResult(first.getListenClient(1)); - // First snapshot is raised from cache immediately after connection is closed. - assertTrue(waitForResult(firstSnapshot).getMetadata().isFromCache()); + // Wait for ListenRequest. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(callback2.getRequest(0))) + .isEqualTo(listenRequest(initRequest("token1"))); - // We expect client to reconnect Listen stream. - TestClientCall callback2 = waitForResult(first.getListenClient(1)); + // This task will complete when clearPersistence is invoked on FirebaseFirestore. + Task clearPersistenceTask = setupClearPersistenceTask(); - // Wait for ListenRequest. - // We expect FirestoreClient to send InitRequest with previous token. - assertThat(waitForResult(callback2.getRequest(0))) - .isEqualTo(listenRequest(initRequest("token1"))); + // Simulate a clear cache InitResponse from server. + waitForSuccess( + first.enqueue( + () -> callback2.listener.onMessage(listenResponse(initResponse("token2", true))))); - // This task will complete when clearPersistence is invoked on FirebaseFirestore. - Task clearPersistenceTask = setupClearPersistenceTask(); + // Wait for cleanPersistence to be run. + waitForSuccess(clearPersistenceTask); - // Simulate a clear cache InitResponse from server. - waitForSuccess(first.enqueue(() -> callback2.listener.onMessage( - listenResponse(initResponse("token2", true))))); + // Verify that the first FirestoreClient was shutdown. If the GrpcCallProvider component has + // has it's shutdown method called, then we know shutdown was triggered. + verify(first.mockGrpcCallProvider, times(1)).shutdown(); - // Wait for cleanPersistence to be run. - waitForSuccess(clearPersistenceTask); + // Snapshot listeners should fail with ABORTED + FirebaseFirestoreException exception = + waitForException(snapshots.next(), FirebaseFirestoreException.class); + assertThat(exception.getCode()).isEqualTo(FirebaseFirestoreException.Code.ABORTED); - // Verify that the first FirestoreClient was shutdown. If the GrpcCallProvider component has - // has it's shutdown method called, then we know shutdown was triggered. - verify(first.mockGrpcCallProvider, times(1)).shutdown(); + // Start another snapshot listener + TestEventListener snapshotListener2 = new TestEventListener<>(); + firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener2); - // Snapshot listeners should fail with ABORTED - FirebaseFirestoreException exception = waitForException(snapshots.next(), FirebaseFirestoreException.class); - assertThat(exception.getCode()).isEqualTo(FirebaseFirestoreException.Code.ABORTED); + // Wait for second FirestoreClient to instantiate + FirebaseFirestoreIntegrationTestFactory.Instance second = + waitForResult(factory.instances.get(1)); - // Start another snapshot listener - TestEventListener snapshotListener2 = new TestEventListener<>(); - firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener2); + // Wait for Listen CallClient to be created. + TestClientCall callback3 = + waitForResult(second.getListenClient(0)); - // Wait for second FirestoreClient to instantiate - FirebaseFirestoreIntegrationTestFactory.Instance second = waitForResult(factory.instances.get(1)); + // Wait for ListenRequest. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(callback3.getRequest(0))) + .isEqualTo(listenRequest(initRequest("token2"))); + } - // Wait for Listen CallClient to be created. - TestClientCall callback3 = waitForResult(second.getListenClient(0)); + @Test + public void preserveWritesWhenDisconnectedWithNotFound() throws Exception { + CollectionReference col = firestore.collection("col"); + DocumentReference doc1 = col.document(); + DocumentReference doc2 = col.document(); + DocumentReference doc3 = col.document(); + doc1.set(map("foo", "A")); + doc2.set(map("foo", "B")); + doc3.set(map("foo", "C")); - // Wait for ListenRequest. + // 1st FirestoreClient instance. + { + // Wait for first FirestoreClient to instantiate + FirebaseFirestoreIntegrationTestFactory.Instance instance = + waitForResult(factory.instances.get(0)); + RemoteSerializer serializer = instance.componentProvider.getRemoteSerializer(); + + // First Write stream connection + { + // Wait for Write CallClient to be created. + TestClientCall callback = + waitForResult(instance.getWriteClient(0)); + Iterator> requests = callback.requestIterator(); + + // Wait for WriteRequest handshake. + // We expect an empty init request because the database is fresh. + assertThat(waitForResult(requests.next())) + .isEqualTo(writeRequest(InitRequest.getDefaultInstance())); + + // Simulate a successful InitResponse from server. + waitForSuccess( + instance.enqueue( + () -> callback.listener.onMessage(writeResponse(initResponse("token1"))))); + + // Expect first write request. + Write write1 = serializer.encodeMutation(setMutation(doc1, map("foo", "A"))); + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(write1)); + + // Simulate write acknowledgement. + waitForSuccess( + instance.enqueue( + () -> + callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); + + // Expect second write request. + Write write2 = serializer.encodeMutation(setMutation(doc2, map("foo", "B"))); + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(write2)); + + // Simulate NOT_FOUND error that was NOT due to database name reuse. ( + waitForSuccess( + instance.enqueue(() -> callback.listener.onClose(Status.NOT_FOUND, new Metadata()))); + } + + // Second Write Stream connection + // Previous connection was closed by server with NOT_FOUND error. + { + // Wait for Write CallClient to be created. + TestClientCall callback = + waitForResult(instance.getWriteClient(1)); + Iterator> requests = callback.requestIterator(); + + // Wait for WriteRequest handshake. // We expect FirestoreClient to send InitRequest with previous token. - assertThat(waitForResult(callback3.getRequest(0))) - .isEqualTo(listenRequest(initRequest("token2"))); - } + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(initRequest("token1"))); - @Test - public void preserveWritesWhenDisconnectedWithNotFound() throws Exception { - CollectionReference col = firestore.collection("col"); - DocumentReference doc1 = col.document(); - DocumentReference doc2 = col.document(); - DocumentReference doc3 = col.document(); - doc1.set(map("foo", "A")); - doc2.set(map("foo", "B")); - doc3.set(map("foo", "C")); - - // 1st FirestoreClient instance. - { - // Wait for first FirestoreClient to instantiate - FirebaseFirestoreIntegrationTestFactory.Instance instance = waitForResult(factory.instances.get(0)); - RemoteSerializer serializer = instance.componentProvider.getRemoteSerializer(); - - // First Write stream connection - { - // Wait for Write CallClient to be created. - TestClientCall callback = waitForResult(instance.getWriteClient(0)); - Iterator> requests = callback.requestIterator(); - - // Wait for WriteRequest handshake. - // We expect an empty init request because the database is fresh. - assertThat(waitForResult(requests.next())) - .isEqualTo(writeRequest(InitRequest.getDefaultInstance())); - - // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token1"))))); - - // Expect first write request. - Write write1 = serializer.encodeMutation(setMutation(doc1, map("foo", "A"))); - assertThat(waitForResult(requests.next())) - .isEqualTo(writeRequest(write1)); - - // Simulate write acknowledgement. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); - - // Expect second write request. - Write write2 = serializer.encodeMutation(setMutation(doc2, map("foo", "B"))); - assertThat(waitForResult(requests.next())) - .isEqualTo(writeRequest(write2)); - - // Simulate NOT_FOUND error that was NOT due to database name reuse. ( - waitForSuccess(instance.enqueue(() -> callback.listener.onClose(Status.NOT_FOUND, new Metadata()))); - } - - // Second Write Stream connection - // Previous connection was closed by server with NOT_FOUND error. - { - // Wait for Write CallClient to be created. - TestClientCall callback = waitForResult(instance.getWriteClient(1)); - Iterator> requests = callback.requestIterator(); - - // Wait for WriteRequest handshake. - // We expect FirestoreClient to send InitRequest with previous token. - assertThat(waitForResult(requests.next())) - .isEqualTo(writeRequest(initRequest("token1"))); - - // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token2"))))); - - // Expect second write to be retried. - Write write2 = serializer.encodeMutation(setMutation(doc2, map("foo", "B"))); - assertThat(waitForResult(requests.next())) - .isEqualTo(writeRequest(write2)); - - // Simulate write acknowledgement. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); - - // Simulate NOT_FOUND error. This time we will clear cache. - waitForSuccess(instance.enqueue(() -> callback.listener.onClose(Status.NOT_FOUND, new Metadata()))); - } - - - // Third Write Stream connection - // Previous connection was closed by server with NOT_FOUND error. - { - // Wait for Write CallClient to be created. - TestClientCall callback = waitForResult(instance.getWriteClient(2)); - Iterator> requests = callback.requestIterator(); - - // Wait for WriteRequest. - // We expect FirestoreClient to send InitRequest with previous token. - assertThat(waitForResult(requests.next())) - .isEqualTo(writeRequest(initRequest("token2"))); - - // Simulate a clear cache InitResponse from server. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token3", true))))); - } - } - - // Interaction with 2nd FirestoreClient instance. - // Previous instance was shutdown due to clear cache command from server. - { - // Wait for second FirestoreClient to instantiate - FirebaseFirestoreIntegrationTestFactory.Instance instance = waitForResult(factory.instances.get(1)); - RemoteSerializer serializer = instance.componentProvider.getRemoteSerializer(); - - // The writes should have been cleared, so we will have to create a new one. - DocumentReference doc4 = col.document(); - doc4.set(map("foo", "D")); - - // Wait for Write CallClient to be created. - TestClientCall callback = waitForResult(instance.getWriteClient(0)); - Iterator> requests = callback.requestIterator(); - - // Wait for WriteRequest. - // We expect FirestoreClient to send InitRequest with previous token. - assertThat(waitForResult(requests.next())) - .isEqualTo(writeRequest(initRequest("token3"))); - - // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(initResponse("token4"))))); - - // Expect the new write request. - Write write4 = serializer.encodeMutation(setMutation(doc4, map("foo", "D"))); - assertThat(waitForResult(requests.next())) - .isEqualTo(writeRequest(write4)); - - // Simulate write acknowledgement. - waitForSuccess(instance.enqueue(() -> callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); - } + // Simulate a successful InitResponse from server. + waitForSuccess( + instance.enqueue( + () -> callback.listener.onMessage(writeResponse(initResponse("token2"))))); + + // Expect second write to be retried. + Write write2 = serializer.encodeMutation(setMutation(doc2, map("foo", "B"))); + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(write2)); + + // Simulate write acknowledgement. + waitForSuccess( + instance.enqueue( + () -> + callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); + + // Simulate NOT_FOUND error. This time we will clear cache. + waitForSuccess( + instance.enqueue(() -> callback.listener.onClose(Status.NOT_FOUND, new Metadata()))); + } + + // Third Write Stream connection + // Previous connection was closed by server with NOT_FOUND error. + { + // Wait for Write CallClient to be created. + TestClientCall callback = + waitForResult(instance.getWriteClient(2)); + Iterator> requests = callback.requestIterator(); + + // Wait for WriteRequest. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(initRequest("token2"))); + + // Simulate a clear cache InitResponse from server. + waitForSuccess( + instance.enqueue( + () -> callback.listener.onMessage(writeResponse(initResponse("token3", true))))); + } } - @Test - public void listenHandshakeMustWaitForWriteHandshakeToComplete() throws Exception { - CollectionReference col = firestore.collection("col"); + // Interaction with 2nd FirestoreClient instance. + // Previous instance was shutdown due to clear cache command from server. + { + // Wait for second FirestoreClient to instantiate + FirebaseFirestoreIntegrationTestFactory.Instance instance = + waitForResult(factory.instances.get(1)); + RemoteSerializer serializer = instance.componentProvider.getRemoteSerializer(); + + // The writes should have been cleared, so we will have to create a new one. + DocumentReference doc4 = col.document(); + doc4.set(map("foo", "D")); + + // Wait for Write CallClient to be created. + TestClientCall callback = + waitForResult(instance.getWriteClient(0)); + Iterator> requests = callback.requestIterator(); + + // Wait for WriteRequest. + // We expect FirestoreClient to send InitRequest with previous token. + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(initRequest("token3"))); + + // Simulate a successful InitResponse from server. + waitForSuccess( + instance.enqueue( + () -> callback.listener.onMessage(writeResponse(initResponse("token4"))))); - // Wait for FirestoreClient to instantiate - FirebaseFirestoreIntegrationTestFactory.Instance instance = waitForResult(factory.instances.get(0)); + // Expect the new write request. + Write write4 = serializer.encodeMutation(setMutation(doc4, map("foo", "D"))); + assertThat(waitForResult(requests.next())).isEqualTo(writeRequest(write4)); - // Trigger Write Stream First - col.document().set(map("foo", "A")); + // Simulate write acknowledgement. + waitForSuccess( + instance.enqueue( + () -> callback.listener.onMessage(writeResponse(WriteResult.getDefaultInstance())))); + } + } + + @Test + public void listenHandshakeMustWaitForWriteHandshakeToComplete() throws Exception { + CollectionReference col = firestore.collection("col"); - TestClientCall write = waitForResult(instance.getWriteClient(0)); - ClientCall.Listener writeResponses = write.listener; - Iterator> writeRequests = write.requestIterator(); + // Wait for FirestoreClient to instantiate + FirebaseFirestoreIntegrationTestFactory.Instance instance = + waitForResult(factory.instances.get(0)); - // Then Trigger Listen Stream; - TestEventListener snapshotListener = new TestEventListener<>(); - firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener); - Iterator> snapshots = snapshotListener.iterator(); + // Trigger Write Stream First + col.document().set(map("foo", "A")); - TestClientCall listen = waitForResult(instance.getListenClient(0)); - Iterator> listenRequests = listen.requestIterator(); - ClientCall.Listener listenResponses = listen.listener; + TestClientCall write = waitForResult(instance.getWriteClient(0)); + ClientCall.Listener writeResponses = write.listener; + Iterator> writeRequests = write.requestIterator(); - // Prepare - Task writeInitRequest = writeRequests.next(); - Task listenInitRequest = listenRequests.next(); + // Then Trigger Listen Stream; + TestEventListener snapshotListener = new TestEventListener<>(); + firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener); + Iterator> snapshots = snapshotListener.iterator(); - // Expect empty InitRequest from Write stream. - assertThat(waitForResult(writeInitRequest)) - .isEqualTo(writeRequest(InitRequest.getDefaultInstance())); + TestClientCall listen = + waitForResult(instance.getListenClient(0)); + Iterator> listenRequests = listen.requestIterator(); + ClientCall.Listener listenResponses = listen.listener; - // No request should have come from Listen stream yet. - assertFalse(listenInitRequest.isComplete()); + // Prepare + Task writeInitRequest = writeRequests.next(); + Task listenInitRequest = listenRequests.next(); - // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> writeResponses.onMessage(writeResponse(initResponse("token1"))))); + // Expect empty InitRequest from Write stream. + assertThat(waitForResult(writeInitRequest)) + .isEqualTo(writeRequest(InitRequest.getDefaultInstance())); - // Now that Write handshake is complete, the Listen stream should send a InitRequest with token from Write handshake. - assertThat(waitForResult(listenInitRequest)) - .isEqualTo(listenRequest(initRequest("token1"))); - } + // No request should have come from Listen stream yet. + assertFalse(listenInitRequest.isComplete()); - @Test - public void writeHandshakeMustWaitForListenHandshakeToComplete() throws Exception { - CollectionReference col = firestore.collection("col"); + // Simulate a successful InitResponse from server. + waitForSuccess( + instance.enqueue(() -> writeResponses.onMessage(writeResponse(initResponse("token1"))))); - // Wait for FirestoreClient to instantiate - FirebaseFirestoreIntegrationTestFactory.Instance instance = waitForResult(factory.instances.get(0)); + // Now that Write handshake is complete, the Listen stream should send a InitRequest with token + // from Write handshake. + assertThat(waitForResult(listenInitRequest)).isEqualTo(listenRequest(initRequest("token1"))); + } - // Trigger Listen Stream First - TestEventListener snapshotListener = new TestEventListener<>(); - firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener); - Iterator> snapshots = snapshotListener.iterator(); + @Test + public void writeHandshakeMustWaitForListenHandshakeToComplete() throws Exception { + CollectionReference col = firestore.collection("col"); - TestClientCall listen = waitForResult(instance.getListenClient(0)); - Iterator> listenRequests = listen.requestIterator(); - ClientCall.Listener listenResponses = listen.listener; + // Wait for FirestoreClient to instantiate + FirebaseFirestoreIntegrationTestFactory.Instance instance = + waitForResult(factory.instances.get(0)); - // Then Trigger Write Stream; - col.document().set(map("foo", "A")); + // Trigger Listen Stream First + TestEventListener snapshotListener = new TestEventListener<>(); + firestore.collection("col").addSnapshotListener(BACKGROUND_EXECUTOR, snapshotListener); + Iterator> snapshots = snapshotListener.iterator(); - TestClientCall write = waitForResult(instance.getWriteClient(0)); - ClientCall.Listener writeResponses = write.listener; - Iterator> writeRequests = write.requestIterator(); + TestClientCall listen = + waitForResult(instance.getListenClient(0)); + Iterator> listenRequests = listen.requestIterator(); + ClientCall.Listener listenResponses = listen.listener; - // Prepare - Task writeInitRequest = writeRequests.next(); - Task listenInitRequest = listenRequests.next(); + // Then Trigger Write Stream; + col.document().set(map("foo", "A")); - // Expect empty InitRequest from Listen stream. - assertThat(waitForResult(listenInitRequest)) - .isEqualTo(listenRequest(InitRequest.getDefaultInstance())); + TestClientCall write = waitForResult(instance.getWriteClient(0)); + ClientCall.Listener writeResponses = write.listener; + Iterator> writeRequests = write.requestIterator(); - // No request should have come from Listen stream yet. - assertFalse(writeInitRequest.isComplete()); + // Prepare + Task writeInitRequest = writeRequests.next(); + Task listenInitRequest = listenRequests.next(); - // Simulate a successful InitResponse from server. - waitForSuccess(instance.enqueue(() -> listenResponses.onMessage(listenResponse(initResponse("token1"))))); + // Expect empty InitRequest from Listen stream. + assertThat(waitForResult(listenInitRequest)) + .isEqualTo(listenRequest(InitRequest.getDefaultInstance())); - // Now that Write handshake is complete, the Listen stream should send a InitRequest with token from Write handshake. - assertThat(waitForResult(writeInitRequest)) - .isEqualTo(writeRequest(initRequest("token1"))); + // No request should have come from Listen stream yet. + assertFalse(writeInitRequest.isComplete()); - } + // Simulate a successful InitResponse from server. + waitForSuccess( + instance.enqueue(() -> listenResponses.onMessage(listenResponse(initResponse("token1"))))); - @NonNull - private DocumentKey key(DocumentReference doc) { - return DocumentKey.fromPathString(doc.getPath()); - } + // Now that Write handshake is complete, the Listen stream should send a InitRequest with token + // from Write handshake. + assertThat(waitForResult(writeInitRequest)).isEqualTo(writeRequest(initRequest("token1"))); + } + + @NonNull + private DocumentKey key(DocumentReference doc) { + return DocumentKey.fromPathString(doc.getPath()); + } @NonNull public SetMutation setMutation(DocumentReference doc, Map values) { @@ -550,25 +589,25 @@ public SetMutation setMutation(DocumentReference doc, Map values } @NonNull - private ListenRequest listenRequest(InitRequest initRequest) { - return ListenRequest.newBuilder() - .setDatabase(getResourcePrefixValue(databaseId)) - .setInitRequest(initRequest) - .build(); - } + private ListenRequest listenRequest(InitRequest initRequest) { + return ListenRequest.newBuilder() + .setDatabase(getResourcePrefixValue(databaseId)) + .setInitRequest(initRequest) + .build(); + } - @NonNull - private static ListenResponse listenResponse(InitResponse initResponse) { - return ListenResponse.newBuilder() - .setInitResponse(initResponse) - .build(); - } + @NonNull + private static ListenResponse listenResponse(InitResponse initResponse) { + return ListenResponse.newBuilder().setInitResponse(initResponse).build(); + } - @NonNull + @NonNull private WriteRequest writeRequest(InitRequest initRequest) { - return WriteRequest.newBuilder().setDatabase(getResourcePrefixValue(databaseId)).setInitRequest(initRequest) - .build(); - } + return WriteRequest.newBuilder() + .setDatabase(getResourcePrefixValue(databaseId)) + .setInitRequest(initRequest) + .build(); + } @NonNull private WriteRequest writeRequest(Write... writes) { @@ -580,11 +619,9 @@ private WriteRequest writeRequest(Write... writes) { } @NonNull - private static WriteResponse writeResponse(InitResponse initResponse) { - return WriteResponse.newBuilder() - .setInitResponse(initResponse) - .build(); - } + private static WriteResponse writeResponse(InitResponse initResponse) { + return WriteResponse.newBuilder().setInitResponse(initResponse).build(); + } @NonNull private static WriteResponse writeResponse(WriteResult... writeResults) { @@ -595,35 +632,32 @@ private static WriteResponse writeResponse(WriteResult... writeResults) { return builder.build(); } - @NonNull - private static InitResponse initResponse(String token) { - return InitResponse.newBuilder() - .setSessionToken(ByteString.copyFromUtf8(token)) - .build(); - } + @NonNull + private static InitResponse initResponse(String token) { + return InitResponse.newBuilder().setSessionToken(ByteString.copyFromUtf8(token)).build(); + } - @NonNull - private static InitResponse initResponse(String token, boolean clearCache) { - return InitResponse.newBuilder() - .setSessionToken(ByteString.copyFromUtf8(token)) - .setClearCache(clearCache) - .build(); - } + @NonNull + private static InitResponse initResponse(String token, boolean clearCache) { + return InitResponse.newBuilder() + .setSessionToken(ByteString.copyFromUtf8(token)) + .setClearCache(clearCache) + .build(); + } - @NonNull - private static InitRequest initRequest(String token) { - return InitRequest.newBuilder() - .setSessionToken(ByteString.copyFromUtf8(token)) - .build(); - } + @NonNull + private static InitRequest initRequest(String token) { + return InitRequest.newBuilder().setSessionToken(ByteString.copyFromUtf8(token)).build(); + } - @NonNull - private Task setupClearPersistenceTask() { - TaskCompletionSource clearPersistenceTask = new TaskCompletionSource<>(); - factory.setClearPersistenceMethod(executor -> { - executor.execute(() -> clearPersistenceTask.setResult(null)); - return clearPersistenceTask.getTask(); + @NonNull + private Task setupClearPersistenceTask() { + TaskCompletionSource clearPersistenceTask = new TaskCompletionSource<>(); + factory.setClearPersistenceMethod( + executor -> { + executor.execute(() -> clearPersistenceTask.setResult(null)); + return clearPersistenceTask.getTask(); }); - return clearPersistenceTask.getTask(); - } + return clearPersistenceTask.getTask(); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java index 01a3b604bd9..7fbeca8f3ca 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/integration/TestEventListener.java @@ -16,36 +16,34 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; - import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.EventListener; import com.google.firebase.firestore.FirebaseFirestoreException; - import java.util.Iterator; /** * EventListener test harness. */ -class TestEventListenerimplements EventListener { +class TestEventListener implements EventListener { - AsyncTaskAccumulator events = new AsyncTaskAccumulator<>(); + AsyncTaskAccumulator events = new AsyncTaskAccumulator<>(); - @Override - public synchronized void onEvent(@Nullable T value, @Nullable FirebaseFirestoreException error) { - if (error == null) { - events.onResult(value); - } else { - events.onException(error); - } + @Override + public synchronized void onEvent(@Nullable T value, @Nullable FirebaseFirestoreException error) { + if (error == null) { + events.onResult(value); + } else { + events.onException(error); } + } - @NonNull - public synchronized Task get(int index) { - return events.get(index); - } + @NonNull + public synchronized Task get(int index) { + return events.get(index); + } - @NonNull - public Iterator> iterator() { - return events.iterator(); - } + @NonNull + public Iterator> iterator() { + return events.iterator(); + } } diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java index 31eb6d27dc9..f067991cf5b 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/remote/MockDatastore.java @@ -27,7 +27,6 @@ import com.google.firebase.firestore.util.Util; import com.google.firestore.v1.InitResponse; import com.google.protobuf.ByteString; - import io.grpc.Status; import java.util.ArrayList; import java.util.HashMap; @@ -83,7 +82,8 @@ public boolean isOpen() { void sendHandshake(ByteString sessionToken) { hardAssert(!handshakeComplete, "Handshake already completed"); handshakeComplete = true; - InitResponse initResponse = InitResponse.newBuilder() + InitResponse initResponse = + InitResponse.newBuilder() .setSessionToken(sessionToken == null ? ByteString.EMPTY : sessionToken) .setClearCache(false) .build(); @@ -192,7 +192,8 @@ public void sendHandshake(ByteString sessionToken) { hardAssert(!handshakeComplete, "Handshake already completed"); writeStreamRequestCount += 1; handshakeComplete = true; - InitResponse initResponse = InitResponse.newBuilder() + InitResponse initResponse = + InitResponse.newBuilder() .setSessionToken(sessionToken == null ? ByteString.EMPTY : sessionToken) .setClearCache(false) .build(); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java index 3ad8cd5ce25..e482b5bbc35 100644 --- a/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/spec/SpecTestCase.java @@ -1305,9 +1305,9 @@ private void runSteps(JSONArray steps, JSONObject config) throws Exception { backgroundExecutor.execute(() -> drainBackgroundQueue.setResult(null)); waitFor(drainBackgroundQueue.getTask()); -// while (!queue.isIdle()) { -// Thread.sleep(1); -// } + // while (!queue.isIdle()) { + // Thread.sleep(1); + // } if (expectedSnapshotEvents != null) { log(" Validating expected snapshot events " + expectedSnapshotEvents);