From 17f2d43e36addb8a471103bf19711da106a63700 Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 16:28:20 +0000 Subject: [PATCH 01/14] Add and/or replace CRUD methods; begin testing --- .../internal/SyncMongoClientIntTests.kt | 17 +- .../android/services/mongodb/remote/Sync.java | 153 +++- .../mongodb/remote/SyncAggregateIterable.java | 4 + .../internal/SyncAggregateIterableImpl.java | 16 + .../mongodb/remote/internal/SyncImpl.java | 106 ++- .../CoreRemoteAggregateIterableImpl.java | 2 +- .../internal/CoreRemoteFindIterableImpl.java | 4 +- .../internal/CoreRemoteMongoIterableImpl.java | 2 +- .../mongodb/remote/sync/CoreSync.java | 149 +++- .../sync/CoreSyncAggregateIterable.java | 6 + .../mongodb/remote/sync/SyncCountOptions.java | 6 + .../mongodb/remote/sync/SyncDeleteResult.java | 9 + .../remote/sync/SyncInsertManyResult.java | 13 + .../remote/sync/SyncInsertOneResult.java | 11 + .../remote/sync/SyncUpdateOptions.java | 6 + .../mongodb/remote/sync/SyncUpdateResult.java | 15 + .../sync/internal/AggregateOperation.java | 41 + .../CoreSyncAggregateIterableImpl.java | 35 + .../internal/CoreSyncFindIterableImpl.java | 4 +- .../remote/sync/internal/CoreSyncImpl.java | 131 ++-- .../internal/CoreSyncMongoIterableImpl.java | 2 +- .../sync/internal/DataSynchronizer.java | 739 +++++++++++------- .../sync/internal/DeleteManyOperation.java | 32 + ...Operation.java => DeleteOneOperation.java} | 24 +- .../sync/internal/FindOneByIdOperation.java | 52 -- .../internal/InsertManyAndSyncOperation.java | 42 + .../internal/InsertOneAndSyncOperation.java | 9 +- .../remote/sync/internal/SyncOperations.java | 94 ++- .../sync/internal/UpdateManyOperation.java | 51 ++ ...Operation.java => UpdateOneOperation.java} | 45 +- .../CoreRemoteMongoCollectionUnitTests.java | 2 +- .../remote/sync/internal/CoreSyncUnitTests.kt | 47 +- .../internal/DataSynchronizerTestContext.kt | 5 +- .../internal/DataSynchronizerUnitTests.kt | 249 ++++-- .../sync/internal/SyncUnitTestHarness.kt | 49 +- .../core/testutils/sync/ProxySyncMethods.kt | 20 +- .../core/testutils/sync/SyncIntTestProxy.kt | 303 +++---- .../core/testutils/sync/SyncIntTestRunner.kt | 3 - .../server/services/mongodb/remote/Sync.java | 145 +++- .../mongodb/remote/SyncAggregateIterable.java | 4 + .../internal/SyncAggregateIterableImpl.java | 12 + .../mongodb/remote/internal/SyncImpl.java | 74 +- .../internal/SyncMongoClientIntTests.kt | 19 +- 43 files changed, 1822 insertions(+), 930 deletions(-) create mode 100644 android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/SyncAggregateIterable.java create mode 100644 android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncAggregateIterableImpl.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSyncAggregateIterable.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncCountOptions.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncDeleteResult.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertManyResult.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertOneResult.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateOptions.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateResult.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/AggregateOperation.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncAggregateIterableImpl.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteManyOperation.java rename core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/{DeleteOneByIdOperation.java => DeleteOneOperation.java} (70%) delete mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/FindOneByIdOperation.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertManyAndSyncOperation.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateManyOperation.java rename core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/{UpdateOneByIdOperation.java => UpdateOneOperation.java} (56%) create mode 100644 server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/SyncAggregateIterable.java create mode 100644 server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncAggregateIterableImpl.java diff --git a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt index 9675e4b08..1dbf542ed 100644 --- a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt +++ b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt @@ -75,16 +75,12 @@ class SyncMongoClientIntTests : BaseStitchAndroidIntTest(), SyncIntTestRunner { return Tasks.await(sync.insertOneAndSync(document)) } - override fun findOneById(id: BsonValue): Document? { - return Tasks.await(sync.findOneById(id)) + override fun updateOne(filter: Bson, update: Bson): RemoteUpdateResult { + return Tasks.await(sync.updateOne(filter, update)) } - override fun updateOneById(documentId: BsonValue, update: Bson): RemoteUpdateResult { - return Tasks.await(sync.updateOneById(documentId, update)) - } - - override fun deleteOneById(documentId: BsonValue): RemoteDeleteResult { - return Tasks.await(sync.deleteOneById(documentId)) + override fun deleteOne(filter: Bson): RemoteDeleteResult { + return Tasks.await(sync.deleteOne(filter)) } override fun desyncOne(id: BsonValue) { @@ -277,11 +273,6 @@ class SyncMongoClientIntTests : BaseStitchAndroidIntTest(), SyncIntTestRunner { testProxy.testInsertInsertConflict() } - @Test - override fun testPausedDocumentConfig() { - testProxy.testPausedDocumentConfig() - } - @Test override fun testConfigure() { } diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java index b55e8b60d..eacde3a1d 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java @@ -25,8 +25,17 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncFindIterable; import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult; +import java.util.List; import java.util.Set; import org.bson.BsonValue; @@ -103,14 +112,38 @@ void configure(@NonNull final ConflictHandler conflictHandler, boolean resumeSyncForDocument(@NonNull final BsonValue documentId); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Counts the number of documents in the collection. + * + * @return the number of documents in the collection + */ + Task count(); + + /** + * Counts the number of documents in the collection according to the given options. + * + * @param filter the query filter + * @return the number of documents in the collection + */ + Task count(final Bson filter); + + /** + * Counts the number of documents in the collection according to the given options. + * + * @param filter the query filter + * @param options the options describing the count + * @return the number of documents in the collection + */ + Task count(final Bson filter, final SyncCountOptions options); + + /** + * Finds all documents in the collection. * * @return the find iterable interface */ SyncFindIterable find(); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection. * * @param resultClass the class to decode each document into * @param the target document type of the iterable. @@ -119,7 +152,7 @@ void configure(@NonNull final ConflictHandler conflictHandler, SyncFindIterable find(final Class resultClass); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection. * * @param filter the query filter * @return the find iterable interface @@ -127,59 +160,119 @@ void configure(@NonNull final ConflictHandler conflictHandler, SyncFindIterable find(final Bson filter); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection. * * @param filter the query filter * @param resultClass the class to decode each document into * @param the target document type of the iterable. * @return the find iterable interface */ - SyncFindIterable find(final Bson filter, final Class resultClass); + SyncFindIterable find( + final Bson filter, + final Class resultClass); + /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Aggregates documents according to the specified aggregation pipeline. * - * @param documentId the _id of the document to search for. - * @return a task containing the document if found locally or remotely. + * @param pipeline the aggregation pipeline + * @return an iterable containing the result of the aggregation operation */ - Task findOneById(final BsonValue documentId); + SyncAggregateIterable aggregate(final List pipeline); /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Aggregates documents according to the specified aggregation pipeline. * - * @param documentId the _id of the document to search for. + * @param pipeline the aggregation pipeline * @param resultClass the class to decode each document into * @param the target document type of the iterable. - * @return a task containing the document if found locally or remotely. + * @return an iterable containing the result of the aggregation operation + */ + SyncAggregateIterable aggregate( + final List pipeline, + final Class resultClass); + + /** + * Inserts the provided document. If the document is missing an identifier, the client should + * generate one. + * + * @param document the document to insert + * @return the result of the insert one operation + */ + Task insertOneAndSync(final DocumentT document); + + /** + * Inserts one or more documents. + * + * @param documents the documents to insert + * @return the result of the insert many operation + */ + Task insertManyAndSync(final List documents); + + /** + * Removes at most one document from the collection that matches the given filter. If no + * documents match, the collection is not + * modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove one operation + */ + Task deleteOne(final Bson filter); + + /** + * Removes all documents from the collection that match the given query filter. If no documents + * match, the collection is not modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove many operation + */ + Task deleteMany(final Bson filter); + + /** + * Update a single document in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update one operation */ - Task findOneById(final BsonValue documentId, final Class resultClass); + Task updateOne(final Bson filter, final Bson update); /** - * Updates a document by the given id. It is first searched for in the local synchronized cache - * and if not found and there is internet connectivity, it is searched for remotely. + * Update a single document in the collection according to the specified arguments. * - * @param documentId the _id of the document to search for. - * @param update the update specifier. - * @return a task containing the result of the local or remote update. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update one operation */ - Task updateOneById(final BsonValue documentId, final Bson update); + Task updateOne( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions); /** - * Inserts a single document and begins to synchronize it. + * Update all documents in the collection according to the specified arguments. * - * @param document the document to insert and synchronize. - * @return the result of the insertion. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update many operation */ - Task insertOneAndSync(final DocumentT document); + Task updateMany(final Bson filter, final Bson update); /** - * Deletes a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Update all documents in the collection according to the specified arguments. * - * @param documentId the _id of the document to search for. - * @return a task containing the result of the local or remote update. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update many operation */ - Task deleteOneById(final BsonValue documentId); + Task updateMany( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions); } diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/SyncAggregateIterable.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/SyncAggregateIterable.java new file mode 100644 index 000000000..2a838fed9 --- /dev/null +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/SyncAggregateIterable.java @@ -0,0 +1,4 @@ +package com.mongodb.stitch.android.services.mongodb.remote; + +public interface SyncAggregateIterable extends RemoteMongoIterable { +} diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncAggregateIterableImpl.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncAggregateIterableImpl.java new file mode 100644 index 000000000..6ee995e05 --- /dev/null +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncAggregateIterableImpl.java @@ -0,0 +1,16 @@ +package com.mongodb.stitch.android.services.mongodb.remote.internal; + +import com.mongodb.stitch.android.core.internal.common.TaskDispatcher; +import com.mongodb.stitch.android.services.mongodb.remote.SyncAggregateIterable; +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; + +class SyncAggregateIterableImpl + extends RemoteMongoIterableImpl + implements SyncAggregateIterable { + SyncAggregateIterableImpl( + final CoreSyncAggregateIterable iterable, + TaskDispatcher dispatcher + ) { + super(iterable, dispatcher); + } +} diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncImpl.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncImpl.java index 1ec75b363..b6c3f8783 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncImpl.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncImpl.java @@ -22,18 +22,24 @@ import com.google.android.gms.tasks.Task; import com.mongodb.stitch.android.core.internal.common.TaskDispatcher; import com.mongodb.stitch.android.services.mongodb.remote.Sync; +import com.mongodb.stitch.android.services.mongodb.remote.SyncAggregateIterable; import com.mongodb.stitch.android.services.mongodb.remote.SyncFindIterable; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSync; import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener; - +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult; + +import java.util.List; import java.util.Set; import java.util.concurrent.Callable; +import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.conversions.Bson; @@ -89,6 +95,37 @@ public boolean resumeSyncForDocument(@NonNull final BsonValue documentId) { return this.proxy.resumeSyncForDocument(documentId); } + @Override + public Task count() { + return this.count(new BsonDocument()); + } + + @Override + public Task count(Bson filter) { + return this.count(filter, new SyncCountOptions()); + } + + @Override + public Task count(Bson filter, SyncCountOptions options) { + return this.dispatcher.dispatchTask(new Callable() { + @Override + public Long call() throws Exception { + return proxy.count(filter, options); + } + }); + } + + @Override + public SyncAggregateIterable aggregate(List pipeline) { + return new SyncAggregateIterableImpl<>(this.proxy.aggregate(pipeline), dispatcher); + } + + @Override + public SyncAggregateIterable aggregate(List pipeline, + Class resultClass) { + return new SyncAggregateIterableImpl<>(this.proxy.aggregate(pipeline, resultClass), dispatcher); + } + @Override public SyncFindIterable find() { return new SyncFindIterableImpl<>(proxy.find(), dispatcher); @@ -111,52 +148,71 @@ public SyncFindIterable find(final Bson filter, } @Override - public Task findOneById(final BsonValue documentId) { - return this.dispatcher.dispatchTask(new Callable() { + public Task insertOneAndSync(DocumentT document) { + return this.dispatcher.dispatchTask(new Callable() { + @Override + public SyncInsertOneResult call() throws Exception { + return proxy.insertOneAndSync(document); + } + }); + } + + @Override + public Task insertManyAndSync(List documents) { + return this.dispatcher.dispatchTask(new Callable() { @Override - public DocumentT call() throws Exception { - return proxy.findOneById(documentId); + public SyncInsertManyResult call() throws Exception { + return proxy.insertManyAndSync(documents); } }); } @Override - public Task findOneById(final BsonValue documentId, - final Class resultClass) { - return this.dispatcher.dispatchTask(new Callable() { + public Task updateOne(Bson filter, Bson update) { + return this.updateOne(filter, update, new SyncUpdateOptions()); + } + + @Override + public Task updateOne(Bson filter, Bson update, SyncUpdateOptions updateOptions) { + return this.dispatcher.dispatchTask(new Callable() { @Override - public ResultT call() throws Exception { - return proxy.findOneById(documentId, resultClass); + public SyncUpdateResult call() throws Exception { + return proxy.updateOne(filter, update, updateOptions); } }); } @Override - public Task deleteOneById(final BsonValue documentId) { - return this.dispatcher.dispatchTask(new Callable() { + public Task updateMany(Bson filter, Bson update) { + return this.updateMany(filter, update, new SyncUpdateOptions()); + } + + @Override + public Task updateMany(Bson filter, Bson update, SyncUpdateOptions updateOptions) { + return this.dispatcher.dispatchTask(new Callable() { @Override - public RemoteDeleteResult call() throws Exception { - return proxy.deleteOneById(documentId); + public SyncUpdateResult call() throws Exception { + return proxy.updateMany(filter, update, updateOptions); } }); } @Override - public Task insertOneAndSync(final DocumentT document) { - return this.dispatcher.dispatchTask(new Callable() { + public Task deleteOne(Bson filter) { + return this.dispatcher.dispatchTask(new Callable() { @Override - public RemoteInsertOneResult call() throws Exception { - return proxy.insertOneAndSync(document); + public SyncDeleteResult call() throws Exception { + return proxy.deleteOne(filter); } }); } @Override - public Task updateOneById(final BsonValue documentId, final Bson update) { - return this.dispatcher.dispatchTask(new Callable() { + public Task deleteMany(Bson filter) { + return this.dispatcher.dispatchTask(new Callable() { @Override - public RemoteUpdateResult call() throws Exception { - return proxy.updateOneById(documentId, update); + public SyncDeleteResult call() throws Exception { + return proxy.deleteMany(filter); } }); } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteAggregateIterableImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteAggregateIterableImpl.java index 2b7c462e3..1f5de014e 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteAggregateIterableImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteAggregateIterableImpl.java @@ -38,6 +38,6 @@ class CoreRemoteAggregateIterableImpl } Operation> asOperation() { - return getOperations().aggregate(pipeline, geResultClass()); + return getOperations().aggregate(pipeline, getResultClass()); } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteFindIterableImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteFindIterableImpl.java index 301be9bec..c51169bff 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteFindIterableImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteFindIterableImpl.java @@ -95,7 +95,7 @@ public CoreRemoteFindIterableImpl sort(@Nullable final Bson @Override public ResultT first() { final Iterator iter = getOperations() - .findFirst(filter, geResultClass(), findOptions) + .findFirst(filter, getResultClass(), findOptions) .execute(getService()) .iterator(); return iter.hasNext() ? iter.next() : null; @@ -103,6 +103,6 @@ public ResultT first() { @Override Operation> asOperation() { - return getOperations().find(filter, geResultClass(), findOptions); + return getOperations().find(filter, getResultClass(), findOptions); } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoIterableImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoIterableImpl.java index 7c5cd2eed..b36ad0109 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoIterableImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoIterableImpl.java @@ -48,7 +48,7 @@ CoreStitchServiceClient getService() { return service; } - Class geResultClass() { + Class getResultClass() { return resultClass; } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java index b45b8710f..302fce83a 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java @@ -16,10 +16,7 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; - +import java.util.List; import java.util.Set; import javax.annotation.Nonnull; @@ -99,14 +96,38 @@ void configure(@Nonnull final ConflictHandler conflictHandler, boolean resumeSyncForDocument(final BsonValue documentId); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Counts the number of documents in the collection. + * + * @return the number of documents in the collection + */ + long count(); + + /** + * Counts the number of documents in the collection according to the given options. + * + * @param filter the query filter + * @return the number of documents in the collection + */ + long count(final Bson filter); + + /** + * Counts the number of documents in the collection according to the given options. + * + * @param filter the query filter + * @param options the options describing the count + * @return the number of documents in the collection + */ + long count(final Bson filter, final SyncCountOptions options); + + /** + * Finds all documents in the collection. * * @return the find iterable interface */ CoreSyncFindIterable find(); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection. * * @param resultClass the class to decode each document into * @param the target document type of the iterable. @@ -115,7 +136,7 @@ void configure(@Nonnull final ConflictHandler conflictHandler, CoreSyncFindIterable find(final Class resultClass); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection. * * @param filter the query filter * @return the find iterable interface @@ -123,59 +144,119 @@ void configure(@Nonnull final ConflictHandler conflictHandler, CoreSyncFindIterable find(final Bson filter); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection. * * @param filter the query filter * @param resultClass the class to decode each document into * @param the target document type of the iterable. * @return the find iterable interface */ - CoreSyncFindIterable find(final Bson filter, final Class resultClass); + CoreSyncFindIterable find( + final Bson filter, + final Class resultClass); + /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Aggregates documents according to the specified aggregation pipeline. * - * @param documentId the _id of the document to search for. - * @return a task containing the document if found locally or remotely. + * @param pipeline the aggregation pipeline + * @return an iterable containing the result of the aggregation operation */ - DocumentT findOneById(final BsonValue documentId); + CoreSyncAggregateIterable aggregate(final List pipeline); /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Aggregates documents according to the specified aggregation pipeline. * - * @param documentId the _id of the document to search for. + * @param pipeline the aggregation pipeline * @param resultClass the class to decode each document into * @param the target document type of the iterable. - * @return a task containing the document if found locally or remotely. + * @return an iterable containing the result of the aggregation operation + */ + CoreSyncAggregateIterable aggregate( + final List pipeline, + final Class resultClass); + + /** + * Inserts the provided document. If the document is missing an identifier, the client should + * generate one. + * + * @param document the document to insert + * @return the result of the insert one operation + */ + SyncInsertOneResult insertOneAndSync(final DocumentT document); + + /** + * Inserts one or more documents. + * + * @param documents the documents to insert + * @return the result of the insert many operation + */ + SyncInsertManyResult insertManyAndSync(final List documents); + + /** + * Removes at most one document from the collection that matches the given filter. If no + * documents match, the collection is not + * modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove one operation + */ + SyncDeleteResult deleteOne(final Bson filter); + + /** + * Removes all documents from the collection that match the given query filter. If no documents + * match, the collection is not modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove many operation + */ + SyncDeleteResult deleteMany(final Bson filter); + + /** + * Update a single document in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update one operation */ - ResultT findOneById(final BsonValue documentId, final Class resultClass); + SyncUpdateResult updateOne(final Bson filter, final Bson update); /** - * Updates a document by the given id. It is first searched for in the local synchronized cache - * and if not found and there is internet connectivity, it is searched for remotely. + * Update a single document in the collection according to the specified arguments. * - * @param documentId the _id of the document to search for. - * @param update the update specifier. - * @return a task containing the result of the local or remote update. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update one operation */ - RemoteUpdateResult updateOneById(final BsonValue documentId, final Bson update); + SyncUpdateResult updateOne( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions); /** - * Inserts a single document and begins to synchronize it. + * Update all documents in the collection according to the specified arguments. * - * @param document the document to insert and synchronize. - * @return the result of the insertion. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update many operation */ - RemoteInsertOneResult insertOneAndSync(final DocumentT document); + SyncUpdateResult updateMany(final Bson filter, final Bson update); /** - * Deletes a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Update all documents in the collection according to the specified arguments. * - * @param documentId the _id of the document to search for. - * @return a task containing the result of the local or remote update. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update many operation */ - RemoteDeleteResult deleteOneById(final BsonValue documentId); + SyncUpdateResult updateMany( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions); } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSyncAggregateIterable.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSyncAggregateIterable.java new file mode 100644 index 000000000..107045512 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSyncAggregateIterable.java @@ -0,0 +1,6 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync; + +import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteAggregateIterable; + +public interface CoreSyncAggregateIterable extends CoreRemoteAggregateIterable { +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncCountOptions.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncCountOptions.java new file mode 100644 index 000000000..1d6d59014 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncCountOptions.java @@ -0,0 +1,6 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync; + +import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; + +public class SyncCountOptions extends RemoteCountOptions { +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncDeleteResult.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncDeleteResult.java new file mode 100644 index 000000000..15a2750e0 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncDeleteResult.java @@ -0,0 +1,9 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync; + +import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; + +public class SyncDeleteResult extends RemoteDeleteResult { + public SyncDeleteResult(final long deletedCount) { + super(deletedCount); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertManyResult.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertManyResult.java new file mode 100644 index 000000000..4c8b9f0af --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertManyResult.java @@ -0,0 +1,13 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync; + +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; + +import org.bson.BsonValue; + +import java.util.Map; + +public class SyncInsertManyResult extends RemoteInsertManyResult { + public SyncInsertManyResult(final Map insertedIds) { + super(insertedIds); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertOneResult.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertOneResult.java new file mode 100644 index 000000000..c1e07ba1d --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertOneResult.java @@ -0,0 +1,11 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync; + +import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; + +import org.bson.BsonValue; + +public class SyncInsertOneResult extends RemoteInsertOneResult { + public SyncInsertOneResult(final BsonValue insertedId) { + super(insertedId); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateOptions.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateOptions.java new file mode 100644 index 000000000..e473748ca --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateOptions.java @@ -0,0 +1,6 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync; + +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions; + +public class SyncUpdateOptions extends RemoteUpdateOptions { +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateResult.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateResult.java new file mode 100644 index 000000000..0b08bba47 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateResult.java @@ -0,0 +1,15 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync; + +import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; + +import org.bson.BsonValue; + +public class SyncUpdateResult extends RemoteUpdateResult { + public SyncUpdateResult( + final long matchedCount, + final long modifiedCount, + final BsonValue upsertedId + ) { + super(matchedCount, modifiedCount, upsertedId); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/AggregateOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/AggregateOperation.java new file mode 100644 index 000000000..35e23eb78 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/AggregateOperation.java @@ -0,0 +1,41 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; + +import com.mongodb.MongoNamespace; +import com.mongodb.client.result.DeleteResult; +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; + +import org.bson.conversions.Bson; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import javax.annotation.Nullable; + +class AggregateOperation implements Operation> { + private final MongoNamespace namespace; + private final DataSynchronizer dataSynchronizer; + private final List pipeline; + private final Class resultClass; + + AggregateOperation( + final MongoNamespace namespace, + final DataSynchronizer dataSynchronizer, + final List pipeline, + final Class resultClass + ) { + this.namespace = namespace; + this.dataSynchronizer = dataSynchronizer; + this.pipeline = pipeline; + this.resultClass = resultClass; + } + + public Collection execute(@Nullable final CoreStitchServiceClient service) { + return this.dataSynchronizer.aggregate(namespace, pipeline, resultClass).into(new ArrayList<>()); + } +} + diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncAggregateIterableImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncAggregateIterableImpl.java new file mode 100644 index 000000000..59015c83e --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncAggregateIterableImpl.java @@ -0,0 +1,35 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; + +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteAggregateIterable; +import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoIterableImpl; +import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; +import com.mongodb.stitch.core.services.mongodb.remote.internal.Operations; +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; + +import org.bson.conversions.Bson; + +import java.util.Collection; +import java.util.List; + +class CoreSyncAggregateIterableImpl + extends CoreSyncMongoIterableImpl, ResultT> + implements CoreSyncAggregateIterable { + + private final List pipeline; + + CoreSyncAggregateIterableImpl( + final List pipeline, + final Class resultClass, + final CoreStitchServiceClient service, + final SyncOperations operations + ) { + super(service, resultClass, operations); + this.pipeline = pipeline; + } + + @Override + Operation> asOperation() { + return getOperations().aggregate(pipeline, getResultClass()); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncFindIterableImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncFindIterableImpl.java index d924f3156..b59afaad4 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncFindIterableImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncFindIterableImpl.java @@ -98,7 +98,7 @@ public CoreSyncFindIterableImpl sort(@Nullable final Bson so @Override public ResultT first() { final Iterator iter = getOperations() - .findFirst(filter, geResultClass(), findOptions) + .findFirst(filter, getResultClass(), findOptions) .execute(getService()) .iterator(); return iter.hasNext() ? iter.next() : null; @@ -106,6 +106,6 @@ public ResultT first() { @Override Operation> asOperation() { - return getOperations().find(filter, geResultClass(), findOptions); + return getOperations().find(filter, getResultClass(), findOptions); } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncImpl.java index 8d91c562c..c22c594b9 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncImpl.java @@ -17,16 +17,22 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; import com.mongodb.MongoNamespace; +import com.mongodb.client.model.CountOptions; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSync; +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncFindIterable; import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener; - +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult; + +import java.util.List; import java.util.Set; import javax.annotation.Nonnull; @@ -107,64 +113,83 @@ public boolean resumeSyncForDocument(final BsonValue documentId) { return this.dataSynchronizer.resumeSyncForDocument(namespace, documentId); } - /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. - * - * @param documentId the _id of the document to search for. - * @return the document if found locally or remotely. - */ - @Nullable - public DocumentT findOneById(final BsonValue documentId) { - return findOneById(documentId, this.documentClass); + @Override + public long count() { + return count(new BsonDocument()); } - /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. - * - * @param documentId the _id of the document to search for. - * @param resultClass the class to decode each document into - * @param the target document type of the iterable. - * @return the document if found locally or remotely. - */ - @Nullable - public ResultT findOneById(final BsonValue documentId, - final Class resultClass) { - return syncOperations.findOneById(documentId, resultClass).execute(service); + @Override + public long count(final Bson filter) { + return count(filter, new SyncCountOptions()); } - /** - * Updates a document by the given id. It is first searched for in the local synchronized cache - * and if not found and there is internet connectivity, it is searched for remotely. - * - * @param documentId the _id of the document to search for. - * @param update the update specifier. - * @return the result of the local or remote update. - */ - public RemoteUpdateResult updateOneById(final BsonValue documentId, final Bson update) { - return syncOperations.updateOneById(documentId, update).execute(service); + @Override + public long count(final Bson filter, final SyncCountOptions options) { + CountOptions countOptions = new CountOptions().limit(options.getLimit()); + return this.dataSynchronizer.count(this.namespace, filter, countOptions); } - /** - * Inserts a single document and begins to synchronize it. - * - * @param document the document to insert and synchronize. - * @return the result of the insertion. - */ - public RemoteInsertOneResult insertOneAndSync(final DocumentT document) { + @Override + public CoreSyncAggregateIterable aggregate(final List pipeline) { + return this.aggregate(pipeline, this.documentClass); + } + + @Override + public CoreSyncAggregateIterable aggregate( + final List pipeline, + final Class resultClass + ) { + return new CoreSyncAggregateIterableImpl<>( + pipeline, resultClass, service, syncOperations + ); + } + + @Override + public SyncUpdateResult updateOne(final Bson filter, final Bson update) { + return this.updateOne(filter, update, new SyncUpdateOptions()); + } + + @Override + public SyncUpdateResult updateOne( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions + ) { + return syncOperations.updateOne(filter, update, updateOptions).execute(service); + } + + @Override + public SyncUpdateResult updateMany(final Bson filter, final Bson update) { + return this.updateMany(filter, update, new SyncUpdateOptions()); + } + + @Override + public SyncUpdateResult updateMany( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions + ) { + return this.syncOperations.updateMany(filter, update, updateOptions).execute(service); + } + + @Override + public SyncInsertOneResult insertOneAndSync(DocumentT document) { return syncOperations.insertOneAndSync(document).execute(service); } - /** - * Deletes a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. - * - * @param documentId the _id of the document to search for. - * @return the result of the local or remote update. - */ - public RemoteDeleteResult deleteOneById(final BsonValue documentId) { - return syncOperations.deleteOneById(documentId).execute(service); + @Override + public SyncInsertManyResult insertManyAndSync(List documents) { + return syncOperations.insertManyAndSync(documents).execute(service); + } + + @Override + public SyncDeleteResult deleteOne(Bson filter) { + return syncOperations.deleteOne(filter).execute(service); + } + + @Override + public SyncDeleteResult deleteMany(Bson filter) { + return syncOperations.deleteMany(filter).execute(service); } @Override diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncMongoIterableImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncMongoIterableImpl.java index 0c75c18ae..34947235c 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncMongoIterableImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncMongoIterableImpl.java @@ -53,7 +53,7 @@ CoreStitchServiceClient getService() { return service; } - Class geResultClass() { + Class getResultClass() { return resultClass; } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 03401ccbc..8aa4c0e64 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -21,16 +21,22 @@ import static com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent.changeEventForLocalReplace; import static com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent.changeEventForLocalUpdate; +import com.mongodb.Block; +import com.mongodb.Function; import com.mongodb.MongoClientSettings; import com.mongodb.MongoNamespace; +import com.mongodb.client.AggregateIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CountOptions; import com.mongodb.client.model.FindOneAndReplaceOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.ReturnDocument; +import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; +import com.mongodb.lang.NonNull; import com.mongodb.stitch.core.StitchServiceErrorCode; import com.mongodb.stitch.core.StitchServiceException; import com.mongodb.stitch.core.internal.common.AuthMonitor; @@ -49,10 +55,10 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -71,6 +77,7 @@ import org.bson.codecs.Codec; import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.configuration.CodecRegistry; +import org.bson.conversions.Bson; import org.bson.diagnostics.Logger; import org.bson.diagnostics.Loggers; @@ -83,7 +90,6 @@ // TODO: Threading model okay? // TODO: implement unwatch // TODO: filter out and forbid usage of version ids outside of here -// TODO: findOneById with filter // TODO: Test delete/delete insert/insert update/update etc... // TODO: StitchReachabilityMonitor for when Stitch goes down and we can gracefully fail and give // you local only results. @@ -554,15 +560,15 @@ private void syncRemoteChangeEventToLocal( desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document with an unsupported synchronization protocol version " - + "%d; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document with an unsupported synchronization protocol version " + + "%d; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); return; } @@ -638,7 +644,7 @@ private void syncRemoteChangeEventToLocal( // iv. Otherwise, check if the version info of the incoming remote change event is different // from the version of the local document. final DocumentVersionInfo lastKnownLocalVersionInfo = DocumentVersionInfo - .getLocalVersionInfo(docConfig); + .getLocalVersionInfo(docConfig); // 1. If both the local document version and the remote change event version are empty, drop // the event. The absence of a version is effectively a version, and the pending write will @@ -660,12 +666,12 @@ private void syncRemoteChangeEventToLocal( // adhering to the mobile sync protocol. if (!lastKnownLocalVersionInfo.hasVersion() || !currentRemoteVersionInfo.hasVersion()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " - + "empty version but a write is pending; waiting for next L2R pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " + + "empty version but a write is pending; waiting for next L2R pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict(nsConfig.getNamespace(), docConfig, remoteChangeEvent); return; } @@ -680,28 +686,28 @@ private void syncRemoteChangeEventToLocal( // i. drop the event if the version counter of the remote event less than or equal to the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " - + "is stale; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " + + "is stale; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } else { // ii. raise a conflict if the version counter of the remote event is greater than the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "has higher counter than local version but a write is pending; " - + "raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "has higher counter than local version but a write is pending; " + + "raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); return; } } @@ -710,27 +716,27 @@ private void syncRemoteChangeEventToLocal( // fetch the latest version (this is to guard against the case where the unprocessed // change event is stale). final BsonDocument newestRemoteDocument = this.getRemoteCollection(nsConfig.getNamespace()) - .find(new Document("_id", docConfig.getDocumentId())).first(); + .find(new Document("_id", docConfig.getDocumentId())).first(); if (newestRemoteDocument == null) { // i. If the document is not found with a remote lookup, this means the document was // deleted remotely, so raise a conflict using a synthesized delete event as the remote // change event. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "stale and latest document lookup indicates a remote delete occurred, but " - + "a write is pending; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "stale and latest document lookup indicates a remote delete occurred, but " + + "a write is pending; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( + nsConfig.getNamespace(), + docConfig, + changeEventForLocalDelete( nsConfig.getNamespace(), - docConfig, - changeEventForLocalDelete( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - docConfig.hasUncommittedWrites())); + docConfig.getDocumentId(), + docConfig.hasUncommittedWrites())); return; } @@ -757,16 +763,16 @@ private void syncRemoteChangeEventToLocal( // to the GUID of the local document, drop the event. We’re believed to be behind in // the change stream at this point. if (newestRemoteVersionInfo.hasVersion() - && newestRemoteVersionInfo.getVersion().getInstanceId() - .equals(localVersion.instanceId)) { + && newestRemoteVersionInfo.getVersion().getInstanceId() + .equals(localVersion.instanceId)) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates that this is a stale event; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates that this is a stale event; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -776,21 +782,21 @@ private void syncRemoteChangeEventToLocal( // event. This means the remote document is a legitimately new document and we should // handle the conflict. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates a remote replace occurred, but a local write is pending; raising " - + "conflict with synthesized replace event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates a remote replace occurred, but a local write is pending; raising " + + "conflict with synthesized replace event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( + nsConfig.getNamespace(), + docConfig, + changeEventForLocalReplace( nsConfig.getNamespace(), - docConfig, - changeEventForLocalReplace( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - newestRemoteDocument, - docConfig.hasUncommittedWrites())); + docConfig.getDocumentId(), + newestRemoteDocument, + docConfig.hasUncommittedWrites())); } /** @@ -854,9 +860,9 @@ private void syncLocalToRemote() { // ii. Check if the internal remote change stream listener has an unprocessed event for // this document. final ChangeEvent unprocessedRemoteEvent = - instanceChangeStreamListener.getUnprocessedEventForDocumentId( - nsConfig.getNamespace(), - docConfig.getDocumentId()); + instanceChangeStreamListener.getUnprocessedEventForDocumentId( + nsConfig.getNamespace(), + docConfig.getDocumentId()); if (unprocessedRemoteEvent != null) { final DocumentVersionInfo unprocessedEventVersion; @@ -883,12 +889,12 @@ private void syncLocalToRemote() { if (!docConfig.hasCommittedVersion(unprocessedEventVersion)) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "unprocessed change event for document; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "unprocessed change event for document; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } // 2. Otherwise, the unprocessed event can be safely dropped and ignored in future R2L @@ -908,31 +914,31 @@ private void syncLocalToRemote() { // a. Insert document into remote database try { remoteColl.insertOne( - withNewVersion(localChangeEvent.getFullDocument(), nextVersion)); + withNewVersion(localChangeEvent.getFullDocument(), nextVersion)); } catch (final StitchServiceException ex) { // b. If an error happens: // i. That is not a duplicate key exception, report an error to the error listener. if (ex.getErrorCode() != StitchServiceErrorCode.MONGODB_ERROR - || !ex.getMessage().contains("E11000")) { + || !ex.getMessage().contains("E11000")) { this.emitError(docConfig, String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), ex); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), ex); continue; } // ii. Otherwise record that a conflict has occurred. logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s duplicate key exception on " - + "insert; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s duplicate key exception on " + + "insert; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); isConflicted = true; } break; @@ -943,12 +949,12 @@ private void syncLocalToRemote() { case REPLACE: { if (localDoc == null) { final IllegalStateException illegalStateException = new IllegalStateException( - "expected document to exist for local replace change event: %s"); + "expected document to exist for local replace change event: %s"); emitError( - docConfig, - illegalStateException.getMessage(), - illegalStateException + docConfig, + illegalStateException.getMessage(), + illegalStateException ); continue; } @@ -961,21 +967,21 @@ private void syncLocalToRemote() { final RemoteUpdateResult result; try { result = remoteColl.updateOne( - localVersionInfo.getFilter(), - nextDoc); + localVersionInfo.getFilter(), + nextDoc); } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. this.emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "replacing: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + "replacing: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -983,12 +989,12 @@ private void syncLocalToRemote() { if (result.getMatchedCount() == 0) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "replaced document or document deleted; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "replaced document or document deleted; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } break; } @@ -997,11 +1003,11 @@ private void syncLocalToRemote() { case UPDATE: { if (localDoc == null) { final IllegalStateException illegalStateException = new IllegalStateException( - "expected document to exist for local update change event"); + "expected document to exist for local update change event"); emitError( - docConfig, - illegalStateException.getMessage(), - illegalStateException + docConfig, + illegalStateException.getMessage(), + illegalStateException ); continue; } @@ -1015,7 +1021,7 @@ private void syncLocalToRemote() { if (!localChangeEvent.getUpdateDescription().getUpdatedFields().isEmpty()) { final BsonDocument sets = new BsonDocument(); for (final Map.Entry fieldValue : - localChangeEvent.getUpdateDescription().getUpdatedFields().entrySet()) { + localChangeEvent.getUpdateDescription().getUpdatedFields().entrySet()) { sets.put(fieldValue.getKey(), fieldValue.getValue()); } sets.put(DOCUMENT_VERSION_FIELD, nextVersion); @@ -1024,7 +1030,7 @@ private void syncLocalToRemote() { if (!localChangeEvent.getUpdateDescription().getRemovedFields().isEmpty()) { final BsonDocument unsets = new BsonDocument(); for (final String field : - localChangeEvent.getUpdateDescription().getRemovedFields()) { + localChangeEvent.getUpdateDescription().getRemovedFields()) { unsets.put(field, BsonBoolean.TRUE); } translatedUpdate.put("$unset", unsets); @@ -1033,21 +1039,21 @@ private void syncLocalToRemote() { final RemoteUpdateResult result; try { result = remoteColl.updateOne( - localVersionInfo.getFilter(), + localVersionInfo.getFilter(), translatedUpdate.isEmpty() ? nextDoc : translatedUpdate); } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "updating: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + "updating: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -1055,12 +1061,12 @@ private void syncLocalToRemote() { // c. If no documents are matched, record that a conflict has occurred. isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "updated document or document deleted; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "updated document or document deleted; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } break; } @@ -1075,16 +1081,16 @@ private void syncLocalToRemote() { } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + " deleting: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + " deleting: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -1095,12 +1101,12 @@ private void syncLocalToRemote() { if (remoteDocument != null) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "removed document; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "removed document; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } else { // d. Desynchronize the document if there is no conflict, or if fetching a remote // document after the conflict is raised returns no remote document. @@ -1114,15 +1120,15 @@ private void syncLocalToRemote() { default: emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s unknown operation " - + "type occurred on the document: %s; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - localChangeEvent.getOperationType().toString()) + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s unknown operation " + + "type occurred on the document: %s; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + localChangeEvent.getOperationType().toString()) ); continue; } @@ -1145,15 +1151,15 @@ private void syncLocalToRemote() { // TODO(STITCH-1972): This event may contain old version info. We should be filtering out // the version anyway from local and remote events. final ChangeEvent committedEvent = - docConfig.getLastUncommittedChangeEvent(); + docConfig.getLastUncommittedChangeEvent(); emitEvent(docConfig.getDocumentId(), new ChangeEvent<>( - committedEvent.getId(), - committedEvent.getOperationType(), - committedEvent.getFullDocument(), - committedEvent.getNamespace(), - committedEvent.getDocumentKey(), - committedEvent.getUpdateDescription(), - false)); + committedEvent.getId(), + committedEvent.getOperationType(), + committedEvent.getFullDocument(), + committedEvent.getNamespace(), + committedEvent.getDocumentKey(), + committedEvent.getUpdateDescription(), + false)); docConfig.setPendingWritesComplete(nextVersion); } else { @@ -1173,9 +1179,9 @@ private void syncLocalToRemote() { } resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); } } } @@ -1217,7 +1223,7 @@ public Object call() { this.logger.error(msg); this.logger.error( - String.format("Setting document %s to frozen", docConfig.getDocumentId())); + String.format("Setting document %s to frozen", docConfig.getDocumentId())); } @@ -1240,7 +1246,7 @@ private void resolveConflict( logger.warn(String.format( Locale.US, "t='%d': resolveConflict ns=%s documentId=%s no conflict resolver set; cannot " - + "resolve yet", + + "resolve yet", logicalT, namespace, docConfig.getDocumentId())); @@ -1348,8 +1354,7 @@ private void resolveConflict( // Update the document locally which will keep the pending writes but with // a new version next time around. - @SuppressWarnings("unchecked") - final BsonDocument docForStorage = + @SuppressWarnings("unchecked") final BsonDocument docForStorage = BsonUtils.documentToBsonDocument( resolvedDocument, syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); @@ -1357,7 +1362,7 @@ private void resolveConflict( logger.info(String.format( Locale.US, "t='%d': resolveConflict ns=%s documentId=%s replacing local with resolved document " - + "with remote version acknowledged: %s", + + "with remote version acknowledged: %s", logicalT, namespace, docConfig.getDocumentId(), @@ -1450,12 +1455,12 @@ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( * Queues up a callback to be removed and invoked on the next change event. */ public void addWatcher(final MongoNamespace namespace, - final Callback, Object> watcher) { + final Callback, Object> watcher) { instanceChangeStreamListener.addWatcher(namespace, watcher); } public void removeWatcher(final MongoNamespace namespace, - final Callback, Object> watcher) { + final Callback, Object> watcher) { instanceChangeStreamListener.removeWatcher(namespace, watcher); } @@ -1543,6 +1548,7 @@ public void desyncDocumentFromRemote( final BsonValue documentId ) { syncConfig.removeSynchronizedDocument(namespace, documentId); + getLocalCollection(namespace).deleteOne(getDocumentIdFilter(documentId)); triggerListeningToNamespace(namespace); } @@ -1550,13 +1556,13 @@ public void desyncDocumentFromRemote( * A document that is paused no longer has remote updates applied to it. * Any local updates to this document cause it to be resumed. An example of pausing a document * is when a conflict is being resolved for that document and the handler throws an exception. - * + *

* This method allows you to resume sync for a document. * - * @param namespace namespace for the document + * @param namespace namespace for the document * @param documentId the id of the document to resume syncing * @return true if successfully resumed, false if the document - * could not be found or there was an error resuming + * could not be found or there was an error resuming */ boolean resumeSyncForDocument( final MongoNamespace namespace, @@ -1578,6 +1584,36 @@ boolean resumeSyncForDocument( return !config.isPaused(); } + /** + * Counts the number of documents in the collection. + * + * @return the number of documents in the collection + */ + long count(final MongoNamespace namespace) { + return count(namespace, new BsonDocument()); + } + + /** + * Counts the number of documents in the collection according to the given options. + * + * @param filter the query filter + * @return the number of documents in the collection + */ + long count(final MongoNamespace namespace, final Bson filter) { + return count(namespace, filter, new CountOptions()); + } + + /** + * Counts the number of documents in the collection according to the given options. + * + * @param filter the query filter + * @param options the options describing the count + * @return the number of documents in the collection + */ + long count(final MongoNamespace namespace, final Bson filter, final CountOptions options) { + return getLocalCollection(namespace).countDocuments(filter, options); + } + public Collection find( final MongoNamespace namespace, final BsonDocument filter, @@ -1587,14 +1623,8 @@ public Collection find( final Class resultClass, final CodecRegistry codecRegistry ) { - // TODO: lock down ids - final Set syncedIds = getSynchronizedDocumentIds(namespace); - final BsonDocument finalFilter = new BsonDocument("$and", new BsonArray(Arrays.asList( - new BsonDocument("_id", new BsonDocument("$in", new BsonArray(new ArrayList<>(syncedIds)))), - filter - ))); return getLocalCollection(namespace, resultClass, codecRegistry) - .find(finalFilter) + .find(filter) .limit(limit) .projection(projection) .sort(sort) @@ -1602,29 +1632,30 @@ public Collection find( } /** - * Finds a single synchronized document by the given _id. If the document is not being - * synchronized or has not yet been found remotely, null will be returned. + * Aggregates documents according to the specified aggregation pipeline. * - * @param namespace the namespace to search for the document in. - * @param documentId the _id of the document. - * @param resultClass the {@link Class} that represents this document in the collection. - * @param codecRegistry the {@link CodecRegistry} that contains a codec for resultClass. - * @param the type of the document in the collection. - * @return the synchronized document if it exists; null otherwise. + * @param pipeline the aggregation pipeline + * @return an iterable containing the result of the aggregation operation */ - public T findOneById( + AggregateIterable aggregate( final MongoNamespace namespace, - final BsonValue documentId, - final Class resultClass, - final CodecRegistry codecRegistry - ) { - // TODO: lock down id - if (!syncConfig.isDocumentSynchronized(namespace, documentId)) { - return null; - } + final List pipeline) { + return aggregate(namespace, pipeline, BsonDocument.class); + } - final BsonDocument filter = new BsonDocument("_id", documentId); - return getLocalCollection(namespace, resultClass, codecRegistry).find(filter).first(); + /** + * Aggregates documents according to the specified aggregation pipeline. + * + * @param pipeline the aggregation pipeline + * @param resultClass the class to decode each document into + * @param the target document type of the iterable. + * @return an iterable containing the result of the aggregation operation + */ + AggregateIterable aggregate( + final MongoNamespace namespace, + final List pipeline, + final Class resultClass) { + return getLocalCollection(namespace).aggregate(pipeline, resultClass); } /** @@ -1634,73 +1665,186 @@ public T findOneById( * @param namespace the namespace to put the document in. * @param document the document to insert. */ - public void insertOneAndSync( - final MongoNamespace namespace, - final BsonDocument document - ) { + void insertOneAndSync(final MongoNamespace namespace, final BsonDocument document) { getLocalCollection(namespace).insertOne(document); - final ChangeEvent event = - changeEventForLocalInsert(namespace, document, true); + final BsonValue documentId = BsonUtils.getDocumentId(document); + final ChangeEvent event = changeEventForLocalInsert(namespace, document, true); final CoreDocumentSynchronizationConfig config = syncConfig.addSynchronizedDocument( namespace, - BsonUtils.getDocumentId(document) + documentId ); config.setSomePendingWrites(logicalT, event); - final BsonValue documentId = BsonUtils.getDocumentId(document); triggerListeningToNamespace(namespace); emitEvent(documentId, event); } /** - * Updates a single synchronized document by its given id with the given update specifiers. - * No update will occur if the _id is not being synchronized. + * Inserts one or more documents. * - * @param namespace the namespace where the document lives. - * @param documentId the _id of the document. - * @param update the update modifiers. - * @return the result of the update. + * @param documents the documents to insert */ - public UpdateResult updateOneById( - final MongoNamespace namespace, - final BsonValue documentId, - final BsonDocument update - ) { - // TODO: lock down id - final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); - if (config == null) { - return UpdateResult.acknowledged(0, 0L, null); + void insertManyAndSync(final MongoNamespace namespace, + final List documents) { + getLocalCollection(namespace).insertMany(documents); + for (final BsonDocument document : documents) { + final BsonValue documentId = BsonUtils.getDocumentId(document); + final ChangeEvent event = changeEventForLocalInsert(namespace, document, true); + final CoreDocumentSynchronizationConfig config = syncConfig.addSynchronizedDocument( + namespace, + documentId + ); + config.setSomePendingWrites(logicalT, event); + emitEvent(documentId, event); } + triggerListeningToNamespace(namespace); + } - // TODO: STITCH-1958 - final BsonDocument documentBeforeUpdate = - getLocalCollection(namespace).find(getDocumentIdFilter(documentId)).first(); + /** + * Update a single document in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update one operation + */ + UpdateResult updateOne(final MongoNamespace namespace, final Bson filter, final Bson update) { + return updateOne(namespace, filter, update, new UpdateOptions()); + } - final BsonDocument result = getLocalCollection(namespace) - .findOneAndUpdate( - getDocumentIdFilter(documentId), - update, - new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER)); + /** + * Update a single document in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update one operation + */ + UpdateResult updateOne( + final MongoNamespace namespace, + final Bson filter, + final Bson update, + final UpdateOptions updateOptions) { + final MongoCollection localCollection = getLocalCollection(namespace); + + final BsonDocument documentBeforeUpdate = getLocalCollection(namespace).find(filter).first(); - if (result == null) { + if (!updateOptions.isUpsert() && documentBeforeUpdate == null) { return UpdateResult.acknowledged(0, 0L, null); } - final ChangeEvent event = - changeEventForLocalUpdate( - namespace, - documentId, - ChangeEvent.UpdateDescription.diff(documentBeforeUpdate, result), - result, - true); + final BsonDocument documentAfterUpdate = localCollection.findOneAndUpdate( + filter, + update, + new FindOneAndUpdateOptions() + .collation(updateOptions.getCollation()) + .upsert(updateOptions.isUpsert()) + .bypassDocumentValidation(updateOptions.getBypassDocumentValidation()) + .arrayFilters(updateOptions.getArrayFilters()) + .returnDocument(ReturnDocument.AFTER)); + + if (documentAfterUpdate == null) { + return UpdateResult.acknowledged(0, 0L, null); + } - config.setSomePendingWrites( - logicalT, - event); + final ChangeEvent event; + final CoreDocumentSynchronizationConfig config; + final BsonValue documentId = BsonUtils.getDocumentId(documentAfterUpdate); + + if (documentBeforeUpdate == null && updateOptions.isUpsert()) { + config = syncConfig.addSynchronizedDocument(namespace, documentId); + event = changeEventForLocalInsert(namespace, documentAfterUpdate, true); + } else { + config = syncConfig.getSynchronizedDocument(namespace, documentId); + event = changeEventForLocalUpdate( + namespace, + BsonUtils.getDocumentId(documentAfterUpdate), + ChangeEvent.UpdateDescription.diff(documentBeforeUpdate, documentAfterUpdate), + documentAfterUpdate, + true); + } + + config.setSomePendingWrites(logicalT, event); emitEvent(documentId, event); return UpdateResult.acknowledged(1, 1L, null); } + /** + * Update all documents in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update many operation + */ + UpdateResult updateMany(final MongoNamespace namespace, + final Bson filter, + final Bson update) { + return updateMany(namespace, filter, update, new UpdateOptions()); + } + + /** + * Update all documents in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update many operation + */ + UpdateResult updateMany( + final MongoNamespace namespace, + final Bson filter, + final Bson update, + final UpdateOptions updateOptions) { + Map idToBeforeDocumentMap = new HashMap<>(); + this.getLocalCollection(namespace) + .find(filter) + .forEach(new Block() { + @Override + public void apply(@NonNull final BsonDocument bsonDocument) { + idToBeforeDocumentMap.put(BsonUtils.getDocumentId(bsonDocument), bsonDocument); + } + }); + + final UpdateResult result = this.getLocalCollection(namespace) + .updateMany(filter, update, updateOptions); + + this.getLocalCollection(namespace).find(filter).forEach(new Block() { + @Override + public void apply(@NonNull final BsonDocument afterDocument) { + final BsonDocument beforeDocument; + final BsonValue documentId = BsonUtils.getDocumentId(afterDocument); + + if ((beforeDocument = idToBeforeDocumentMap.get(documentId)) == null + && !updateOptions.isUpsert()) { + return; + } + + final CoreDocumentSynchronizationConfig config; + final ChangeEvent event; + + if (beforeDocument == null && updateOptions.isUpsert()) { + config = syncConfig.addSynchronizedDocument(namespace, documentId); + event = changeEventForLocalInsert(namespace, afterDocument, true); + } else { + config = syncConfig.getSynchronizedDocument(namespace, documentId); + event = changeEventForLocalUpdate( + namespace, + documentId, + ChangeEvent.UpdateDescription.diff(beforeDocument, afterDocument), + afterDocument, + true); + } + + config.setSomePendingWrites(logicalT, event); + emitEvent(documentId, event); + } + }); + + return result; + } + /** * Replaces a single synchronized document by its given id with the given full document * replacement. No replacement will occur if the _id is not being synchronized. @@ -1779,18 +1923,24 @@ private void replaceOrUpsertOneFromRemote( } /** - * Deletes a single synchronized document by its given id. No deletion will occur if the _id is - * not being synchronized. + * Removes at most one document from the collection that matches the given filter. If no + * documents match, the collection is not + * modified. * - * @param namespace the namespace where the document lives. - * @param documentId the _id of the document. - * @return the result of the deletion. + * @param filter the query filter to apply the the delete operation + * @return the result of the remove one operation */ - public DeleteResult deleteOneById( - final MongoNamespace namespace, - final BsonValue documentId - ) { - // TODO: lock down id + DeleteResult deleteOne(final MongoNamespace namespace, final Bson filter) { + final MongoCollection localCollection = getLocalCollection(namespace); + final BsonDocument docToDelete = localCollection + .find(filter) + .first(); + + if (docToDelete == null) { + return DeleteResult.acknowledged(0); + } + + final BsonValue documentId = BsonUtils.getDocumentId(docToDelete); final CoreDocumentSynchronizationConfig config = syncConfig.getSynchronizedDocument(namespace, documentId); @@ -1798,10 +1948,8 @@ public DeleteResult deleteOneById( return DeleteResult.acknowledged(0); } - final DeleteResult result = getLocalCollection(namespace) - .deleteOne(getDocumentIdFilter(documentId)); - final ChangeEvent event = - changeEventForLocalDelete(namespace, documentId, true); + final DeleteResult result = getLocalCollection(namespace).deleteOne(filter); + final ChangeEvent event = changeEventForLocalDelete(namespace, documentId, true); // this block is to trigger coalescence for a delete after insert if (config.getLastUncommittedChangeEvent() != null @@ -1811,12 +1959,61 @@ public DeleteResult deleteOneById( return result; } - config.setSomePendingWrites( - logicalT, event); + config.setSomePendingWrites(logicalT, event); emitEvent(documentId, event); return result; } + /** + * Removes all documents from the collection that match the given query filter. If no documents + * match, the collection is not modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove many operation + */ + DeleteResult deleteMany(final MongoNamespace namespace, + final Bson filter) { + final MongoCollection localCollection = getLocalCollection(namespace); + final Set idsToDelete = + localCollection + .find(filter) + .map(new Function() { + @Override + @NonNull + public BsonValue apply(@NonNull BsonDocument bsonDocument) { + return BsonUtils.getDocumentId(bsonDocument); + } + }).into(new HashSet<>()); + + final DeleteResult result = getLocalCollection(namespace).deleteMany(filter); + + for (BsonValue documentId : idsToDelete) { + final CoreDocumentSynchronizationConfig config = + syncConfig.getSynchronizedDocument(namespace, documentId); + + if (config == null) { + continue; + } + + final ChangeEvent event = + changeEventForLocalDelete(namespace, documentId, true); + + // this block is to trigger coalescence for a delete after insert + if (config.getLastUncommittedChangeEvent() != null + && config.getLastUncommittedChangeEvent().getOperationType() + == ChangeEvent.OperationType.INSERT) { + desyncDocumentFromRemote(config.getNamespace(), config.getDocumentId()); + return result; + } + + config.setSomePendingWrites( + logicalT, event); + emitEvent(documentId, event); + } + + return result; + } + /** * Deletes a single synchronized document by its given id. No deletion will occur if the _id is * not being synchronized. @@ -2037,7 +2234,7 @@ private Set getLatestDocumentsForStaleFromRemote( private Set getDocumentIds(final Set documents) { final Set ids = new HashSet<>(); - for (final BsonDocument document: documents) { + for (final BsonDocument document : documents) { ids.add(document.get("_id")); } return ids; @@ -2056,13 +2253,13 @@ private static BsonDocument getDocumentIdFilter(final BsonValue documentId) { /** * Adds and returns a document with a new version to the given document. * - * @param document the document to attach a new version to. + * @param document the document to attach a new version to. * @param newVersion the version to attach to the document * @return a document with a new version to the given document. */ private static BsonDocument withNewVersion( - final BsonDocument document, - final BsonDocument newVersion + final BsonDocument document, + final BsonDocument newVersion ) { final BsonDocument newDocument = BsonUtils.copyOfDocument(document); newDocument.put(DOCUMENT_VERSION_FIELD, newVersion); diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteManyOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteManyOperation.java new file mode 100644 index 000000000..ba56a244b --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteManyOperation.java @@ -0,0 +1,32 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; + +import com.mongodb.MongoNamespace; +import com.mongodb.client.result.DeleteResult; +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; + +import org.bson.conversions.Bson; + +import javax.annotation.Nullable; + +class DeleteManyOperation implements Operation { + private final MongoNamespace namespace; + private final Bson filter; + private final DataSynchronizer dataSynchronizer; + + DeleteManyOperation( + final MongoNamespace namespace, + final Bson filter, + final DataSynchronizer dataSynchronizer + ) { + this.namespace = namespace; + this.filter = filter; + this.dataSynchronizer = dataSynchronizer; + } + + public SyncDeleteResult execute(@Nullable final CoreStitchServiceClient service) { + final DeleteResult localResult = this.dataSynchronizer.deleteMany(namespace, filter); + return new SyncDeleteResult(localResult.getDeletedCount()); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneByIdOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneOperation.java similarity index 70% rename from core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneByIdOperation.java rename to core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneOperation.java index 29d9290f5..e6978ac7c 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneByIdOperation.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneOperation.java @@ -21,31 +21,29 @@ import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; + import javax.annotation.Nullable; import org.bson.BsonValue; +import org.bson.conversions.Bson; -class DeleteOneByIdOperation implements Operation { - +class DeleteOneOperation implements Operation { private final MongoNamespace namespace; - private final BsonValue documentId; + private final Bson filter; private final DataSynchronizer dataSynchronizer; - DeleteOneByIdOperation( + DeleteOneOperation( final MongoNamespace namespace, - final BsonValue documentId, + final Bson filter, final DataSynchronizer dataSynchronizer ) { this.namespace = namespace; - this.documentId = documentId; + this.filter = filter; this.dataSynchronizer = dataSynchronizer; } - public RemoteDeleteResult execute(@Nullable final CoreStitchServiceClient service) { - final DeleteResult localResult = - this.dataSynchronizer.deleteOneById(namespace, documentId); - if (localResult.getDeletedCount() == 1) { - return new RemoteDeleteResult(localResult.getDeletedCount()); - } - return new RemoteDeleteResult(0); + public SyncDeleteResult execute(@Nullable final CoreStitchServiceClient service) { + final DeleteResult localResult = this.dataSynchronizer.deleteOne(namespace, filter); + return new SyncDeleteResult(localResult.getDeletedCount()); } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/FindOneByIdOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/FindOneByIdOperation.java deleted file mode 100644 index 42c624392..000000000 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/FindOneByIdOperation.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2018-present MongoDB, Inc. - * - * 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.mongodb.stitch.core.services.mongodb.remote.sync.internal; - -import com.mongodb.MongoNamespace; -import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; -import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; - -import javax.annotation.Nonnull; -import org.bson.BsonValue; - -class FindOneByIdOperation implements Operation { - - private final MongoNamespace namespace; - private final BsonValue documentId; - private final Class resultClass; - private final DataSynchronizer dataSynchronizer; - - FindOneByIdOperation( - final MongoNamespace namespace, - final BsonValue documentId, - final Class resultClass, - final DataSynchronizer dataSynchronizer - ) { - this.namespace = namespace; - this.documentId = documentId; - this.resultClass = resultClass; - this.dataSynchronizer = dataSynchronizer; - } - - public T execute(@Nonnull final CoreStitchServiceClient service) { - return this.dataSynchronizer.findOneById( - namespace, - documentId, - resultClass, - service.getCodecRegistry()); - } -} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertManyAndSyncOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertManyAndSyncOperation.java new file mode 100644 index 000000000..859772952 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertManyAndSyncOperation.java @@ -0,0 +1,42 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; + +import com.mongodb.MongoNamespace; +import com.mongodb.stitch.core.internal.common.BsonUtils; +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult; + +import org.bson.BsonDocument; +import org.bson.BsonValue; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; + +class InsertManyAndSyncOperation implements Operation { + + private final MongoNamespace namespace; + private final List documents; + private final DataSynchronizer dataSynchronizer; + + InsertManyAndSyncOperation( + final MongoNamespace namespace, + final List documents, + final DataSynchronizer dataSynchronizer + ) { + this.namespace = namespace; + this.documents = documents; + this.dataSynchronizer = dataSynchronizer; + } + + public SyncInsertManyResult execute(@Nullable final CoreStitchServiceClient service) { + this.dataSynchronizer.insertManyAndSync(namespace, documents); + final Map indexToId = new HashMap<>(); + for (int i = 0; i < this.documents.size(); i++) { + indexToId.put((long)i, BsonUtils.getDocumentId(this.documents.get(i))); + } + return new SyncInsertManyResult(indexToId); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertOneAndSyncOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertOneAndSyncOperation.java index e7e4a51d6..12aed8a96 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertOneAndSyncOperation.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertOneAndSyncOperation.java @@ -19,12 +19,13 @@ import com.mongodb.MongoNamespace; import com.mongodb.stitch.core.internal.common.BsonUtils; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult; + import javax.annotation.Nullable; import org.bson.BsonDocument; -class InsertOneAndSyncOperation implements Operation { +class InsertOneAndSyncOperation implements Operation { private final MongoNamespace namespace; private final BsonDocument document; @@ -40,8 +41,8 @@ class InsertOneAndSyncOperation implements Operation { this.dataSynchronizer = dataSynchronizer; } - public RemoteInsertOneResult execute(@Nullable final CoreStitchServiceClient service) { + public SyncInsertOneResult execute(@Nullable final CoreStitchServiceClient service) { this.dataSynchronizer.insertOneAndSync(namespace, document); - return new RemoteInsertOneResult(BsonUtils.getDocumentId(document)); + return new SyncInsertOneResult(BsonUtils.getDocumentId(document)); } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncOperations.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncOperations.java index b75538ace..31bad39da 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncOperations.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncOperations.java @@ -24,13 +24,16 @@ import com.mongodb.MongoNamespace; import com.mongodb.stitch.core.internal.common.BsonUtils; import com.mongodb.stitch.core.services.mongodb.remote.RemoteFindOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; import org.bson.BsonDocument; -import org.bson.BsonValue; import org.bson.codecs.CollectibleCodec; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; +import java.util.ArrayList; +import java.util.List; + public class SyncOperations { private final MongoNamespace namespace; @@ -66,8 +69,6 @@ SyncFindOperation find( return createSyncFindOperation(namespace, filter, resultClass, options); } - - private SyncFindOperation createSyncFindOperation( final MongoNamespace findNamespace, final Bson filter, @@ -93,32 +94,47 @@ private SyncFindOperation createSyncFindOperation( .sort(sortDoc); } - FindOneByIdOperation findOneById( - final BsonValue documentId, - final Class resultClass + /** + * Aggregates documents according to the specified aggregation pipeline. + * + * @param pipeline the aggregation pipeline + * @param resultClass the class to decode each document into + * @param the target document type of the iterable. + * @return an iterable containing the result of the aggregation operation + */ + AggregateOperation aggregate( + final List pipeline, + final Class resultClass) { + return new AggregateOperation<>(namespace, dataSynchronizer, pipeline, resultClass); + } + + UpdateOneOperation updateOne( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions ) { - notNull("documentId", documentId); - return new FindOneByIdOperation<>( + return new UpdateOneOperation( namespace, - documentId, - resultClass, - dataSynchronizer); + filter, + toBsonDocument(update, documentClass, codecRegistry), + dataSynchronizer, + updateOptions); } - UpdateOneByIdOperation updateOneById( - final BsonValue documentId, - final Bson update + UpdateManyOperation updateMany( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions ) { - return new UpdateOneByIdOperation<>( + return new UpdateManyOperation( namespace, - documentId, + filter, toBsonDocument(update, documentClass, codecRegistry), - dataSynchronizer); + dataSynchronizer, + updateOptions); } - public InsertOneAndSyncOperation insertOneAndSync( - final DocumentT document - ) { + public InsertOneAndSyncOperation insertOneAndSync(final DocumentT document) { notNull("document", document); final DocumentT docToInsert; if (getCodec(codecRegistry, documentClass) instanceof CollectibleCodec) { @@ -128,16 +144,46 @@ public InsertOneAndSyncOperation insertOneAndSync( } else { docToInsert = document; } - return new InsertOneAndSyncOperation<>( + + return new InsertOneAndSyncOperation( namespace, documentToBsonDocument(docToInsert, codecRegistry), dataSynchronizer); } - DeleteOneByIdOperation deleteOneById(final BsonValue documentId) { - return new DeleteOneByIdOperation( + InsertManyAndSyncOperation insertManyAndSync(final List documents) { + final List bsonDocuments = new ArrayList<>(); + for (final DocumentT document : documents) { + if (getCodec(codecRegistry, documentClass) instanceof CollectibleCodec) { + bsonDocuments.add( + documentToBsonDocument( + ((CollectibleCodec) getCodec(codecRegistry, documentClass)) + .generateIdIfAbsentFromDocument(document), + codecRegistry + ) + ); + } else { + bsonDocuments.add(documentToBsonDocument(document, codecRegistry)); + } + } + + return new InsertManyAndSyncOperation( + namespace, + bsonDocuments, + dataSynchronizer); + } + + DeleteOneOperation deleteOne(final Bson filter) { + return new DeleteOneOperation( + namespace, + filter, + dataSynchronizer); + } + + DeleteManyOperation deleteMany(final Bson filter) { + return new DeleteManyOperation( namespace, - documentId, + filter, dataSynchronizer); } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateManyOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateManyOperation.java new file mode 100644 index 000000000..677dbf147 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateManyOperation.java @@ -0,0 +1,51 @@ +package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; + +import com.mongodb.MongoNamespace; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult; + +import org.bson.BsonDocument; +import org.bson.conversions.Bson; + +import javax.annotation.Nullable; + +class UpdateManyOperation implements Operation { + + private final MongoNamespace namespace; + private final Bson filter; + private final BsonDocument update; + private final DataSynchronizer dataSynchronizer; + private final SyncUpdateOptions syncUpdateOptions; + + UpdateManyOperation( + final MongoNamespace namespace, + final Bson filter, + final BsonDocument update, + final DataSynchronizer dataSynchronizer, + final SyncUpdateOptions syncUpdateOptions + ) { + this.namespace = namespace; + this.filter = filter; + this.update = update; + this.dataSynchronizer = dataSynchronizer; + this.syncUpdateOptions = syncUpdateOptions; + } + + public SyncUpdateResult execute(@Nullable final CoreStitchServiceClient service) { + final UpdateResult localResult = this.dataSynchronizer.updateMany( + namespace, + filter, + update, + new UpdateOptions().upsert(this.syncUpdateOptions.isUpsert())); + + return new SyncUpdateResult( + localResult.getMatchedCount(), + localResult.getModifiedCount(), + localResult.getUpsertedId() + ); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateOneByIdOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateOneOperation.java similarity index 56% rename from core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateOneByIdOperation.java rename to core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateOneOperation.java index de85459e9..95c088e43 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateOneByIdOperation.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateOneOperation.java @@ -17,43 +17,50 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; import com.mongodb.MongoNamespace; +import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.result.UpdateResult; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult; + import javax.annotation.Nullable; import org.bson.BsonDocument; -import org.bson.BsonValue; +import org.bson.conversions.Bson; -class UpdateOneByIdOperation implements Operation { +class UpdateOneOperation implements Operation { private final MongoNamespace namespace; - private final BsonValue documentId; + private final Bson filter; private final BsonDocument update; private final DataSynchronizer dataSynchronizer; + private final SyncUpdateOptions syncUpdateOptions; - UpdateOneByIdOperation( + UpdateOneOperation( final MongoNamespace namespace, - final BsonValue documentId, + final Bson filter, final BsonDocument update, - final DataSynchronizer dataSynchronizer + final DataSynchronizer dataSynchronizer, + final SyncUpdateOptions syncUpdateOptions ) { this.namespace = namespace; - this.documentId = documentId; + this.filter = filter; this.update = update; this.dataSynchronizer = dataSynchronizer; + this.syncUpdateOptions = syncUpdateOptions; } - public RemoteUpdateResult execute(@Nullable final CoreStitchServiceClient service) { - final UpdateResult localResult = - this.dataSynchronizer.updateOneById(namespace, documentId, update); - if (localResult.getMatchedCount() == 1) { - return new RemoteUpdateResult( - localResult.getMatchedCount(), - localResult.getModifiedCount(), - null); - } - - return new RemoteUpdateResult(0, 0, null); + public SyncUpdateResult execute(@Nullable final CoreStitchServiceClient service) { + final UpdateResult localResult = this.dataSynchronizer.updateOne( + namespace, + filter, + update, + new UpdateOptions().upsert(this.syncUpdateOptions.isUpsert())); + + return new SyncUpdateResult( + localResult.getMatchedCount(), + localResult.getModifiedCount(), + localResult.getUpsertedId() + ); } } diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionUnitTests.java b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionUnitTests.java index 1c4a767ca..103b37620 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionUnitTests.java +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionUnitTests.java @@ -355,7 +355,7 @@ public void testInsertOne() { funcArgsArg.capture(), resultClassArg.capture()); - assertEquals("insertMany", funcNameArg.getValue()); + assertEquals("insertManyAndSync", funcNameArg.getValue()); assertEquals(1, funcArgsArg.getValue().size()); final Document expectedArgs = new Document(); expectedArgs.put("database", "dbName1"); diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt index 9df196a9e..a49a2acca 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt @@ -13,6 +13,8 @@ import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatcher +import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.times @@ -116,50 +118,29 @@ class CoreSyncUnitTests { compareRemoteFindOptions(expectedRemoteFindOptions, remoteFindCaptor.value) } - @Test - fun testFindOneById() { - val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) - - assertNull(coreSync.findOneById(ctx.testDocumentId)) - - ctx.insertTestDocument() - - assertEquals( - ctx.testDocument, - SyncUnitTestHarness.withoutSyncVersion(coreSync.findOneById(ctx.testDocumentId))) - - verify(syncOperations, times(2)).findOneById( - eq(ctx.testDocumentId), eq(BsonDocument::class.java)) - - verify(ctx.dataSynchronizer, times(2)).findOneById( - eq(ctx.namespace), eq(ctx.testDocumentId), eq(BsonDocument::class.java), any() - ) - } - @Test fun testUpdateOneById() { val ctx = harness.freshTestContext() val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) - var result = coreSync.updateOneById(ctx.testDocumentId, ctx.updateDocument) + var result = coreSync.updateOne(ctx.testDocumentFilter, ctx.updateDocument) assertEquals(0, result.matchedCount) assertEquals(0, result.modifiedCount) assertNull(result.upsertedId) ctx.insertTestDocument() - result = coreSync.updateOneById(ctx.testDocumentId, ctx.updateDocument) + result = coreSync.updateOne(ctx.testDocumentFilter, ctx.updateDocument) assertEquals(1, result.matchedCount) assertEquals(1, result.modifiedCount) assertNull(result.upsertedId) - verify(syncOperations, times(2)).updateOneById( - eq(ctx.testDocumentId), eq(ctx.updateDocument)) + verify(syncOperations, times(2)).updateOne( + eq(ctx.testDocumentFilter), eq(ctx.updateDocument), any()) - verify(ctx.dataSynchronizer, times(2)).updateOneById( - eq(ctx.namespace), eq(ctx.testDocumentId), eq(ctx.updateDocument)) + verify(ctx.dataSynchronizer, times(2)).updateOne( + eq(ctx.namespace), eq(ctx.testDocumentFilter), eq(ctx.updateDocument), any()) } @Test @@ -191,20 +172,20 @@ class CoreSyncUnitTests { val ctx = harness.freshTestContext() val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) - var deleteResult = coreSync.deleteOneById(ctx.testDocumentId) + var deleteResult = coreSync.deleteOne(ctx.testDocumentFilter) assertEquals(0, deleteResult.deletedCount) ctx.insertTestDocument() - deleteResult = coreSync.deleteOneById(ctx.testDocumentId) + deleteResult = coreSync.deleteOne(ctx.testDocumentFilter) assertEquals(1, deleteResult.deletedCount) - verify(syncOperations, times(2)).deleteOneById( - eq(ctx.testDocumentId)) + verify(syncOperations, times(2)).deleteOne( + eq(ctx.testDocumentFilter)) - verify(ctx.dataSynchronizer, times(2)).deleteOneById( - eq(ctx.namespace), eq(ctx.testDocumentId)) + verify(ctx.dataSynchronizer, times(2)).deleteOne( + eq(ctx.namespace), eq(ctx.testDocumentFilter)) } } diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt index f044ccbab..bd4d927d9 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt @@ -25,6 +25,7 @@ interface DataSynchronizerTestContext : Closeable { val namespace: MongoNamespace val testDocument: BsonDocument val testDocumentId: BsonValue + val testDocumentFilter: BsonDocument var updateDocument: BsonDocument val collectionMock: CoreRemoteMongoCollectionImpl @@ -62,7 +63,7 @@ interface DataSynchronizerTestContext : Closeable { /** * Wait for an event to be emitted. */ - fun waitForEvent() + fun waitForEvents(amount: Int = 1) /** * Reconfigure dataSynchronizer. Insert the contextual test document. @@ -93,7 +94,7 @@ interface DataSynchronizerTestContext : Closeable { /** * Verify the changeEventListener was called for the test document. */ - fun verifyChangeEventListenerCalledForActiveDoc(times: Int, expectedChangeEvent: ChangeEvent? = null) + fun verifyChangeEventListenerCalledForActiveDoc(times: Int, vararg expectedChangeEvents: ChangeEvent = arrayOf()) /** * Verify the errorListener was called for the test document. diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt index b490c1225..d1033d25b 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt @@ -1,5 +1,6 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal +import com.mongodb.operation.UpdateOperation import com.mongodb.stitch.core.StitchServiceErrorCode import com.mongodb.stitch.core.StitchServiceException import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult @@ -8,6 +9,9 @@ import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.SyncUnitTes import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory import org.bson.BsonDocument import org.bson.BsonInt32 +import org.bson.BsonString +import org.bson.codecs.BsonDocumentCodec +import org.bson.codecs.configuration.CodecRegistries import org.junit.After import org.junit.Assert.assertEquals @@ -52,15 +56,18 @@ class DataSynchronizerUnitTests { if (shouldWaitForError) { ctx.waitForError() } else { - ctx.waitForEvent() + ctx.waitForEvents() } val expectedChangeEvent = if (shouldConflictBeResolvedByRemote) ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false) else ChangeEvent.changeEventForLocalInsert(ctx.namespace, expectedDocument, true) + + val expectedChangeEvents = if (shouldWaitForError) emptyArray>() else arrayOf(expectedChangeEvent) + ctx.verifyChangeEventListenerCalledForActiveDoc( - times = if (shouldWaitForError) 0 else 1, - expectedChangeEvent = if (shouldWaitForError) null else expectedChangeEvent) + if (shouldWaitForError) 0 else 1, + *expectedChangeEvents) ctx.verifyConflictHandlerCalledForActiveDoc(times = 1) ctx.verifyErrorListenerCalledForActiveDoc(times = if (shouldWaitForError) 1 else 0, error = if (shouldWaitForError) ctx.exceptionToThrowDuringConflict else null) @@ -119,18 +126,18 @@ class DataSynchronizerUnitTests { // insert the doc, wait, sync, and assert that the expected change events are emitted ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( - times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalInsert( + 1, + ChangeEvent.changeEventForLocalInsert( ctx.namespace, ctx.testDocument, true)) ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( - times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalInsert( + 1, + ChangeEvent.changeEventForLocalInsert( ctx.namespace, ctx.testDocument, false)) @@ -161,10 +168,10 @@ class DataSynchronizerUnitTests { // sync and assert that the conflict handler was called, // accepting the remote delete, nullifying the document ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( - times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) + 1, + ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) ctx.verifyConflictHandlerCalledForActiveDoc( times = 1, expectedLocalConflictEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true), @@ -182,10 +189,10 @@ class DataSynchronizerUnitTests { // assert that the local doc has been inserted ctx.shouldConflictBeResolvedByRemote = false ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( - times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) + 1, + ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) ctx.verifyConflictHandlerCalledForActiveDoc( times = 1, expectedLocalConflictEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true), @@ -238,7 +245,7 @@ class DataSynchronizerUnitTests { // insert the document, prepare for an error ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() // sync, verifying that the expected exceptionToThrow was emitted, pausing the document ctx.doSyncPass() @@ -264,10 +271,10 @@ class DataSynchronizerUnitTests { ctx.mockUpdateResult(RemoteUpdateResult(1, 1, null)) ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( - times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalInsert( + 1, + ChangeEvent.changeEventForLocalInsert( ctx.namespace, expectedDocument, false)) ctx.verifyConflictHandlerCalledForActiveDoc(times = 0) ctx.verifyErrorListenerCalledForActiveDoc(times = 0) @@ -286,12 +293,12 @@ class DataSynchronizerUnitTests { // do a sync pass, addressing the conflict ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() // verify that a change event has been emitted. the conflict will have been handled // in setupPendingReplace ctx.verifyChangeEventListenerCalledForActiveDoc( - times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalInsert( + 1, + ChangeEvent.changeEventForLocalInsert( ctx.namespace, expectedDoc, false )) ctx.verifyConflictHandlerCalledForActiveDoc(times = 0) @@ -358,15 +365,15 @@ class DataSynchronizerUnitTests { // insert, sync the doc, update, and verify that the change event was emitted ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.doSyncPass() - ctx.waitForEvent() - ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + ctx.waitForEvents() + ctx.verifyChangeEventListenerCalledForActiveDoc(1, + ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) ctx.updateTestDocument() - ctx.waitForEvent() - ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalUpdate( + ctx.waitForEvents() + ctx.verifyChangeEventListenerCalledForActiveDoc(1, + ChangeEvent.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), @@ -378,8 +385,8 @@ class DataSynchronizerUnitTests { // was of the correct doc, and that no conflicts or errors occured ctx.mockUpdateResult(RemoteUpdateResult(1, 1, null)) ctx.doSyncPass() - ctx.waitForEvent() - ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, expectedChangeEvent = ChangeEvent.changeEventForLocalUpdate( + ctx.waitForEvents() + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), @@ -424,22 +431,21 @@ class DataSynchronizerUnitTests { // 1: Update -> Conflict -> Delete (remote wins) // insert a new document, and sync. ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.doSyncPass() // update the document and wait for the local update event ctx.updateTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, - expectedChangeEvent = expectedLocalEvent) + ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedLocalEvent) // create conflict here by claiming there is no remote doc to update ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) // do a sync pass, addressing the conflict ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() // verify that a change event has been emitted, a conflict has been handled, // and no errors were emitted ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1) @@ -463,27 +469,28 @@ class DataSynchronizerUnitTests { ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.doSyncPass() - ctx.waitForEvent() - ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, expectedChangeEvent = - ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + ctx.waitForEvents() + ctx.verifyChangeEventListenerCalledForActiveDoc( + 1, + ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) // update the document and wait for the local update event ctx.updateTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() // do a sync pass, addressing the conflict. let local win ctx.shouldConflictBeResolvedByRemote = false ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() // verify that a change event has been emitted, a conflict has been handled, // and no errors were emitted ctx.verifyChangeEventListenerCalledForActiveDoc( - times = 1, - expectedChangeEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, docAfterUpdate, true)) + 1, + ChangeEvent.changeEventForLocalInsert(ctx.namespace, docAfterUpdate, true)) ctx.verifyConflictHandlerCalledForActiveDoc(1, expectedLocalEvent, expectedRemoteEvent) ctx.verifyErrorListenerCalledForActiveDoc(0) @@ -508,16 +515,16 @@ class DataSynchronizerUnitTests { ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( 1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) ctx.doSyncPass() // update the reset doc ctx.updateTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() // prepare an exceptionToThrow to be thrown, and sync ctx.exceptionToThrowDuringConflict = Exception("bad") @@ -562,7 +569,7 @@ class DataSynchronizerUnitTests { @Test fun testFailedUpdate() { val ctx = harness.freshTestContext() - // set up expectations and insert + // set up expectations and insert val docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) val expectedEvent = ChangeEvent.changeEventForLocalUpdate( ctx.namespace, @@ -573,13 +580,13 @@ class DataSynchronizerUnitTests { ) ctx.insertTestDocument() ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() // update the inserted doc, and prepare our exceptionToThrow ctx.updateTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(times = 1, expectedChangeEvent = expectedEvent) + ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedEvent) val expectedException = StitchServiceException("bad", StitchServiceErrorCode.UNKNOWN) ctx.mockUpdateException(expectedException) @@ -622,15 +629,15 @@ class DataSynchronizerUnitTests { // insert a new document. assert that the correct change events // have been reflected w/ and w/o pending writes ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) // delete the document and wait ctx.deleteTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() // verify a delete event with pending writes is called ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalDelete( @@ -642,7 +649,7 @@ class DataSynchronizerUnitTests { // sync. verify the correct doc was deleted and that a change event // with no pending writes was emitted ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() val docCaptor = ArgumentCaptor.forClass(BsonDocument::class.java) verify(ctx.collectionMock, times(1)).deleteOne(docCaptor.capture()) assertEquals(ctx.testDocument["_id"], docCaptor.value["_id"]) @@ -667,10 +674,10 @@ class DataSynchronizerUnitTests { ctx.insertTestDocument() ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.deleteTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedLocalEvent) @@ -680,7 +687,7 @@ class DataSynchronizerUnitTests { ctx.queueConsumableRemoteUpdateEvent() ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalReplace( ctx.namespace, @@ -707,10 +714,10 @@ class DataSynchronizerUnitTests { ctx.insertTestDocument() ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.deleteTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedLocalEvent) @@ -719,7 +726,7 @@ class DataSynchronizerUnitTests { ctx.queueConsumableRemoteUpdateEvent() ctx.shouldConflictBeResolvedByRemote = false ctx.doSyncPass() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalDelete( ctx.namespace, @@ -747,14 +754,14 @@ class DataSynchronizerUnitTests { ) ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.doSyncPass() ctx.deleteTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedChangeEvent = expectedEvent) + ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedEvent) val expectedException = StitchServiceException("bad", StitchServiceErrorCode.UNKNOWN) ctx.mockDeleteException(expectedException) @@ -784,7 +791,7 @@ class DataSynchronizerUnitTests { ctx.deleteTestDocument() ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, expectedEvent) @@ -792,7 +799,49 @@ class DataSynchronizerUnitTests { } @Test - fun testUpdateOneById() { + fun testInsertManyAndSync() { + val ctx = harness.freshTestContext() + + ctx.reconfigure() + + val doc1 = BsonDocument("hello", BsonString("world")) + val doc2 = BsonDocument("goodbye", BsonString("computer")) + + ctx.dataSynchronizer.insertManyAndSync(ctx.namespace, listOf(doc1, doc2)) + + val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1, true) + val expectedEvent2 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc2, true) + + ctx.waitForEvents(amount = 2) + + ctx.verifyChangeEventListenerCalledForActiveDoc(2, expectedEvent1, expectedEvent2) + + assertEquals( + doc1, + ctx.dataSynchronizer.find( + ctx.namespace, + BsonDocument("_id", doc1["_id"]), + 0, + null, + null, + BsonDocument::class.java, + CodecRegistries.fromCodecs(BsonDocumentCodec()) + ).firstOrNull()) + assertEquals( + doc2, + ctx.dataSynchronizer.find( + ctx.namespace, + BsonDocument("_id", doc2["_id"]), + 0, + null, + null, + BsonDocument::class.java, + CodecRegistries.fromCodecs(BsonDocumentCodec()) + ).firstOrNull()) + } + + @Test + fun testUpdateOne() { val ctx = harness.freshTestContext() val expectedDocumentAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) // assert this doc does not exist @@ -810,12 +859,12 @@ class DataSynchronizerUnitTests { // insert the initial document ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1) // do the actual update updateResult = ctx.updateTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() // assert the UpdateResult is non-zero assertEquals(1, updateResult.matchedCount) @@ -832,7 +881,63 @@ class DataSynchronizerUnitTests { } @Test - fun testDeleteOneById() { + fun testUpdateMany() { + val ctx = harness.freshTestContext() + + ctx.reconfigure() + + val doc1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(1)) + val doc2 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(1)) + val doc3 = BsonDocument("name", BsonString("timothy")).append("count", BsonInt32(1)) + + ctx.dataSynchronizer.insertManyAndSync(ctx.namespace, listOf(doc1, doc2, doc3)) + + var expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1, true) + var expectedEvent2 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc2, true) + var expectedEvent3 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc3, true) + + ctx.waitForEvents(amount = 3) + + ctx.verifyChangeEventListenerCalledForActiveDoc( + 3, + expectedEvent1, + expectedEvent2, + expectedEvent3) + + ctx.dataSynchronizer.updateMany( + ctx.namespace, + BsonDocument("name", BsonString("philip")), + BsonDocument("\$inc", BsonDocument("count", BsonInt32(1)))) + + ctx.waitForEvents(amount = 2) + + expectedEvent1 = ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + doc1["_id"], + ChangeEvent.UpdateDescription( + BsonDocument("count", BsonInt32(2)), + listOf() + ), + BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)), + true) + expectedEvent2 = ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + doc2["_id"], + ChangeEvent.UpdateDescription( + BsonDocument("count", BsonInt32(2)), + listOf() + ), + BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)), + true) + + ctx.verifyChangeEventListenerCalledForActiveDoc( + 5, + expectedEvent1, + expectedEvent2) + } + + @Test + fun testDeleteOne() { val ctx = harness.freshTestContext() // 0: Pre-checks @@ -850,7 +955,7 @@ class DataSynchronizerUnitTests { // 1: Insert -> Delete -> Coalescence // insert the initial document ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1) // do the actual delete @@ -870,7 +975,7 @@ class DataSynchronizerUnitTests { // do the actual delete deleteResult = ctx.deleteTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() // assert the UpdateResult is non-zero assertEquals(1, deleteResult.deletedCount) @@ -900,7 +1005,7 @@ class DataSynchronizerUnitTests { ctx.deleteTestDocument() ctx.insertTestDocument() - ctx.waitForEvent() + ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert( ctx.namespace, ctx.testDocument, true)) diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt index ac0581a89..cafd8a1b9 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt @@ -130,18 +130,25 @@ class SyncUnitTestHarness : Closeable { } } + val eventAccumulator = mutableListOf>() + var totalEventsToAccumulate = 1 + private open class TestChangeEventListener( private val expectedEvent: ChangeEvent?, private val emitEventSemaphore: Semaphore? ) : ChangeEventListener { - override fun onEvent(documentId: BsonValue?, actualEvent: ChangeEvent?) { + override fun onEvent(documentId: BsonValue?, actualEvent: ChangeEvent) { + eventAccumulator.add(actualEvent) try { if (expectedEvent != null) { - compareEvents(expectedEvent, actualEvent!!) + compareEvents(expectedEvent, actualEvent) Assert.assertEquals(expectedEvent.id, documentId) } } finally { - emitEventSemaphore?.release() + if (eventAccumulator.size == totalEventsToAccumulate) { + eventAccumulator.clear() + emitEventSemaphore?.release() + } } } } @@ -234,6 +241,7 @@ class SyncUnitTestHarness : Closeable { private val streamMock = Stream(TestEventStream(this), ChangeEvent.changeEventCoder) override val testDocument = newDoc("count", BsonInt32(1)) override val testDocumentId: BsonObjectId by lazy { testDocument["_id"] as BsonObjectId } + override val testDocumentFilter by lazy { BsonDocument("_id", testDocumentId) } override var updateDocument: BsonDocument = BsonDocument("\$inc", BsonDocument("count", BsonInt32(1))) private val bsonDocumentCodec = BsonDocumentCodec() @@ -354,7 +362,8 @@ class SyncUnitTestHarness : Closeable { bsonDocumentCodec) } - override fun waitForEvent() { + override fun waitForEvents(amount: Int) { + totalEventsToAccumulate = amount assertTrue(eventSemaphore?.tryAcquire(10, TimeUnit.SECONDS) ?: true) } @@ -378,7 +387,11 @@ class SyncUnitTestHarness : Closeable { configureNewErrorListener() configureNewConflictHandler() - return dataSynchronizer.updateOneById(namespace, testDocumentId, updateDocument) + return dataSynchronizer.updateOne( + namespace, + BsonDocument("_id", testDocumentId), + updateDocument + ) } override fun deleteTestDocument(): DeleteResult { @@ -386,7 +399,7 @@ class SyncUnitTestHarness : Closeable { configureNewErrorListener() configureNewConflictHandler() - return dataSynchronizer.deleteOneById(namespace, testDocumentId) + return dataSynchronizer.deleteOne(namespace, BsonDocument("_id", testDocumentId)) } override fun doSyncPass() { @@ -431,21 +444,31 @@ class SyncUnitTestHarness : Closeable { override fun findTestDocumentFromLocalCollection(): BsonDocument? { // TODO: this may be rendered unnecessary with STITCH-1972 return withoutSyncVersion( - dataSynchronizer.findOneById( + dataSynchronizer.find( namespace, - testDocumentId, + BsonDocument("_id", testDocumentId), + 10, + null, + null, BsonDocument::class.java, - CodecRegistries.fromCodecs(bsonDocumentCodec))) + CodecRegistries.fromCodecs(bsonDocumentCodec)).firstOrNull()) } - override fun verifyChangeEventListenerCalledForActiveDoc(times: Int, expectedChangeEvent: ChangeEvent?) { + override fun verifyChangeEventListenerCalledForActiveDoc( + times: Int, + vararg expectedChangeEvents: ChangeEvent + ) { val changeEventArgumentCaptor = ArgumentCaptor.forClass(ChangeEvent::class.java) Mockito.verify(changeEventListener, times(times)).onEvent( - eq(testDocumentId), + any(), changeEventArgumentCaptor.capture() as ChangeEvent?) - if (expectedChangeEvent != null) { - compareEvents(expectedChangeEvent, changeEventArgumentCaptor.value as ChangeEvent) + if (expectedChangeEvents.isNotEmpty()) { + changeEventArgumentCaptor.allValues.forEachIndexed { i, actualChangeEvent -> + compareEvents( + expectedChangeEvents[i], + actualChangeEvent as ChangeEvent) + } } } diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt index cf13c4df2..3b610e11b 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt @@ -6,6 +6,7 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import org.bson.BsonDocument import org.bson.BsonValue import org.bson.Document import org.bson.conversions.Bson @@ -55,26 +56,17 @@ interface ProxySyncMethods { * @param filter the query filter * @return the find iterable interface */ - fun find(filter: Bson): Iterable - - /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. - * - * @param id the _id of the document to search for. - * @return the document if found locally or remotely. - */ - fun findOneById(id: BsonValue): Document? + fun find(filter: Bson = BsonDocument()): Iterable /** * Updates a document by the given id. It is first searched for in the local synchronized cache * and if not found and there is internet connectivity, it is searched for remotely. * - * @param documentId the _id of the document to search for. + * @param filter the query filter * @param update the update specifier. * @return the result of the local or remote update. */ - fun updateOneById(documentId: BsonValue, update: Bson): RemoteUpdateResult + fun updateOne(filter: Bson, update: Bson): RemoteUpdateResult /** * Inserts a single document and begins to synchronize it. @@ -88,10 +80,10 @@ interface ProxySyncMethods { * Deletes a single document by the given id. It is first searched for in the local synchronized * cache and if not found and there is internet connectivity, it is searched for remotely. * - * @param documentId the _id of the document to search for. + * @param filter the query filter * @return the result of the local or remote update. */ - fun deleteOneById(documentId: BsonValue): RemoteDeleteResult + fun deleteOne(filter: Bson): RemoteDeleteResult /** * Return the set of synchronized document _ids in a namespace diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt index df5150fa0..02f868f04 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt @@ -49,7 +49,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { fun testSync() { testSyncInBothDirections { val remoteMethods = syncTestRunner.remoteMethods() - val remoteOperations = syncTestRunner.syncMethods() + val syncOperations = syncTestRunner.syncMethods() val doc1 = Document("hello", "world") val doc2 = Document("hello", "friend") @@ -62,7 +62,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { val doc1Filter = Document("_id", doc1Id) // start watching it and always set the value to hello world in a conflict - remoteOperations.configure(ConflictHandler { id: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> + syncOperations.configure(ConflictHandler { id: BsonValue, localEvent: ChangeEvent, remoteEvent: ChangeEvent -> if (id == doc1Id) { val merged = localEvent.fullDocument.getInteger("foo") + remoteEvent.fullDocument.getInteger("foo") @@ -75,7 +75,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { }, null, null) // sync on the remote document - remoteOperations.syncOne(doc1Id) + syncOperations.syncOne(doc1Id) streamAndSync() // 1. updating a document remotely should not be reflected until coming back online. @@ -87,20 +87,22 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { assertEquals(1, result.matchedCount) streamAndSync() // because we are offline, the remote doc should not have updated - Assert.assertEquals(doc, remoteOperations.findOneById(doc1Id)) + Assert.assertEquals(doc, syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()) // go back online, and sync // the remote document should now equal our expected update goOnline() streamAndSync() val expectedDocument = Document(doc) expectedDocument["foo"] = 1 - assertEquals(expectedDocument, remoteOperations.findOneById(doc1Id)) + assertEquals(expectedDocument, syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()) // 2. insertOneAndSync should work offline and then sync the document when online. goOffline() val doc3 = Document("so", "syncy") - val insResult = remoteOperations.insertOneAndSync(doc3) - Assert.assertEquals(doc3, withoutSyncVersion(remoteOperations.findOneById(insResult.insertedId)!!)) + val insResult = syncOperations.insertOneAndSync(doc3) + Assert.assertEquals( + doc3, + withoutSyncVersion(syncOperations.find(documentIdFilter(insResult.insertedId)).firstOrNull()!!)) streamAndSync() Assert.assertNull(remoteMethods.find(Document("_id", doc3["_id"])).firstOrNull()) goOnline() @@ -117,21 +119,23 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { Assert.assertEquals(1, result2.matchedCount) expectedDocument["foo"] = 2 Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) - val result3 = remoteOperations.updateOneById( - doc1Id, + val result3 = syncOperations.updateOne( + documentIdFilter(doc1Id), doc1Update) Assert.assertEquals(1, result3.matchedCount) expectedDocument["foo"] = 2 - Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteOperations.findOneById(doc1Id)!!)) + Assert.assertEquals( + expectedDocument, + withoutSyncVersion(syncOperations.find(documentIdFilter(doc1Id)).firstOrNull()!!)) // first pass will invoke the conflict handler and update locally but not remotely yet streamAndSync() Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) expectedDocument["foo"] = 4 expectedDocument.remove("fooOps") - Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteOperations.findOneById(doc1Id)!!)) + Assert.assertEquals(expectedDocument, withoutSyncVersion(syncOperations.find(doc1Filter).first()!!)) // second pass will update with the ack'd version id streamAndSync() - Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteOperations.findOneById(doc1Id)!!)) + Assert.assertEquals(expectedDocument, withoutSyncVersion(syncOperations.find(doc1Filter).first()!!)) Assert.assertEquals(expectedDocument, withoutSyncVersion(remoteMethods.find(doc1Filter).first()!!)) } } @@ -174,21 +178,21 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // Update local val localUpdate = Document("\$set", Document("local", "updateWow")) - result = coll.updateOneById(doc1Id, localUpdate) + result = coll.updateOne(doc1Filter, localUpdate) assertEquals(1, result.matchedCount) val expectedLocalDocument = Document(doc) expectedLocalDocument["local"] = "updateWow" - assertEquals(expectedLocalDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedLocalDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // first pass will invoke the conflict handler and update locally but not remotely yet streamAndSync() assertEquals(expectedRemoteDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) expectedLocalDocument["remote"] = "update" - assertEquals(expectedLocalDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedLocalDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // second pass will update with the ack'd version id streamAndSync() - assertEquals(expectedLocalDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedLocalDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) assertEquals(expectedLocalDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) } } @@ -229,16 +233,16 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // update the local collection. // the count field locally should be 2 // the count field remotely should be 3 - result = coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) + result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) assertEquals(1, result.matchedCount) expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // sync the collection. the remote document should be accepted // and this resolution should be reflected locally and remotely streamAndSync() expectedDocument["foo"] = 3 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) streamAndSync() assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) } @@ -280,16 +284,16 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // update the local collection. // the count field locally should be 2 // the count field remotely should be 3 - result = coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) + result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) assertEquals(1, result.matchedCount) expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // sync the collection. the local document should be accepted // and this resolution should be reflected locally and remotely streamAndSync() expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) streamAndSync() assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) } @@ -319,13 +323,13 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // go offline to avoid processing events. // delete the document locally goOffline() - val result = coll.deleteOneById(doc1Id) + val result = coll.deleteOne(doc1Filter) assertEquals(1, result.deletedCount) // assert that, while the remote document remains val expectedDocument = withoutSyncVersion(Document(doc)) assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) // go online to begin the syncing process. // when syncing, our local delete will be synced to the remote. @@ -333,7 +337,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { goOnline() streamAndSync() Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) } } @@ -368,7 +372,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // go offline, and delete the document locally goOffline() - val result = coll.deleteOneById(doc1Id) + val result = coll.deleteOne(doc1Filter) assertEquals(1, result.deletedCount) // assert that the remote document has not been deleted, @@ -376,7 +380,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { val expectedDocument = Document(doc) expectedDocument["foo"] = 1 assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) // go back online and sync. assert that the remote document has been updated // while the local document reflects the resolution of the conflict @@ -386,7 +390,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { expectedDocument.remove("hello") expectedDocument.remove("foo") expectedDocument["well"] = "shoot" - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) } } @@ -403,19 +407,19 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { val insertResult = coll.insertOneAndSync(docToInsert) // find the local document we just inserted - val doc = coll.findOneById(insertResult.insertedId)!! + val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! val doc1Id = BsonObjectId(doc.getObjectId("_id")) val doc1Filter = Document("_id", doc1Id) // update the document locally val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOneById(doc1Id, doc1Update).matchedCount) + assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) // assert that nothing has been inserting remotely val expectedDocument = withoutSyncVersion(Document(doc)) expectedDocument["foo"] = 1 Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // go online (in case we weren't already). sync. goOnline() @@ -423,7 +427,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // assert that the local insertion reflects remotely assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) } } @@ -440,7 +444,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { val insertResult = coll.insertOneAndSync(docToInsert) // find the document we just inserted - val doc = coll.findOneById(insertResult.insertedId)!! + val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! val doc1Id = BsonObjectId(doc.getObjectId("_id")) val doc1Filter = Document("_id", doc1Id) @@ -450,21 +454,21 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { streamAndSync() val expectedDocument = withoutSyncVersion(Document(doc)) assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // update the document locally val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOneById(doc1Id, doc1Update).matchedCount) + assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) // assert that this update has not been reflected remotely, but has locally assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // sync. assert that our update is reflected locally and remotely streamAndSync() assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) } } @@ -482,34 +486,34 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { streamAndSync() // assert the sync'd document is found locally and remotely - val doc = coll.findOneById(insertResult.insertedId)!! + val doc = coll.find(documentIdFilter(insertResult.insertedId)).first()!! val doc1Id = BsonObjectId(doc.getObjectId("_id")) val doc1Filter = Document("_id", doc1Id) val expectedDocument = withoutSyncVersion(Document(doc)) assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // delete the doc locally, then re-insert it. // assert the document is still the same locally and remotely - assertEquals(1, coll.deleteOneById(doc1Id).deletedCount) + assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) coll.insertOneAndSync(doc) assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // update the document locally val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOneById(doc1Id, doc1Update).matchedCount) + assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) // assert that the document has not been updated remotely yet, // but has locally assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) expectedDocument["foo"] = 1 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // sync. assert that the update has been reflected remotely and locally streamAndSync() assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) } } @@ -543,7 +547,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // assert that the remote deletion is reflected locally Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) // sync. this should not re-sync the document streamAndSync() @@ -554,7 +558,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // assert that the remote insertion is NOT reflected locally assertEquals(doc, remoteColl.find(doc1Filter).first()) - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) } } @@ -581,27 +585,27 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { }, null, null) coll.syncOne(doc1Id) streamAndSync() - assertEquals(doc, coll.findOneById(doc1Id)) - Assert.assertNotNull(coll.findOneById(doc1Id)) + assertEquals(doc, coll.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.find(doc1Filter)) // go offline. // delete the document remotely. // update the document locally. goOffline() remoteColl.deleteOne(doc1Filter) - assertEquals(1, coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))).matchedCount) + assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) // go back online and sync. assert that the document remains deleted remotely, // but has not been reflected locally yet goOnline() streamAndSync() Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) // sync again. assert that the resolution is reflected locally and remotely streamAndSync() Assert.assertNotNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) } } @@ -628,8 +632,8 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { }, null, null) coll.syncOne(doc1Id) streamAndSync() - assertEquals(doc, coll.findOneById(doc1Id)) - Assert.assertNotNull(coll.findOneById(doc1Id)) + assertEquals(doc, coll.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) // delete the document remotely, then reinsert it. // wait for the events to stream @@ -639,7 +643,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { wait.acquire() // update the local document concurrently. sync. - assertEquals(1, coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))).matchedCount) + assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) streamAndSync() // assert that the remote doc has not reflected the update. @@ -648,12 +652,12 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { assertEquals(doc, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) val expectedDocument = Document("_id", doc1Id.value) expectedDocument["hello"] = "again" - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // do another sync pass. assert that the local and remote docs are in sync streamAndSync() assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) } } @@ -678,17 +682,17 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { coll.configure(failingConflictHandler, null, null) coll.syncOne(doc1Id) streamAndSync() - assertEquals(doc, coll.findOneById(doc1Id)) + assertEquals(doc, coll.find(doc1Filter).firstOrNull()) // update the document locally. sync. - assertEquals(1, coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))).matchedCount) + assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) streamAndSync() // assert that the local update has been reflected remotely. val expectedDocument = Document(withoutSyncVersion(doc)) expectedDocument["foo"] = 1 assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) } } @@ -716,8 +720,8 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { }, null, null) coll.syncOne(doc1Id) streamAndSync() - assertEquals(doc, coll.findOneById(doc1Id)) - Assert.assertNotNull(coll.findOneById(doc1Id)) + assertEquals(doc, coll.find(doc1Filter).firstOrNull()) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) // update the document remotely. wait for the update event to store. val sem = watchForEvents(syncTestRunner.namespace) @@ -725,7 +729,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { sem.acquire() // update the document locally. - assertEquals(1, coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))).matchedCount) + assertEquals(1, coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))).matchedCount) // sync. assert that the remote document has received that update, // but locally the document has resolved to deletion @@ -734,13 +738,13 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { expectedDocument["foo"] = 1 assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) goOffline() - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) // go online and sync. the deletion should be reflected remotely and locally now goOnline() streamAndSync() Assert.assertNull(remoteColl.find(doc1Filter).firstOrNull()) - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) } } @@ -772,12 +776,14 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // reconfigure sync and the same way. do a sync pass. powerCycleDevice() coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) + val sem = watchForEvents(syncTestRunner.namespace) streamAndSync() // update the document remotely. assert the update is reflected remotely. // reload our configuration again. reconfigure Sync again. val expectedDocument = Document(doc) var result = remoteColl.updateOne(doc1Filter, withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) + assertTrue(sem.tryAcquire(10, TimeUnit.SECONDS)) assertEquals(1, result.matchedCount) expectedDocument["foo"] = 3 assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) @@ -785,10 +791,10 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) // update the document locally. assert its success, after reconfiguration. - result = coll.updateOneById(doc1Id, Document("\$inc", Document("foo", 1))) + result = coll.updateOne(doc1Filter, Document("\$inc", Document("foo", 1))) assertEquals(1, result.matchedCount) expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // reconfigure again. powerCycleDevice() @@ -805,7 +811,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // assert the update was reflected locally. reconfigure again. expectedDocument["foo"] = 2 - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) powerCycleDevice() coll.configure(DefaultSyncConflictResolvers.localWins(), null, null) @@ -827,12 +833,12 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { val doc1Id = coll.insertOneAndSync(docToInsert).insertedId // assert the document exists locally. desync it. - assertEquals(docToInsert, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(docToInsert, withoutSyncVersion(coll.find(documentIdFilter(doc1Id)).first()!!)) coll.desyncOne(doc1Id) // sync. assert that the desync'd document no longer exists locally streamAndSync() - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) } } @@ -859,113 +865,23 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { streamAndSync() val expectedDocument = Document(docToInsert) expectedDocument["friend"] = "welcome" - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) assertEquals(docToInsert, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) // sync again. assert that the resolution is reflected // locally and remotely. streamAndSync() - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) assertEquals(expectedDocument, withoutSyncVersion(remoteColl.find(doc1Filter).first()!!)) } } - @Test - fun testPausedDocumentConfig() { - testSyncInBothDirections { - val testSync = syncTestRunner.syncMethods() - val remoteColl = syncTestRunner.remoteMethods() - var errorEmitted = false - - var conflictCounter = 0 - - testSync.configure( - ConflictHandler { _: BsonValue, _: ChangeEvent, remoteEvent: ChangeEvent -> - if (conflictCounter == 0) { - conflictCounter++ - errorEmitted = true - throw Exception("ouch") - } - remoteEvent.fullDocument - }, - ChangeEventListener { _: BsonValue, _: ChangeEvent -> - }, - ErrorListener { _, _ -> - }) - - // insert an initial doc - val testDoc = Document("hello", "world") - val result = testSync.insertOneAndSync(testDoc) - - // do a sync pass, synchronizing the doc - streamAndSync() - - Assert.assertNotNull(remoteColl.find(Document("_id", testDoc.get("_id"))).first()) - - // update the doc - val expectedDoc = Document("hello", "computer") - testSync.updateOneById(result.insertedId, Document("\$set", expectedDoc)) - - // create a conflict - var sem = watchForEvents(syncTestRunner.namespace) - remoteColl.updateOne(Document("_id", result.insertedId), withNewSyncVersionSet(Document("\$inc", Document("foo", 2)))) - sem.acquire() - - // do a sync pass, and throw an error during the conflict resolver - // freezing the document - streamAndSync() - Assert.assertTrue(errorEmitted) - - // update the doc remotely - val nextDoc = Document("hello", "friend") - - sem = watchForEvents(syncTestRunner.namespace) - remoteColl.updateOne(Document("_id", result.insertedId), nextDoc) - sem.acquire() - streamAndSync() - - // it should not have updated the local doc, as the local doc should be paused - assertEquals( - withoutId(expectedDoc), - withoutSyncVersion(withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) - - // update the local doc. this should unfreeze the config - testSync.updateOneById(result.insertedId, Document("\$set", Document("no", "op"))) - - streamAndSync() - - // this should still be the remote doc since remote wins - assertEquals( - withoutId(nextDoc), - withoutSyncVersion(withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) - - // update the doc remotely - val lastDoc = Document("good night", "computer") - - sem = watchForEvents(syncTestRunner.namespace) - remoteColl.updateOne( - Document("_id", result.insertedId), - withNewSyncVersion(lastDoc) - ) - sem.acquire() - - // now that we're sync'd and resumed, it should be reflected locally - // TODO: STITCH-1958 Possible race condition here for update listening - streamAndSync() - - assertEquals( - withoutId(lastDoc), - withoutSyncVersion( - withoutId(testSync.find(Document("_id", result.insertedId)).first()!!))) - } - } - @Test fun testConfigure() { val coll = syncTestRunner.syncMethods() val remoteColl = syncTestRunner.remoteMethods() - // insert a documnet locally + // insert a document locally val docToInsert = Document("hello", "world") val insertedId = coll.insertOneAndSync(docToInsert).insertedId @@ -1014,7 +930,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { coll.configure(failingConflictHandler, null, null) val insertResult = coll.insertOneAndSync(docToInsert) - val doc = coll.findOneById(insertResult.insertedId) + val doc = coll.find(documentIdFilter(insertResult.insertedId)).first() val doc1Id = BsonObjectId(doc?.getObjectId("_id")) val doc1Filter = Document("_id", doc1Id) @@ -1029,19 +945,19 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { assertEquals(0, versionCounterOf(firstRemoteDoc)) - assertEquals(expectedDocument, coll.findOneById(doc1Id)) + assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) // the remote document after a local update, but before a sync pass, should have the // same version as the original document, and be equivalent to the unupdated document val doc1Update = Document("\$inc", Document("foo", 1)) - assertEquals(1, coll.updateOneById(doc1Id, doc1Update).matchedCount) + assertEquals(1, coll.updateOne(doc1Filter, doc1Update).matchedCount) val secondRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! assertEquals(expectedDocument, withoutSyncVersion(secondRemoteDocBeforeSyncPass)) assertEquals(versionOf(firstRemoteDoc), versionOf(secondRemoteDocBeforeSyncPass)) expectedDocument["foo"] = 1 - assertEquals(expectedDocument, coll.findOneById(doc1Id)) + assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) // the remote document after a local update, and after a sync pass, should have a new // version with the same instance ID as the original document, a version counter @@ -1052,18 +968,18 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { assertEquals(instanceIdOf(firstRemoteDoc), instanceIdOf(secondRemoteDoc)) assertEquals(1, versionCounterOf(secondRemoteDoc)) - assertEquals(expectedDocument, coll.findOneById(doc1Id)) + assertEquals(expectedDocument, coll.find(doc1Filter).firstOrNull()) // the remote document after a local delete and local insert, but before a sync pass, // should have the same version as the previous document - assertEquals(1, coll.deleteOneById(doc1Id).deletedCount) + assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) coll.insertOneAndSync(doc!!) val thirdRemoteDocBeforeSyncPass = remoteColl.find(doc1Filter).first()!! assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDocBeforeSyncPass)) expectedDocument.remove("foo") - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) // the remote document after a local delete and local insert, and after a sync pass, // should have the same instance ID as before and a version count, since the change @@ -1072,7 +988,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { val thirdRemoteDoc = remoteColl.find(doc1Filter).first()!! assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) assertEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(thirdRemoteDoc)) assertEquals(2, versionCounterOf(thirdRemoteDoc)) @@ -1080,14 +996,14 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // the remote document after a local delete, a sync pass, a local insert, and after // another sync pass should have a new instance ID, with a version counter of zero, // since the change events are not coalesced - assertEquals(1, coll.deleteOneById(doc1Id).deletedCount) + assertEquals(1, coll.deleteOne(doc1Filter).deletedCount) streamAndSync() coll.insertOneAndSync(doc) streamAndSync() val fourthRemoteDoc = remoteColl.find(doc1Filter).first()!! assertEquals(expectedDocument, withoutSyncVersion(thirdRemoteDoc)) - assertEquals(expectedDocument, withoutSyncVersion(coll.findOneById(doc1Id)!!)) + assertEquals(expectedDocument, withoutSyncVersion(coll.find(doc1Filter).first()!!)) Assert.assertNotEquals(instanceIdOf(secondRemoteDoc), instanceIdOf(fourthRemoteDoc)) assertEquals(0, versionCounterOf(fourthRemoteDoc)) @@ -1146,18 +1062,18 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // sync. assert the document has been synced. streamAndSync() - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) // update the document locally. - coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) + coll.updateOne(documentIdFilter(doc1Id), Document("\$inc", Document("i", 1))) // sync. assert the document still exists streamAndSync() - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) // sync. assert the document still exists streamAndSync() - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id))) } } @@ -1182,11 +1098,11 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { coll.syncOne(doc1Id) streamAndSync() - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) - coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) + coll.updateOne(doc1Filter, Document("\$inc", Document("i", 1))) streamAndSync() - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(doc1Filter).firstOrNull()) assertEquals(1, remoteColl.deleteOne(doc1Filter).deletedCount) powerCycleDevice() @@ -1195,7 +1111,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { }, null, null) streamAndSync() - Assert.assertNull(coll.findOneById(doc1Id)) + Assert.assertNull(coll.find(doc1Filter).firstOrNull()) } } @@ -1221,16 +1137,16 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { coll.syncOne(doc1Id!!) streamAndSync() - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) - coll.updateOneById(doc1Id, Document("\$inc", Document("i", 1))) + coll.updateOne(documentIdFilter(doc1Id), Document("\$inc", Document("i", 1))) streamAndSync() - Assert.assertNotNull(coll.findOneById(doc1Id)) + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id))) coll.syncOne(doc2Id!!) streamAndSync() - Assert.assertNotNull(coll.findOneById(doc1Id)) - Assert.assertNotNull(coll.findOneById(doc2Id)) + Assert.assertNotNull(coll.find(documentIdFilter(doc1Id)).firstOrNull()) + Assert.assertNotNull(coll.find(documentIdFilter(doc2Id)).firstOrNull()) } } @@ -1313,9 +1229,9 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // only an actual update document (with $set and $unset) // can work for the rest of this test syncTestRunner.mdbService.rules.rule(syncTestRunner.mdbRule._id).remove() - val result = coll.updateOneById(doc1Id, updateDoc) + val result = coll.updateOne(doc1Filter, updateDoc) assertEquals(1, result.matchedCount) - assertEquals(docAfterUpdate, withoutId(withoutSyncVersion(coll.findOneById(doc1Id)!!))) + assertEquals(docAfterUpdate, withoutId(withoutSyncVersion(coll.find(doc1Filter).first()!!))) // set they_are to unwriteable. the update should only update i_am // setting i_am to false and they_are to true would fail this test @@ -1339,7 +1255,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { ) streamAndSync() - assertEquals(docAfterUpdate, withoutId(withoutSyncVersion(coll.findOneById(doc1Id)!!))) + assertEquals(docAfterUpdate, withoutId(withoutSyncVersion(coll.find(doc1Filter).first()!!))) assertEquals(docAfterUpdate, withoutId(withoutSyncVersion(remoteColl.find(doc1Filter).first()!!))) assertTrue(eventSemaphore.tryAcquire(10, TimeUnit.SECONDS)) } @@ -1379,7 +1295,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // update the doc val expectedDoc = Document("hello", "computer") - testSync.updateOneById(result.insertedId, Document("\$set", expectedDoc)) + testSync.updateOne(documentIdFilter(result.insertedId), Document("\$set", expectedDoc)) // create a conflict var sem = watchForEvents(syncTestRunner.namespace) @@ -1407,15 +1323,13 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // resume syncing here assertTrue(testSync.resumeSyncForDocument(result.insertedId)) + streamAndSync() // update the doc remotely val lastDoc = Document("good night", "computer") sem = watchForEvents(syncTestRunner.namespace) - remoteColl.updateOne( - Document("_id", result.insertedId), - withNewSyncVersion(lastDoc) - ) + remoteColl.updateOne(Document("_id", result.insertedId), withNewSyncVersion(lastDoc)) sem.acquire() // now that we're sync'd and resumed, it should be reflected locally @@ -1547,6 +1461,9 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { return newDocument } + private fun documentIdFilter(documentId: BsonValue) = + BsonDocument("_id", documentId) + private val failingConflictHandler = ConflictHandler { _: BsonValue, _: ChangeEvent, _: ChangeEvent -> Assert.fail("did not expect a conflict") throw IllegalStateException("unreachable") diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt index 3be678fb6..61be07097 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt @@ -112,9 +112,6 @@ interface SyncIntTestRunner { @Test fun testInsertInsertConflict() - @Test - fun testPausedDocumentConfig() - @Test fun testConfigure() diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java index 1d7682441..d05ebf5b6 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java @@ -21,8 +21,17 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncFindIterable; import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult; +import java.util.List; import java.util.Set; import javax.annotation.Nonnull; @@ -102,6 +111,30 @@ void configure(@Nonnull final ConflictHandler conflictResolver, */ boolean resumeSyncForDocument(@Nonnull final BsonValue documentId); + /** + * Counts the number of documents in the collection. + * + * @return the number of documents in the collection + */ + long count(); + + /** + * Counts the number of documents in the collection according to the given options. + * + * @param filter the query filter + * @return the number of documents in the collection + */ + long count(final Bson filter); + + /** + * Counts the number of documents in the collection according to the given options. + * + * @param filter the query filter + * @param options the options describing the count + * @return the number of documents in the collection + */ + long count(final Bson filter, final SyncCountOptions options); + /** * Finds all documents in the collection. * @@ -134,52 +167,112 @@ void configure(@Nonnull final ConflictHandler conflictResolver, * @param the target document type of the iterable. * @return the find iterable interface */ - SyncFindIterable find(final Bson filter, final Class resultClass); + SyncFindIterable find( + final Bson filter, + final Class resultClass); + /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Aggregates documents according to the specified aggregation pipeline. * - * @param documentId the _id of the document to search for. - * @return a task containing the document if found locally or remotely. + * @param pipeline the aggregation pipeline + * @return an iterable containing the result of the aggregation operation */ - DocumentT findOneById(final BsonValue documentId); + SyncAggregateIterable aggregate(final List pipeline); /** - * Finds a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Aggregates documents according to the specified aggregation pipeline. * - * @param documentId the _id of the document to search for. + * @param pipeline the aggregation pipeline * @param resultClass the class to decode each document into * @param the target document type of the iterable. - * @return a task containing the document if found locally or remotely. + * @return an iterable containing the result of the aggregation operation + */ + SyncAggregateIterable aggregate( + final List pipeline, + final Class resultClass); + + /** + * Inserts the provided document. If the document is missing an identifier, the client should + * generate one. + * + * @param document the document to insert + * @return the result of the insert one operation + */ + SyncInsertOneResult insertOneAndSync(final DocumentT document); + + /** + * Inserts one or more documents. + * + * @param documents the documents to insert + * @return the result of the insert many operation + */ + SyncInsertManyResult insertManyAndSync(final List documents); + + /** + * Removes at most one document from the collection that matches the given filter. If no + * documents match, the collection is not + * modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove one operation + */ + SyncDeleteResult deleteOne(final Bson filter); + + /** + * Removes all documents from the collection that match the given query filter. If no documents + * match, the collection is not modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove many operation + */ + SyncDeleteResult deleteMany(final Bson filter); + + /** + * Update a single document in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update one operation */ - ResultT findOneById(final BsonValue documentId, final Class resultClass); + SyncUpdateResult updateOne(final Bson filter, final Bson update); /** - * Updates a document by the given id. It is first searched for in the local synchronized cache - * and if not found and there is internet connectivity, it is searched for remotely. + * Update a single document in the collection according to the specified arguments. * - * @param documentId the _id of the document to search for. - * @param update the update specifier. - * @return a task containing the result of the local or remote update. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update one operation */ - RemoteUpdateResult updateOneById(final BsonValue documentId, final Bson update); + SyncUpdateResult updateOne( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions); /** - * Inserts a single document and begins to synchronize it. + * Update all documents in the collection according to the specified arguments. * - * @param document the document to insert and synchronize. - * @return the result of the insertion. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @return the result of the update many operation */ - RemoteInsertOneResult insertOneAndSync(final DocumentT document); + SyncUpdateResult updateMany(final Bson filter, final Bson update); /** - * Deletes a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. + * Update all documents in the collection according to the specified arguments. * - * @param documentId the _id of the document to search for. - * @return a task containing the result of the local or remote update. + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update many operation */ - RemoteDeleteResult deleteOneById(final BsonValue documentId); + SyncUpdateResult updateMany( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions); } diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/SyncAggregateIterable.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/SyncAggregateIterable.java new file mode 100644 index 000000000..eb81cff35 --- /dev/null +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/SyncAggregateIterable.java @@ -0,0 +1,4 @@ +package com.mongodb.stitch.server.services.mongodb.remote; + +public interface SyncAggregateIterable extends RemoteMongoIterable { +} diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncAggregateIterableImpl.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncAggregateIterableImpl.java new file mode 100644 index 000000000..fc026458f --- /dev/null +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncAggregateIterableImpl.java @@ -0,0 +1,12 @@ +package com.mongodb.stitch.server.services.mongodb.remote.internal; + +import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; +import com.mongodb.stitch.server.services.mongodb.remote.SyncAggregateIterable; + +public class SyncAggregateIterableImpl + extends RemoteMongoIterableImpl + implements SyncAggregateIterable { + SyncAggregateIterableImpl(final CoreSyncAggregateIterable iterable) { + super(iterable); + } +} diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncImpl.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncImpl.java index c943fd6e7..392fe2127 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncImpl.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncImpl.java @@ -16,21 +16,27 @@ package com.mongodb.stitch.server.services.mongodb.remote.internal; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSync; import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult; import com.mongodb.stitch.server.services.mongodb.remote.Sync; +import com.mongodb.stitch.server.services.mongodb.remote.SyncAggregateIterable; import com.mongodb.stitch.server.services.mongodb.remote.SyncFindIterable; +import java.util.List; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.conversions.Bson; @@ -108,28 +114,68 @@ public SyncFindIterable find(final Bson filter, } @Override - public DocumentT findOneById(final BsonValue documentId) { - return proxy.findOneById(documentId); + public long count() { + return this.count(new BsonDocument()); } @Override - public ResultT findOneById(final BsonValue documentId, - final Class resultClass) { - return proxy.findOneById(documentId, resultClass); + public long count(Bson filter) { + return this.count(filter, new SyncCountOptions()); } @Override - public RemoteDeleteResult deleteOneById(final BsonValue documentId) { - return proxy.deleteOneById(documentId); + public long count(Bson filter, SyncCountOptions options) { + return this.proxy.count(filter, options); } @Override - public RemoteInsertOneResult insertOneAndSync(final DocumentT document) { - return this.proxy.insertOneAndSync(document); + public SyncAggregateIterable aggregate(List pipeline) { + return new SyncAggregateIterableImpl<>(this.proxy.aggregate(pipeline)); } @Override - public RemoteUpdateResult updateOneById(final BsonValue documentId, final Bson update) { - return proxy.updateOneById(documentId, update); + public SyncAggregateIterable aggregate(List pipeline, + Class resultClass) { + return new SyncAggregateIterableImpl<>(this.proxy.aggregate(pipeline, resultClass)); + } + + @Override + public SyncInsertOneResult insertOneAndSync(DocumentT document) { + return proxy.insertOneAndSync(document); + } + + @Override + public SyncInsertManyResult insertManyAndSync(List documents) { + return proxy.insertManyAndSync(documents); + } + + @Override + public SyncUpdateResult updateOne(Bson filter, Bson update) { + return proxy.updateOne(filter, update); + } + + @Override + public SyncUpdateResult updateOne(Bson filter, Bson update, SyncUpdateOptions updateOptions) { + return proxy.updateOne(filter, update, updateOptions); + } + + @Override + public SyncUpdateResult updateMany(Bson filter, Bson update) { + return proxy.updateMany(filter, update); + } + + @Override + public SyncUpdateResult updateMany(Bson filter, Bson update, SyncUpdateOptions updateOptions) { + return proxy.updateMany(filter, update, updateOptions); + } + + @Override + public SyncDeleteResult deleteOne(Bson filter) { + return proxy.deleteOne(filter); + } + + @Override + public SyncDeleteResult deleteMany(Bson filter) { + return proxy.deleteMany(filter); } } diff --git a/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt b/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt index 6baae6cb6..677fcde30 100644 --- a/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt +++ b/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt @@ -73,16 +73,12 @@ class SyncMongoClientIntTests : BaseStitchServerIntTest(), SyncIntTestRunner { return sync.insertOneAndSync(document) } - override fun findOneById(id: BsonValue): Document? { - return sync.findOneById(id) + override fun updateOne(filter: Bson, update: Bson): RemoteUpdateResult { + return sync.updateOne(filter, update) } - override fun updateOneById(documentId: BsonValue, update: Bson): RemoteUpdateResult { - return sync.updateOneById(documentId, update) - } - - override fun deleteOneById(documentId: BsonValue): RemoteDeleteResult { - return sync.deleteOneById(documentId) + override fun deleteOne(filter: Bson): RemoteDeleteResult { + return sync.deleteOne(filter) } override fun getSyncedIds(): Set { @@ -93,7 +89,7 @@ class SyncMongoClientIntTests : BaseStitchServerIntTest(), SyncIntTestRunner { sync.desyncOne(id) } - override fun find(filter: Bson): Iterable { + override fun find(filter: Bson): Iterable { return sync.find(filter) } @@ -276,11 +272,6 @@ class SyncMongoClientIntTests : BaseStitchServerIntTest(), SyncIntTestRunner { testProxy.testInsertInsertConflict() } - @Test - override fun testPausedDocumentConfig() { - testProxy.testPausedDocumentConfig() - } - @Test override fun testConfigure() { testProxy.testConfigure() From 9add837065dbe378b130ae6a7bdaff2f87e66a56 Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 18:28:45 +0000 Subject: [PATCH 02/14] DataSync reformat --- .../android/services/mongodb/remote/Sync.java | 26 +- .../sync/internal/DataSynchronizer.java | 1239 +++++++++-------- .../internal/DataSynchronizerUnitTests.kt | 242 +++- .../sync/internal/SyncUnitTestHarness.kt | 63 +- 4 files changed, 894 insertions(+), 676 deletions(-) diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java index eacde3a1d..6dd606bba 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java @@ -194,7 +194,7 @@ SyncAggregateIterable aggregate( /** * Inserts the provided document. If the document is missing an identifier, the client should - * generate one. + * generate one. Begin synchronizating on the document's id. * * @param document the document to insert * @return the result of the insert one operation @@ -202,7 +202,7 @@ SyncAggregateIterable aggregate( Task insertOneAndSync(final DocumentT document); /** - * Inserts one or more documents. + * Inserts one or more documents. Begin synchronizing on the documents' ids. * * @param documents the documents to insert * @return the result of the insert many operation @@ -210,9 +210,9 @@ SyncAggregateIterable aggregate( Task insertManyAndSync(final List documents); /** - * Removes at most one document from the collection that matches the given filter. If no - * documents match, the collection is not - * modified. + * Removes at most one document that has been synchronized from the remote + * from the collection that matches the given filter. If no + * documents match, the collection is not modified. * * @param filter the query filter to apply the the delete operation * @return the result of the remove one operation @@ -220,8 +220,8 @@ SyncAggregateIterable aggregate( Task deleteOne(final Bson filter); /** - * Removes all documents from the collection that match the given query filter. If no documents - * match, the collection is not modified. + * Removes all documents from the collection that have been synchronized from the remote + * that match the given query filter. If no documents match, the collection is not modified. * * @param filter the query filter to apply the the delete operation * @return the result of the remove many operation @@ -229,7 +229,8 @@ SyncAggregateIterable aggregate( Task deleteMany(final Bson filter); /** - * Update a single document in the collection according to the specified arguments. + * Update a single document that has been synchronized from the remote + * in the collection according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -239,7 +240,8 @@ SyncAggregateIterable aggregate( Task updateOne(final Bson filter, final Bson update); /** - * Update a single document in the collection according to the specified arguments. + * Update a single document that has been synchronized from the remote + * in the collection according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -253,7 +255,8 @@ Task updateOne( final SyncUpdateOptions updateOptions); /** - * Update all documents in the collection according to the specified arguments. + * Update all documents that have been synchronized from the remote + * in the collection according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -263,7 +266,8 @@ Task updateOne( Task updateMany(final Bson filter, final Bson update); /** - * Update all documents in the collection according to the specified arguments. + * Update all documents that have been synchronized from the remote + * in the collection according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 8aa4c0e64..a41cd12d1 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -121,13 +121,13 @@ public class DataSynchronizer implements NetworkMonitor.StateListener { private ErrorListener errorListener; public DataSynchronizer( - final String instanceKey, - final CoreStitchServiceClient service, - final MongoClient localClient, - final CoreRemoteMongoClient remoteClient, - final NetworkMonitor networkMonitor, - final AuthMonitor authMonitor, - final Dispatcher eventDispatcher + final String instanceKey, + final CoreStitchServiceClient service, + final MongoClient localClient, + final CoreRemoteMongoClient remoteClient, + final NetworkMonitor networkMonitor, + final AuthMonitor authMonitor, + final Dispatcher eventDispatcher ) { this.service = service; this.localClient = localClient; @@ -141,39 +141,39 @@ public DataSynchronizer( // TODO: add back after SERVER-35421 // final MongoDatabase configDb = localClient.getDatabase("sync_config"); this.configDb = - localClient.getDatabase("sync_config" + instanceKey) - .withCodecRegistry(CodecRegistries.fromRegistries( - CodecRegistries.fromCodecs( - InstanceSynchronizationConfig.configCodec, - NamespaceSynchronizationConfig.configCodec, - CoreDocumentSynchronizationConfig.configCodec), - BsonUtils.DEFAULT_CODEC_REGISTRY)); + localClient.getDatabase("sync_config" + instanceKey) + .withCodecRegistry(CodecRegistries.fromRegistries( + CodecRegistries.fromCodecs( + InstanceSynchronizationConfig.configCodec, + NamespaceSynchronizationConfig.configCodec, + CoreDocumentSynchronizationConfig.configCodec), + BsonUtils.DEFAULT_CODEC_REGISTRY)); this.instancesColl = configDb - .getCollection("instances", InstanceSynchronizationConfig.class); + .getCollection("instances", InstanceSynchronizationConfig.class); if (instancesColl.countDocuments() != 0) { this.syncConfig = new InstanceSynchronizationConfig( - configDb, - instancesColl); + configDb, + instancesColl); instancesColl.insertOne(this.syncConfig); } else { this.syncConfig = new InstanceSynchronizationConfig( - configDb, - instancesColl, - instancesColl.find().first()); + configDb, + instancesColl, + instancesColl.find().first()); } this.instanceChangeStreamListener = new InstanceChangeStreamListenerImpl( - syncConfig, - service, - networkMonitor, - authMonitor); + syncConfig, + service, + networkMonitor, + authMonitor); for (final MongoNamespace ns : this.syncConfig.getSynchronizedNamespaces()) { this.instanceChangeStreamListener.addNamespace(ns); } this.logger = - Loggers.getLogger(String.format("DataSynchronizer-%s", instanceKey)); + Loggers.getLogger(String.format("DataSynchronizer-%s", instanceKey)); if (this.networkMonitor != null) { this.networkMonitor.addNetworkStateListener(this); } @@ -196,14 +196,14 @@ public void reloadConfig() { try { this.instanceChangeStreamListener.stop(); this.syncConfig = new InstanceSynchronizationConfig( - configDb, - instancesColl, - instancesColl.find().first()); + configDb, + instancesColl, + instancesColl.find().first()); this.instanceChangeStreamListener = new InstanceChangeStreamListenerImpl( - syncConfig, - service, - networkMonitor, - authMonitor + syncConfig, + service, + networkMonitor, + authMonitor ); this.isConfigured = false; this.stop(); @@ -219,18 +219,18 @@ public void configure(@Nonnull final MongoNamespace namespace, @Nonnull final Codec codec) { if (conflictHandler == null) { logger.warn( - "Invalid configuration: conflictHandler should not be null. " - + "The DataSynchronizer will not begin syncing until a ConflictHandler has been " - + "provided."); + "Invalid configuration: conflictHandler should not be null. " + + "The DataSynchronizer will not begin syncing until a ConflictHandler has been " + + "provided."); return; } this.errorListener = errorListener; this.syncConfig.getNamespaceConfig(namespace).configure( - conflictHandler, - changeEventListener, - codec + conflictHandler, + changeEventListener, + codec ); syncLock.lock(); @@ -260,9 +260,9 @@ public void start() { instanceChangeStreamListener.start(); if (syncThread == null) { syncThread = new Thread(new DataSynchronizerRunner( - new WeakReference<>(this), - networkMonitor, - logger)); + new WeakReference<>(this), + networkMonitor, + logger)); } if (syncThreadEnabled && !isRunning) { syncThread.start(); @@ -359,21 +359,21 @@ public boolean doSyncPass() { logicalT++; logger.info(String.format( - Locale.US, - "t='%d': doSyncPass START", - logicalT)); + Locale.US, + "t='%d': doSyncPass START", + logicalT)); if (networkMonitor == null || !networkMonitor.isConnected()) { logger.info(String.format( - Locale.US, - "t='%d': doSyncPass END - Network disconnected", - logicalT)); + Locale.US, + "t='%d': doSyncPass END - Network disconnected", + logicalT)); return false; } if (authMonitor == null || !authMonitor.isLoggedIn()) { logger.info(String.format( - Locale.US, - "t='%d': doSyncPass END - Logged out", - logicalT)); + Locale.US, + "t='%d': doSyncPass END - Logged out", + logicalT)); return false; } @@ -386,9 +386,9 @@ public boolean doSyncPass() { } logger.info(String.format( - Locale.US, - "t='%d': doSyncPass END", - logicalT)); + Locale.US, + "t='%d': doSyncPass END", + logicalT)); } finally { syncLock.unlock(); } @@ -405,18 +405,18 @@ public boolean doSyncPass() { */ private void syncRemoteToLocal() { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteToLocal START", - logicalT)); + Locale.US, + "t='%d': syncRemoteToLocal START", + logicalT)); // 2. Run remote to local (R2L) sync routine for (final NamespaceSynchronizationConfig nsConfig : syncConfig) { final Map> remoteChangeEvents = - getEventsForNamespace(nsConfig.getNamespace()); + getEventsForNamespace(nsConfig.getNamespace()); final Set unseenIds = nsConfig.getStaleDocumentIds(); final Set latestDocumentsFromStale = - getLatestDocumentsForStaleFromRemote(nsConfig, unseenIds); + getLatestDocumentsForStaleFromRemote(nsConfig, unseenIds); final Map latestDocumentMap = new HashMap<>(); for (final BsonDocument latestDocument : latestDocumentsFromStale) { @@ -425,16 +425,16 @@ private void syncRemoteToLocal() { // a. For each unprocessed change event for (final Map.Entry> eventEntry : - remoteChangeEvents.entrySet()) { + remoteChangeEvents.entrySet()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteToLocal consuming event of type: %s", - logicalT, - eventEntry.getValue().getOperationType())); + Locale.US, + "t='%d': syncRemoteToLocal consuming event of type: %s", + logicalT, + eventEntry.getValue().getOperationType())); // i. Find the corresponding local document config. final CoreDocumentSynchronizationConfig docConfig = - nsConfig.getSynchronizedDocument(eventEntry.getKey().asDocument().get("_id")); + nsConfig.getSynchronizedDocument(eventEntry.getKey().asDocument().get("_id")); if (docConfig == null || docConfig.isPaused()) { // Not interested in this event. @@ -451,7 +451,7 @@ private void syncRemoteToLocal() { // remote copy. for (final BsonValue docId : unseenIds) { final CoreDocumentSynchronizationConfig docConfig = - nsConfig.getSynchronizedDocument(docId); + nsConfig.getSynchronizedDocument(docId); if (docConfig == null || docConfig.isPaused()) { // means we aren't actually synchronizing on this remote doc continue; @@ -459,14 +459,14 @@ private void syncRemoteToLocal() { if (latestDocumentMap.containsKey(docId)) { syncRemoteChangeEventToLocal( - nsConfig, - docConfig, - changeEventForLocalReplace( - nsConfig.getNamespace(), - docId, - latestDocumentMap.get(docId), - false - )); + nsConfig, + docConfig, + changeEventForLocalReplace( + nsConfig.getNamespace(), + docId, + latestDocumentMap.get(docId), + false + )); docConfig.setStale(false); } @@ -478,22 +478,22 @@ private void syncRemoteToLocal() { unseenIds.removeAll(latestDocumentMap.keySet()); for (final BsonValue unseenId : unseenIds) { final CoreDocumentSynchronizationConfig docConfig = - nsConfig.getSynchronizedDocument(unseenId); + nsConfig.getSynchronizedDocument(unseenId); if (docConfig == null - || docConfig.getLastKnownRemoteVersion() == null - || docConfig.isPaused()) { + || docConfig.getLastKnownRemoteVersion() == null + || docConfig.isPaused()) { // means we aren't actually synchronizing on this remote doc continue; } syncRemoteChangeEventToLocal( - nsConfig, - docConfig, - changeEventForLocalDelete( - nsConfig.getNamespace(), - unseenId, - docConfig.hasUncommittedWrites() - )); + nsConfig, + docConfig, + changeEventForLocalDelete( + nsConfig.getNamespace(), + unseenId, + docConfig.hasUncommittedWrites() + )); docConfig.setStale(false); } @@ -501,9 +501,9 @@ private void syncRemoteToLocal() { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteToLocal END", - logicalT)); + Locale.US, + "t='%d': syncRemoteToLocal END", + logicalT)); } /** @@ -514,61 +514,61 @@ private void syncRemoteToLocal() { * @param remoteChangeEvent the remote change event to synchronize into the local database. */ private void syncRemoteChangeEventToLocal( - final NamespaceSynchronizationConfig nsConfig, - final CoreDocumentSynchronizationConfig docConfig, - final ChangeEvent remoteChangeEvent + final NamespaceSynchronizationConfig nsConfig, + final CoreDocumentSynchronizationConfig docConfig, + final ChangeEvent remoteChangeEvent ) { if (docConfig.hasUncommittedWrites() && docConfig.getLastResolution() == logicalT) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal have writes for %s but happened at same t; " - + "waiting until next pass", - logicalT, - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal have writes for %s but happened at same t; " + + "waiting until next pass", + logicalT, + docConfig.getDocumentId())); return; } logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s processing operation='%s'", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - remoteChangeEvent.getOperationType().toString())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s processing operation='%s'", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + remoteChangeEvent.getOperationType().toString())); final DocumentVersionInfo currentRemoteVersionInfo; try { currentRemoteVersionInfo = DocumentVersionInfo - .getRemoteVersionInfo(remoteChangeEvent.getFullDocument()); + .getRemoteVersionInfo(remoteChangeEvent.getFullDocument()); } catch (final Exception e) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } if (currentRemoteVersionInfo.hasVersion() - && currentRemoteVersionInfo.getVersion().getSyncProtocolVersion() != 1) { + && currentRemoteVersionInfo.getVersion().getSyncProtocolVersion() != 1) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document with an unsupported synchronization protocol version " - + "%d; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document with an unsupported synchronization protocol version " + + "%d; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); return; } @@ -580,12 +580,12 @@ private void syncRemoteChangeEventToLocal( if (docConfig.hasCommittedVersion(currentRemoteVersionInfo)) { // Skip this event since we generated it. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event was " - + "generated by us; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event was " + + "generated by us; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -598,41 +598,41 @@ private void syncRemoteChangeEventToLocal( case UPDATE: case INSERT: logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s replacing local with " - + "remote document with new version as there are no local pending writes: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - remoteChangeEvent.getFullDocument())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s replacing local with " + + "remote document with new version as there are no local pending writes: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + remoteChangeEvent.getFullDocument())); replaceOrUpsertOneFromRemote( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - remoteChangeEvent.getFullDocument(), - DocumentVersionInfo.getDocumentVersionDoc(remoteChangeEvent.getFullDocument())); + nsConfig.getNamespace(), + docConfig.getDocumentId(), + remoteChangeEvent.getFullDocument(), + DocumentVersionInfo.getDocumentVersionDoc(remoteChangeEvent.getFullDocument())); return; case DELETE: logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s deleting local as " - + "there are no local pending writes", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s deleting local as " + + "there are no local pending writes", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); deleteOneFromRemote( - nsConfig.getNamespace(), - docConfig.getDocumentId()); + nsConfig.getNamespace(), + docConfig.getDocumentId()); return; default: emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s unknown operation type " - + "occurred on the document: %s; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - remoteChangeEvent.getOperationType().toString())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s unknown operation type " + + "occurred on the document: %s; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + remoteChangeEvent.getOperationType().toString())); return; } } @@ -644,19 +644,19 @@ private void syncRemoteChangeEventToLocal( // iv. Otherwise, check if the version info of the incoming remote change event is different // from the version of the local document. final DocumentVersionInfo lastKnownLocalVersionInfo = DocumentVersionInfo - .getLocalVersionInfo(docConfig); + .getLocalVersionInfo(docConfig); // 1. If both the local document version and the remote change event version are empty, drop // the event. The absence of a version is effectively a version, and the pending write will // set a version on the next L2R pass if it’s not a delete. if (!lastKnownLocalVersionInfo.hasVersion() && !currentRemoteVersionInfo.hasVersion()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " - + "empty version but a write is pending; waiting for next L2R pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " + + "empty version but a write is pending; waiting for next L2R pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -666,12 +666,12 @@ private void syncRemoteChangeEventToLocal( // adhering to the mobile sync protocol. if (!lastKnownLocalVersionInfo.hasVersion() || !currentRemoteVersionInfo.hasVersion()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " - + "empty version but a write is pending; waiting for next L2R pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " + + "empty version but a write is pending; waiting for next L2R pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict(nsConfig.getNamespace(), docConfig, remoteChangeEvent); return; } @@ -686,28 +686,28 @@ private void syncRemoteChangeEventToLocal( // i. drop the event if the version counter of the remote event less than or equal to the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " - + "is stale; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " + + "is stale; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } else { // ii. raise a conflict if the version counter of the remote event is greater than the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "has higher counter than local version but a write is pending; " - + "raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "has higher counter than local version but a write is pending; " + + "raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); return; } } @@ -716,27 +716,27 @@ private void syncRemoteChangeEventToLocal( // fetch the latest version (this is to guard against the case where the unprocessed // change event is stale). final BsonDocument newestRemoteDocument = this.getRemoteCollection(nsConfig.getNamespace()) - .find(new Document("_id", docConfig.getDocumentId())).first(); + .find(new Document("_id", docConfig.getDocumentId())).first(); if (newestRemoteDocument == null) { // i. If the document is not found with a remote lookup, this means the document was // deleted remotely, so raise a conflict using a synthesized delete event as the remote // change event. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "stale and latest document lookup indicates a remote delete occurred, but " - + "a write is pending; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "stale and latest document lookup indicates a remote delete occurred, but " + + "a write is pending; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - changeEventForLocalDelete( nsConfig.getNamespace(), - docConfig.getDocumentId(), - docConfig.hasUncommittedWrites())); + docConfig, + changeEventForLocalDelete( + nsConfig.getNamespace(), + docConfig.getDocumentId(), + docConfig.hasUncommittedWrites())); return; } @@ -744,18 +744,18 @@ private void syncRemoteChangeEventToLocal( final DocumentVersionInfo newestRemoteVersionInfo; try { newestRemoteVersionInfo = DocumentVersionInfo - .getRemoteVersionInfo(newestRemoteDocument); + .getRemoteVersionInfo(newestRemoteDocument); } catch (final Exception e) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -763,16 +763,16 @@ private void syncRemoteChangeEventToLocal( // to the GUID of the local document, drop the event. We’re believed to be behind in // the change stream at this point. if (newestRemoteVersionInfo.hasVersion() - && newestRemoteVersionInfo.getVersion().getInstanceId() - .equals(localVersion.instanceId)) { + && newestRemoteVersionInfo.getVersion().getInstanceId() + .equals(localVersion.instanceId)) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates that this is a stale event; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates that this is a stale event; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -782,21 +782,21 @@ private void syncRemoteChangeEventToLocal( // event. This means the remote document is a legitimately new document and we should // handle the conflict. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates a remote replace occurred, but a local write is pending; raising " - + "conflict with synthesized replace event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates a remote replace occurred, but a local write is pending; raising " + + "conflict with synthesized replace event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - changeEventForLocalReplace( nsConfig.getNamespace(), - docConfig.getDocumentId(), - newestRemoteDocument, - docConfig.hasUncommittedWrites())); + docConfig, + changeEventForLocalReplace( + nsConfig.getNamespace(), + docConfig.getDocumentId(), + newestRemoteDocument, + docConfig.hasUncommittedWrites())); } /** @@ -806,15 +806,15 @@ private void syncRemoteChangeEventToLocal( */ private void syncLocalToRemote() { logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote START", - logicalT)); + Locale.US, + "t='%d': syncLocalToRemote START", + logicalT)); // 1. Run local to remote (L2R) sync routine // Search for modifications in each namespace. for (final NamespaceSynchronizationConfig nsConfig : syncConfig) { final CoreRemoteMongoCollection remoteColl = - getRemoteCollection(nsConfig.getNamespace()); + getRemoteCollection(nsConfig.getNamespace()); // a. For each document that has local writes pending for (final CoreDocumentSynchronizationConfig docConfig : nsConfig) { @@ -823,25 +823,25 @@ private void syncLocalToRemote() { } if (docConfig.getLastResolution() == logicalT) { logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s has writes from current logicalT; " - + "waiting until next pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s has writes from current logicalT; " + + "waiting until next pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); continue; } // i. Retrieve the change event for this local document in the local config metadata final ChangeEvent localChangeEvent = - docConfig.getLastUncommittedChangeEvent(); + docConfig.getLastUncommittedChangeEvent(); logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s processing operation='%s'", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - localChangeEvent.getOperationType().toString())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s processing operation='%s'", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + localChangeEvent.getOperationType().toString())); final BsonDocument localDoc = localChangeEvent.getFullDocument(); final BsonDocument docFilter = getDocumentIdFilter(docConfig.getDocumentId()); @@ -854,32 +854,32 @@ private void syncLocalToRemote() { boolean remoteDocumentFetched = false; final DocumentVersionInfo localVersionInfo = - DocumentVersionInfo.getLocalVersionInfo(docConfig); + DocumentVersionInfo.getLocalVersionInfo(docConfig); final BsonDocument nextVersion; // ii. Check if the internal remote change stream listener has an unprocessed event for // this document. final ChangeEvent unprocessedRemoteEvent = - instanceChangeStreamListener.getUnprocessedEventForDocumentId( - nsConfig.getNamespace(), - docConfig.getDocumentId()); + instanceChangeStreamListener.getUnprocessedEventForDocumentId( + nsConfig.getNamespace(), + docConfig.getDocumentId()); if (unprocessedRemoteEvent != null) { final DocumentVersionInfo unprocessedEventVersion; try { unprocessedEventVersion = DocumentVersionInfo - .getRemoteVersionInfo(unprocessedRemoteEvent.getFullDocument()); + .getRemoteVersionInfo(unprocessedRemoteEvent.getFullDocument()); } catch (final Exception e) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -889,12 +889,12 @@ private void syncLocalToRemote() { if (!docConfig.hasCommittedVersion(unprocessedEventVersion)) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "unprocessed change event for document; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "unprocessed change event for document; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } // 2. Otherwise, the unprocessed event can be safely dropped and ignored in future R2L @@ -914,31 +914,31 @@ private void syncLocalToRemote() { // a. Insert document into remote database try { remoteColl.insertOne( - withNewVersion(localChangeEvent.getFullDocument(), nextVersion)); + withNewVersion(localChangeEvent.getFullDocument(), nextVersion)); } catch (final StitchServiceException ex) { // b. If an error happens: // i. That is not a duplicate key exception, report an error to the error listener. if (ex.getErrorCode() != StitchServiceErrorCode.MONGODB_ERROR - || !ex.getMessage().contains("E11000")) { + || !ex.getMessage().contains("E11000")) { this.emitError(docConfig, String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), ex); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), ex); continue; } // ii. Otherwise record that a conflict has occurred. logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s duplicate key exception on " - + "insert; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s duplicate key exception on " + + "insert; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); isConflicted = true; } break; @@ -949,12 +949,12 @@ private void syncLocalToRemote() { case REPLACE: { if (localDoc == null) { final IllegalStateException illegalStateException = new IllegalStateException( - "expected document to exist for local replace change event: %s"); + "expected document to exist for local replace change event: %s"); emitError( - docConfig, - illegalStateException.getMessage(), - illegalStateException + docConfig, + illegalStateException.getMessage(), + illegalStateException ); continue; } @@ -967,21 +967,21 @@ private void syncLocalToRemote() { final RemoteUpdateResult result; try { result = remoteColl.updateOne( - localVersionInfo.getFilter(), - nextDoc); + localVersionInfo.getFilter(), + nextDoc); } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. this.emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "replacing: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + "replacing: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -989,12 +989,12 @@ private void syncLocalToRemote() { if (result.getMatchedCount() == 0) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "replaced document or document deleted; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "replaced document or document deleted; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } break; } @@ -1003,11 +1003,11 @@ private void syncLocalToRemote() { case UPDATE: { if (localDoc == null) { final IllegalStateException illegalStateException = new IllegalStateException( - "expected document to exist for local update change event"); + "expected document to exist for local update change event"); emitError( - docConfig, - illegalStateException.getMessage(), - illegalStateException + docConfig, + illegalStateException.getMessage(), + illegalStateException ); continue; } @@ -1021,7 +1021,7 @@ private void syncLocalToRemote() { if (!localChangeEvent.getUpdateDescription().getUpdatedFields().isEmpty()) { final BsonDocument sets = new BsonDocument(); for (final Map.Entry fieldValue : - localChangeEvent.getUpdateDescription().getUpdatedFields().entrySet()) { + localChangeEvent.getUpdateDescription().getUpdatedFields().entrySet()) { sets.put(fieldValue.getKey(), fieldValue.getValue()); } sets.put(DOCUMENT_VERSION_FIELD, nextVersion); @@ -1030,7 +1030,7 @@ private void syncLocalToRemote() { if (!localChangeEvent.getUpdateDescription().getRemovedFields().isEmpty()) { final BsonDocument unsets = new BsonDocument(); for (final String field : - localChangeEvent.getUpdateDescription().getRemovedFields()) { + localChangeEvent.getUpdateDescription().getRemovedFields()) { unsets.put(field, BsonBoolean.TRUE); } translatedUpdate.put("$unset", unsets); @@ -1039,21 +1039,21 @@ private void syncLocalToRemote() { final RemoteUpdateResult result; try { result = remoteColl.updateOne( - localVersionInfo.getFilter(), - translatedUpdate.isEmpty() ? nextDoc : translatedUpdate); + localVersionInfo.getFilter(), + translatedUpdate.isEmpty() ? nextDoc : translatedUpdate); } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "updating: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + "updating: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -1061,12 +1061,12 @@ private void syncLocalToRemote() { // c. If no documents are matched, record that a conflict has occurred. isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "updated document or document deleted; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "updated document or document deleted; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } break; } @@ -1081,16 +1081,16 @@ private void syncLocalToRemote() { } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + " deleting: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + " deleting: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -1101,12 +1101,12 @@ private void syncLocalToRemote() { if (remoteDocument != null) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "removed document; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "removed document; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } else { // d. Desynchronize the document if there is no conflict, or if fetching a remote // document after the conflict is raised returns no remote document. @@ -1120,15 +1120,15 @@ private void syncLocalToRemote() { default: emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s unknown operation " - + "type occurred on the document: %s; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - localChangeEvent.getOperationType().toString()) + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s unknown operation " + + "type occurred on the document: %s; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + localChangeEvent.getOperationType().toString()) ); continue; } @@ -1138,12 +1138,12 @@ private void syncLocalToRemote() { logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s conflict=%s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - isConflicted)); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s conflict=%s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + isConflicted)); if (!isConflicted) { // iv. If no conflict has occurred, move on to the remote to local sync routine. @@ -1151,15 +1151,15 @@ private void syncLocalToRemote() { // TODO(STITCH-1972): This event may contain old version info. We should be filtering out // the version anyway from local and remote events. final ChangeEvent committedEvent = - docConfig.getLastUncommittedChangeEvent(); + docConfig.getLastUncommittedChangeEvent(); emitEvent(docConfig.getDocumentId(), new ChangeEvent<>( - committedEvent.getId(), - committedEvent.getOperationType(), - committedEvent.getFullDocument(), - committedEvent.getNamespace(), - committedEvent.getDocumentKey(), - committedEvent.getUpdateDescription(), - false)); + committedEvent.getId(), + committedEvent.getOperationType(), + committedEvent.getFullDocument(), + committedEvent.getNamespace(), + committedEvent.getDocumentKey(), + committedEvent.getUpdateDescription(), + false)); docConfig.setPendingWritesComplete(nextVersion); } else { @@ -1169,27 +1169,27 @@ private void syncLocalToRemote() { final ChangeEvent remoteChangeEvent; if (!remoteDocumentFetched) { remoteChangeEvent = - getSynthesizedRemoteChangeEventForDocument(remoteColl, docConfig.getDocumentId()); + getSynthesizedRemoteChangeEventForDocument(remoteColl, docConfig.getDocumentId()); } else { remoteChangeEvent = - getSynthesizedRemoteChangeEventForDocument( - remoteColl.getNamespace(), - docConfig.getDocumentId(), - remoteDocument); + getSynthesizedRemoteChangeEventForDocument( + remoteColl.getNamespace(), + docConfig.getDocumentId(), + remoteDocument); } resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); } } } logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote END", - logicalT)); + Locale.US, + "t='%d': syncLocalToRemote END", + logicalT)); // 3. If there are still local writes pending for the document, it will go through the L2R // phase on a subsequent pass and try to commit changes again. @@ -1223,7 +1223,7 @@ public Object call() { this.logger.error(msg); this.logger.error( - String.format("Setting document %s to frozen", docConfig.getDocumentId())); + String.format("Setting document %s to frozen", docConfig.getDocumentId())); } @@ -1238,64 +1238,64 @@ public Object call() { * @param remoteEvent the remote change event that is conflicting. */ private void resolveConflict( - final MongoNamespace namespace, - final CoreDocumentSynchronizationConfig docConfig, - final ChangeEvent remoteEvent + final MongoNamespace namespace, + final CoreDocumentSynchronizationConfig docConfig, + final ChangeEvent remoteEvent ) { if (this.syncConfig.getNamespaceConfig(namespace).getConflictHandler() == null) { logger.warn(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s no conflict resolver set; cannot " - + "resolve yet", - logicalT, - namespace, - docConfig.getDocumentId())); + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s no conflict resolver set; cannot " + + "resolve yet", + logicalT, + namespace, + docConfig.getDocumentId())); return; } logger.info(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s resolving conflict between localOp=%s " - + "remoteOp=%s", - logicalT, - namespace, - docConfig.getDocumentId(), - docConfig.getLastUncommittedChangeEvent().getOperationType(), - remoteEvent.getOperationType())); + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s resolving conflict between localOp=%s " + + "remoteOp=%s", + logicalT, + namespace, + docConfig.getDocumentId(), + docConfig.getLastUncommittedChangeEvent().getOperationType(), + remoteEvent.getOperationType())); // 2. Based on the result of the handler determine the next state of the document. final Object resolvedDocument; final ChangeEvent transformedRemoteEvent; try { final ChangeEvent transformedLocalEvent = ChangeEvent.transformChangeEventForUser( - docConfig.getLastUncommittedChangeEvent(), - syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); - transformedRemoteEvent = - ChangeEvent.transformChangeEventForUser( - remoteEvent, + docConfig.getLastUncommittedChangeEvent(), syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); + transformedRemoteEvent = + ChangeEvent.transformChangeEventForUser( + remoteEvent, + syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); resolvedDocument = resolveConflictWithResolver( - this.syncConfig.getNamespaceConfig(namespace).getConflictHandler(), - docConfig.getDocumentId(), - transformedLocalEvent, - transformedRemoteEvent); + this.syncConfig.getNamespaceConfig(namespace).getConflictHandler(), + docConfig.getDocumentId(), + transformedLocalEvent, + transformedRemoteEvent); } catch (final Exception ex) { logger.error(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s resolution exception: %s", - logicalT, - namespace, - docConfig.getDocumentId(), - ex)); - emitError(docConfig, - String.format( Locale.US, "t='%d': resolveConflict ns=%s documentId=%s resolution exception: %s", logicalT, namespace, docConfig.getDocumentId(), - ex), - ex); + ex)); + emitError(docConfig, + String.format( + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s resolution exception: %s", + logicalT, + namespace, + docConfig.getDocumentId(), + ex), + ex); return; } @@ -1308,37 +1308,37 @@ private void resolveConflict( } else { try { final DocumentVersionInfo remoteVersionInfo = DocumentVersionInfo - .getRemoteVersionInfo(remoteEvent.getFullDocument()); + .getRemoteVersionInfo(remoteEvent.getFullDocument()); remoteVersion = remoteVersionInfo.getVersionDoc(); } catch (final Exception e) { desyncDocumentFromRemote(namespace, docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - namespace, - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + namespace, + docConfig.getDocumentId())); return; } } final boolean acceptRemote = - (remoteEvent.getFullDocument() == null && resolvedDocument == null) - || (remoteEvent.getFullDocument() != null - && transformedRemoteEvent.getFullDocument().equals(resolvedDocument)); + (remoteEvent.getFullDocument() == null && resolvedDocument == null) + || (remoteEvent.getFullDocument() != null + && transformedRemoteEvent.getFullDocument().equals(resolvedDocument)); // a. If the resolved document is null: if (resolvedDocument == null) { logger.info(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s deleting local and remote with remote " - + "version acknowledged", - logicalT, - namespace, - docConfig.getDocumentId())); + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s deleting local and remote with remote " + + "version acknowledged", + logicalT, + namespace, + docConfig.getDocumentId())); if (acceptRemote) { // i. If the remote event was a DELETE, delete the document locally, desynchronize the @@ -1355,37 +1355,37 @@ private void resolveConflict( // Update the document locally which will keep the pending writes but with // a new version next time around. @SuppressWarnings("unchecked") final BsonDocument docForStorage = - BsonUtils.documentToBsonDocument( - resolvedDocument, - syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); + BsonUtils.documentToBsonDocument( + resolvedDocument, + syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); logger.info(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s replacing local with resolved document " - + "with remote version acknowledged: %s", - logicalT, - namespace, - docConfig.getDocumentId(), - docForStorage.toJson())); + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s replacing local with resolved document " + + "with remote version acknowledged: %s", + logicalT, + namespace, + docConfig.getDocumentId(), + docForStorage.toJson())); if (acceptRemote) { // i. If the remote document is equal to the resolved document, replace the document // locally, mark the document as having no pending writes, and emit a REPLACE change // event if the document had not existed prior, or UPDATE if it had. replaceOrUpsertOneFromRemote( - namespace, - docConfig.getDocumentId(), - docForStorage, - remoteVersion); + namespace, + docConfig.getDocumentId(), + docForStorage, + remoteVersion); } else { // ii. Otherwise, replace the local document with the resolved document locally, mark that // there are pending writes for this document, and emit an UPDATE change event, or a // DELETE change event (if the remoteEvent's operation type was DELETE). updateOrUpsertOneFromResolution( - namespace, - docConfig.getDocumentId(), - docForStorage, - remoteVersion, - remoteEvent); + namespace, + docConfig.getDocumentId(), + docForStorage, + remoteVersion, + remoteEvent); } } } @@ -1402,15 +1402,15 @@ private void resolveConflict( */ @SuppressWarnings("unchecked") private static Object resolveConflictWithResolver( - final ConflictHandler conflictResolver, - final BsonValue documentId, - final ChangeEvent localEvent, - final ChangeEvent remoteEvent + final ConflictHandler conflictResolver, + final BsonValue documentId, + final ChangeEvent localEvent, + final ChangeEvent remoteEvent ) { return conflictResolver.resolveConflict( - documentId, - localEvent, - remoteEvent); + documentId, + localEvent, + remoteEvent); } /** @@ -1421,13 +1421,13 @@ private static Object resolveConflictWithResolver( * @return a synthesized change event for a remote document. */ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( - final CoreRemoteMongoCollection remoteColl, - final BsonValue documentId + final CoreRemoteMongoCollection remoteColl, + final BsonValue documentId ) { return getSynthesizedRemoteChangeEventForDocument( - remoteColl.getNamespace(), - documentId, - remoteColl.find(getDocumentIdFilter(documentId)).first()); + remoteColl.getNamespace(), + documentId, + remoteColl.find(getDocumentIdFilter(documentId)).first()); } /** @@ -1439,9 +1439,9 @@ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( * @return a synthesized change event for a remote document. */ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( - final MongoNamespace ns, - final BsonValue documentId, - final BsonDocument document + final MongoNamespace ns, + final BsonValue documentId, + final BsonDocument document ) { // a. When the document is looked up, if it cannot be found the synthesized change event is a // DELETE, otherwise it's a REPLACE. @@ -1486,7 +1486,7 @@ public Set getSynchronizedNamespaces() { * @return the set of synchronized documents in a namespace. */ public Set getSynchronizedDocuments( - final MongoNamespace namespace + final MongoNamespace namespace ) { return this.syncConfig.getSynchronizedDocuments(namespace); } @@ -1512,7 +1512,7 @@ public Set getPausedDocumentIds(final MongoNamespace namespace) { final Set pausedDocumentIds = new HashSet<>(); for (final CoreDocumentSynchronizationConfig config : - this.syncConfig.getSynchronizedDocuments(namespace)) { + this.syncConfig.getSynchronizedDocuments(namespace)) { if (config.isPaused()) { pausedDocumentIds.add(config.getDocumentId()); } @@ -1529,8 +1529,8 @@ public Set getPausedDocumentIds(final MongoNamespace namespace) { * @param documentId the _id of the document. */ public void syncDocumentFromRemote( - final MongoNamespace namespace, - final BsonValue documentId + final MongoNamespace namespace, + final BsonValue documentId ) { syncConfig.addSynchronizedDocument(namespace, documentId); triggerListeningToNamespace(namespace); @@ -1544,8 +1544,8 @@ public void syncDocumentFromRemote( * @param documentId the _id of the document. */ public void desyncDocumentFromRemote( - final MongoNamespace namespace, - final BsonValue documentId + final MongoNamespace namespace, + final BsonValue documentId ) { syncConfig.removeSynchronizedDocument(namespace, documentId); getLocalCollection(namespace).deleteOne(getDocumentIdFilter(documentId)); @@ -1565,8 +1565,8 @@ public void desyncDocumentFromRemote( * could not be found or there was an error resuming */ boolean resumeSyncForDocument( - final MongoNamespace namespace, - final BsonValue documentId + final MongoNamespace namespace, + final BsonValue documentId ) { if (namespace == null || documentId == null) { return false; @@ -1576,7 +1576,7 @@ boolean resumeSyncForDocument( final CoreDocumentSynchronizationConfig config; if ((namespaceSynchronizationConfig = syncConfig.getNamespaceConfig(namespace)) == null - || (config = namespaceSynchronizationConfig.getSynchronizedDocument(documentId)) == null) { + || (config = namespaceSynchronizationConfig.getSynchronizedDocument(documentId)) == null) { return false; } @@ -1614,21 +1614,30 @@ long count(final MongoNamespace namespace, final Bson filter, final CountOptions return getLocalCollection(namespace).countDocuments(filter, options); } + Collection find( + final MongoNamespace namespace, + final BsonDocument filter + ) { + return getLocalCollection(namespace) + .find(filter) + .into(new ArrayList<>()); + } + public Collection find( - final MongoNamespace namespace, - final BsonDocument filter, - final int limit, - final BsonDocument projection, - final BsonDocument sort, - final Class resultClass, - final CodecRegistry codecRegistry + final MongoNamespace namespace, + final BsonDocument filter, + final int limit, + final BsonDocument projection, + final BsonDocument sort, + final Class resultClass, + final CodecRegistry codecRegistry ) { return getLocalCollection(namespace, resultClass, codecRegistry) - .find(filter) - .limit(limit) - .projection(projection) - .sort(sort) - .into(new ArrayList()); + .find(filter) + .limit(limit) + .projection(projection) + .sort(sort) + .into(new ArrayList()); } /** @@ -1638,8 +1647,8 @@ public Collection find( * @return an iterable containing the result of the aggregation operation */ AggregateIterable aggregate( - final MongoNamespace namespace, - final List pipeline) { + final MongoNamespace namespace, + final List pipeline) { return aggregate(namespace, pipeline, BsonDocument.class); } @@ -1652,9 +1661,9 @@ AggregateIterable aggregate( * @return an iterable containing the result of the aggregation operation */ AggregateIterable aggregate( - final MongoNamespace namespace, - final List pipeline, - final Class resultClass) { + final MongoNamespace namespace, + final List pipeline, + final Class resultClass) { return getLocalCollection(namespace).aggregate(pipeline, resultClass); } @@ -1670,8 +1679,8 @@ void insertOneAndSync(final MongoNamespace namespace, final BsonDocument documen final BsonValue documentId = BsonUtils.getDocumentId(document); final ChangeEvent event = changeEventForLocalInsert(namespace, document, true); final CoreDocumentSynchronizationConfig config = syncConfig.addSynchronizedDocument( - namespace, - documentId + namespace, + documentId ); config.setSomePendingWrites(logicalT, event); triggerListeningToNamespace(namespace); @@ -1690,8 +1699,8 @@ void insertManyAndSync(final MongoNamespace namespace, final BsonValue documentId = BsonUtils.getDocumentId(document); final ChangeEvent event = changeEventForLocalInsert(namespace, document, true); final CoreDocumentSynchronizationConfig config = syncConfig.addSynchronizedDocument( - namespace, - documentId + namespace, + documentId ); config.setSomePendingWrites(logicalT, event); emitEvent(documentId, event); @@ -1721,10 +1730,10 @@ UpdateResult updateOne(final MongoNamespace namespace, final Bson filter, final * @return the result of the update one operation */ UpdateResult updateOne( - final MongoNamespace namespace, - final Bson filter, - final Bson update, - final UpdateOptions updateOptions) { + final MongoNamespace namespace, + final Bson filter, + final Bson update, + final UpdateOptions updateOptions) { final MongoCollection localCollection = getLocalCollection(namespace); final BsonDocument documentBeforeUpdate = getLocalCollection(namespace).find(filter).first(); @@ -1734,14 +1743,14 @@ UpdateResult updateOne( } final BsonDocument documentAfterUpdate = localCollection.findOneAndUpdate( - filter, - update, - new FindOneAndUpdateOptions() - .collation(updateOptions.getCollation()) - .upsert(updateOptions.isUpsert()) - .bypassDocumentValidation(updateOptions.getBypassDocumentValidation()) - .arrayFilters(updateOptions.getArrayFilters()) - .returnDocument(ReturnDocument.AFTER)); + filter, + update, + new FindOneAndUpdateOptions() + .collation(updateOptions.getCollation()) + .upsert(updateOptions.isUpsert()) + .bypassDocumentValidation(updateOptions.getBypassDocumentValidation()) + .arrayFilters(updateOptions.getArrayFilters()) + .returnDocument(ReturnDocument.AFTER)); if (documentAfterUpdate == null) { return UpdateResult.acknowledged(0, 0L, null); @@ -1757,16 +1766,16 @@ UpdateResult updateOne( } else { config = syncConfig.getSynchronizedDocument(namespace, documentId); event = changeEventForLocalUpdate( - namespace, - BsonUtils.getDocumentId(documentAfterUpdate), - ChangeEvent.UpdateDescription.diff(documentBeforeUpdate, documentAfterUpdate), - documentAfterUpdate, - true); + namespace, + BsonUtils.getDocumentId(documentAfterUpdate), + ChangeEvent.UpdateDescription.diff(documentBeforeUpdate, documentAfterUpdate), + documentAfterUpdate, + true); } config.setSomePendingWrites(logicalT, event); emitEvent(documentId, event); - return UpdateResult.acknowledged(1, 1L, null); + return UpdateResult.acknowledged(1, 1L, updateOptions.isUpsert() ? documentId : null); } /** @@ -1793,22 +1802,22 @@ UpdateResult updateMany(final MongoNamespace namespace, * @return the result of the update many operation */ UpdateResult updateMany( - final MongoNamespace namespace, - final Bson filter, - final Bson update, - final UpdateOptions updateOptions) { + final MongoNamespace namespace, + final Bson filter, + final Bson update, + final UpdateOptions updateOptions) { Map idToBeforeDocumentMap = new HashMap<>(); this.getLocalCollection(namespace) - .find(filter) - .forEach(new Block() { - @Override - public void apply(@NonNull final BsonDocument bsonDocument) { - idToBeforeDocumentMap.put(BsonUtils.getDocumentId(bsonDocument), bsonDocument); - } - }); + .find(filter) + .forEach(new Block() { + @Override + public void apply(@NonNull final BsonDocument bsonDocument) { + idToBeforeDocumentMap.put(BsonUtils.getDocumentId(bsonDocument), bsonDocument); + } + }); final UpdateResult result = this.getLocalCollection(namespace) - .updateMany(filter, update, updateOptions); + .updateMany(filter, update, updateOptions); this.getLocalCollection(namespace).find(filter).forEach(new Block() { @Override @@ -1817,7 +1826,7 @@ public void apply(@NonNull final BsonDocument afterDocument) { final BsonValue documentId = BsonUtils.getDocumentId(afterDocument); if ((beforeDocument = idToBeforeDocumentMap.get(documentId)) == null - && !updateOptions.isUpsert()) { + && !updateOptions.isUpsert()) { return; } @@ -1830,11 +1839,11 @@ public void apply(@NonNull final BsonDocument afterDocument) { } else { config = syncConfig.getSynchronizedDocument(namespace, documentId); event = changeEventForLocalUpdate( - namespace, - documentId, - ChangeEvent.UpdateDescription.diff(beforeDocument, afterDocument), - afterDocument, - true); + namespace, + documentId, + ChangeEvent.UpdateDescription.diff(beforeDocument, afterDocument), + afterDocument, + true); } config.setSomePendingWrites(logicalT, event); @@ -1854,40 +1863,40 @@ public void apply(@NonNull final BsonDocument afterDocument) { * @param document the replacement document. */ private void updateOrUpsertOneFromResolution( - final MongoNamespace namespace, - final BsonValue documentId, - final BsonDocument document, - final BsonDocument atVersion, - final ChangeEvent remoteEvent + final MongoNamespace namespace, + final BsonValue documentId, + final BsonDocument document, + final BsonDocument atVersion, + final ChangeEvent remoteEvent ) { // TODO: lock down id final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return; } final BsonDocument documentAfterUpdate = getLocalCollection(namespace) - .findOneAndReplace( - getDocumentIdFilter(documentId), - document, - new FindOneAndReplaceOptions().upsert(true).returnDocument(ReturnDocument.AFTER)); + .findOneAndReplace( + getDocumentIdFilter(documentId), + document, + new FindOneAndReplaceOptions().upsert(true).returnDocument(ReturnDocument.AFTER)); final ChangeEvent event; if (remoteEvent.getOperationType() == ChangeEvent.OperationType.DELETE) { event = changeEventForLocalInsert(namespace, documentAfterUpdate, true); } else { event = changeEventForLocalUpdate( - namespace, - documentId, - ChangeEvent.UpdateDescription.diff(remoteEvent.getFullDocument(), documentAfterUpdate), - document, - true); + namespace, + documentId, + ChangeEvent.UpdateDescription.diff(remoteEvent.getFullDocument(), documentAfterUpdate), + document, + true); } config.setSomePendingWrites( - logicalT, - atVersion, - event); + logicalT, + atVersion, + event); emitEvent(documentId, event); } @@ -1900,23 +1909,23 @@ private void updateOrUpsertOneFromResolution( * @param document the replacement document. */ private void replaceOrUpsertOneFromRemote( - final MongoNamespace namespace, - final BsonValue documentId, - final BsonDocument document, - final BsonDocument atVersion + final MongoNamespace namespace, + final BsonValue documentId, + final BsonDocument document, + final BsonDocument atVersion ) { // TODO: lock down id final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return; } getLocalCollection(namespace) - .findOneAndReplace( - getDocumentIdFilter(documentId), - document, - new FindOneAndReplaceOptions().upsert(true)); + .findOneAndReplace( + getDocumentIdFilter(documentId), + document, + new FindOneAndReplaceOptions().upsert(true)); config.setPendingWritesComplete(atVersion); emitEvent(documentId, changeEventForLocalReplace(namespace, documentId, document, false)); @@ -1933,8 +1942,8 @@ private void replaceOrUpsertOneFromRemote( DeleteResult deleteOne(final MongoNamespace namespace, final Bson filter) { final MongoCollection localCollection = getLocalCollection(namespace); final BsonDocument docToDelete = localCollection - .find(filter) - .first(); + .find(filter) + .first(); if (docToDelete == null) { return DeleteResult.acknowledged(0); @@ -1942,7 +1951,7 @@ DeleteResult deleteOne(final MongoNamespace namespace, final Bson filter) { final BsonValue documentId = BsonUtils.getDocumentId(docToDelete); final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return DeleteResult.acknowledged(0); @@ -1953,8 +1962,8 @@ DeleteResult deleteOne(final MongoNamespace namespace, final Bson filter) { // this block is to trigger coalescence for a delete after insert if (config.getLastUncommittedChangeEvent() != null - && config.getLastUncommittedChangeEvent().getOperationType() - == ChangeEvent.OperationType.INSERT) { + && config.getLastUncommittedChangeEvent().getOperationType() + == ChangeEvent.OperationType.INSERT) { desyncDocumentFromRemote(config.getNamespace(), config.getDocumentId()); return result; } @@ -1975,39 +1984,39 @@ DeleteResult deleteMany(final MongoNamespace namespace, final Bson filter) { final MongoCollection localCollection = getLocalCollection(namespace); final Set idsToDelete = - localCollection - .find(filter) - .map(new Function() { - @Override - @NonNull - public BsonValue apply(@NonNull BsonDocument bsonDocument) { - return BsonUtils.getDocumentId(bsonDocument); - } - }).into(new HashSet<>()); + localCollection + .find(filter) + .map(new Function() { + @Override + @NonNull + public BsonValue apply(@NonNull BsonDocument bsonDocument) { + return BsonUtils.getDocumentId(bsonDocument); + } + }).into(new HashSet<>()); final DeleteResult result = getLocalCollection(namespace).deleteMany(filter); for (BsonValue documentId : idsToDelete) { final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { continue; } final ChangeEvent event = - changeEventForLocalDelete(namespace, documentId, true); + changeEventForLocalDelete(namespace, documentId, true); // this block is to trigger coalescence for a delete after insert if (config.getLastUncommittedChangeEvent() != null - && config.getLastUncommittedChangeEvent().getOperationType() - == ChangeEvent.OperationType.INSERT) { + && config.getLastUncommittedChangeEvent().getOperationType() + == ChangeEvent.OperationType.INSERT) { desyncDocumentFromRemote(config.getNamespace(), config.getDocumentId()); return result; } config.setSomePendingWrites( - logicalT, event); + logicalT, event); emitEvent(documentId, event); } @@ -2022,23 +2031,23 @@ public BsonValue apply(@NonNull BsonDocument bsonDocument) { * @param documentId the _id of the document. */ private void deleteOneFromResolution( - final MongoNamespace namespace, - final BsonValue documentId, - final BsonDocument atVersion + final MongoNamespace namespace, + final BsonValue documentId, + final BsonDocument atVersion ) { // TODO: lock down id final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return; } getLocalCollection(namespace) - .deleteOne(getDocumentIdFilter(documentId)); + .deleteOne(getDocumentIdFilter(documentId)); final ChangeEvent event = - changeEventForLocalDelete(namespace, documentId, true); + changeEventForLocalDelete(namespace, documentId, true); config.setSomePendingWrites( - logicalT, atVersion, event); + logicalT, atVersion, event); emitEvent(documentId, event); } @@ -2050,12 +2059,12 @@ private void deleteOneFromResolution( * @param documentId the _id of the document. */ private void deleteOneFromRemote( - final MongoNamespace namespace, - final BsonValue documentId + final MongoNamespace namespace, + final BsonValue documentId ) { // TODO: lock down id final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return; } @@ -2080,11 +2089,11 @@ private void triggerListeningToNamespace(final MongoNamespace namespace) { instanceChangeStreamListener.start(namespace); } catch (final Exception ex) { logger.error(String.format( - Locale.US, - "t='%d': triggerListeningToNamespace ns=%s exception: %s", - logicalT, - namespace, - ex)); + Locale.US, + "t='%d': triggerListeningToNamespace ns=%s exception: %s", + logicalT, + namespace, + ex)); } finally { syncLock.unlock(); } @@ -2118,13 +2127,13 @@ private void emitEvent(final BsonValue documentId, final ChangeEvent() { @Override @@ -2133,17 +2142,17 @@ public Object call() { try { if (namespaceListener.getEventListener() != null) { namespaceListener.getEventListener().onEvent( - documentId, - ChangeEvent.transformChangeEventForUser( - event, namespaceListener.getDocumentCodec())); + documentId, + ChangeEvent.transformChangeEventForUser( + event, namespaceListener.getDocumentCodec())); } } catch (final Exception ex) { logger.error(String.format( - Locale.US, - "emitEvent ns=%s documentId=%s emit exception: %s", - event.getNamespace(), - documentId, - ex), ex); + Locale.US, + "emitEvent ns=%s documentId=%s emit exception: %s", + event.getNamespace(), + documentId, + ex), ex); } return null; } @@ -2163,14 +2172,14 @@ public Object call() { * @return the local collection representing the given namespace. */ private MongoCollection getLocalCollection( - final MongoNamespace namespace, - final Class resultClass, - final CodecRegistry codecRegistry + final MongoNamespace namespace, + final Class resultClass, + final CodecRegistry codecRegistry ) { return localClient - .getDatabase(String.format("sync_user_%s", namespace.getDatabaseName())) - .getCollection(namespace.getCollectionName(), resultClass) - .withCodecRegistry(codecRegistry); + .getDatabase(String.format("sync_user_%s", namespace.getDatabaseName())) + .getCollection(namespace.getCollectionName(), resultClass) + .withCodecRegistry(codecRegistry); } /** @@ -2181,9 +2190,9 @@ private MongoCollection getLocalCollection( */ private MongoCollection getLocalCollection(final MongoNamespace namespace) { return getLocalCollection( - namespace, - BsonDocument.class, - MongoClientSettings.getDefaultCodecRegistry()); + namespace, + BsonDocument.class, + MongoClientSettings.getDefaultCodecRegistry()); } /** @@ -2195,12 +2204,12 @@ private MongoCollection getLocalCollection(final MongoNamespace na * @return the remote collection representing the given namespace. */ private CoreRemoteMongoCollection getRemoteCollection( - final MongoNamespace namespace, - final Class resultClass + final MongoNamespace namespace, + final Class resultClass ) { return remoteClient - .getDatabase(namespace.getDatabaseName()) - .getCollection(namespace.getCollectionName(), resultClass); + .getDatabase(namespace.getDatabaseName()) + .getCollection(namespace.getCollectionName(), resultClass); } /** @@ -2210,14 +2219,14 @@ private CoreRemoteMongoCollection getRemoteCollection( * @return the remote collection representing the given namespace for raw document operations. */ private CoreRemoteMongoCollection getRemoteCollection( - final MongoNamespace namespace + final MongoNamespace namespace ) { return getRemoteCollection(namespace, BsonDocument.class); } private Set getLatestDocumentsForStaleFromRemote( - final NamespaceSynchronizationConfig nsConfig, - final Set staleIds) { + final NamespaceSynchronizationConfig nsConfig, + final Set staleIds) { final BsonArray ids = new BsonArray(); for (final BsonValue bsonValue : staleIds) { ids.add(new BsonDocument("_id", bsonValue)); @@ -2228,7 +2237,7 @@ private Set getLatestDocumentsForStaleFromRemote( } return this.getRemoteCollection(nsConfig.getNamespace()).find( - new Document("$or", ids) + new Document("$or", ids) ).into(new HashSet()); } @@ -2258,8 +2267,8 @@ private static BsonDocument getDocumentIdFilter(final BsonValue documentId) { * @return a document with a new version to the given document. */ private static BsonDocument withNewVersion( - final BsonDocument document, - final BsonDocument newVersion + final BsonDocument document, + final BsonDocument newVersion ) { final BsonDocument newDocument = BsonUtils.copyOfDocument(document); newDocument.put(DOCUMENT_VERSION_FIELD, newVersion); diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt index d1033d25b..2556f417b 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt @@ -1,6 +1,7 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal -import com.mongodb.operation.UpdateOperation +import com.mongodb.client.model.CountOptions +import com.mongodb.client.model.UpdateOptions import com.mongodb.stitch.core.StitchServiceErrorCode import com.mongodb.stitch.core.StitchServiceException import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult @@ -16,6 +17,7 @@ import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test @@ -780,6 +782,65 @@ class DataSynchronizerUnitTests { assertNull(ctx.findTestDocumentFromLocalCollection()) } + @Test + fun testCount() { + val ctx = harness.freshTestContext() + + ctx.reconfigure() + + assertEquals(0, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument())) + + val doc1 = BsonDocument("hello", BsonString("world")) + val doc2 = BsonDocument("goodbye", BsonString("computer")) + + ctx.dataSynchronizer.insertManyAndSync(ctx.namespace, listOf(doc1, doc2)) + + assertEquals(2, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument())) + + assertEquals(1, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument(), CountOptions().limit(1))) + + assertEquals(1, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument("_id", doc1["_id"]))) + + ctx.dataSynchronizer.deleteMany(ctx.namespace, BsonDocument()) + + assertEquals(0, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument())) + } + + @Test + fun testAggregate() { + val ctx = harness.freshTestContext() + + ctx.reconfigure() + + assertEquals(0, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument())) + + val doc1 = BsonDocument("hello", BsonString("world")).append("a", BsonString("b")) + val doc2 = BsonDocument("hello", BsonString("computer")).append("a", BsonString("b")) + + ctx.dataSynchronizer.insertManyAndSync(ctx.namespace, listOf(doc1, doc2)) + + val iterable = ctx.dataSynchronizer.aggregate(ctx.namespace, + listOf( + BsonDocument( + "\$project", + BsonDocument("_id", BsonInt32(0)) + .append("a", BsonInt32(0)) + ), + BsonDocument( + "\$match", + BsonDocument("hello", BsonString("computer")) + ))) + + assertEquals(2, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument())) + assertEquals(1, iterable.count()) + + val actualDoc = iterable.first()!! + + assertNull(actualDoc["a"]) + assertNull(actualDoc["_id"]) + assertEquals(BsonString("computer"), actualDoc["hello"]) + } + @Test fun testInsertOneAndSync() { val ctx = harness.freshTestContext() @@ -880,6 +941,50 @@ class DataSynchronizerUnitTests { assertEquals(expectedDocumentAfterUpdate, ctx.findTestDocumentFromLocalCollection()!!) } + @Test + fun testUpsertOne() { + val ctx = harness.freshTestContext() + + ctx.reconfigure() + + val doc1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(1)) + + val result = ctx.dataSynchronizer.updateOne( + ctx.namespace, + BsonDocument("name", BsonString("philip")), + BsonDocument("\$inc", BsonDocument("count", BsonInt32(1))), + UpdateOptions().upsert(true)) + + assertEquals(1, result.matchedCount) + assertEquals(1, result.modifiedCount) + assertNotNull(result.upsertedId) + + val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1.append("_id", result.upsertedId), true) + + ctx.waitForEvents(amount = 1) + + ctx.verifyChangeEventListenerCalledForActiveDoc( + 1, + expectedEvent1) + + assertEquals( + doc1, + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) + + ctx.dataSynchronizer.updateMany( + ctx.namespace, + BsonDocument("name", BsonString("philip")), + BsonDocument("\$inc", BsonDocument("count", BsonInt32(1)))) + + ctx.waitForEvents(amount = 2) + + val expectedDocAfterUpdate1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)).append("_id", doc1["_id"]) + + assertEquals( + expectedDocAfterUpdate1, + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) + } + @Test fun testUpdateMany() { val ctx = harness.freshTestContext() @@ -892,9 +997,9 @@ class DataSynchronizerUnitTests { ctx.dataSynchronizer.insertManyAndSync(ctx.namespace, listOf(doc1, doc2, doc3)) - var expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1, true) - var expectedEvent2 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc2, true) - var expectedEvent3 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc3, true) + val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1, true) + val expectedEvent2 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc2, true) + val expectedEvent3 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc3, true) ctx.waitForEvents(amount = 3) @@ -904,36 +1009,101 @@ class DataSynchronizerUnitTests { expectedEvent2, expectedEvent3) - ctx.dataSynchronizer.updateMany( + val result = ctx.dataSynchronizer.updateMany( ctx.namespace, BsonDocument("name", BsonString("philip")), BsonDocument("\$inc", BsonDocument("count", BsonInt32(1)))) + assertEquals(2, result.modifiedCount) + assertEquals(2, result.matchedCount) + assertNull(result.upsertedId) + ctx.waitForEvents(amount = 2) - expectedEvent1 = ChangeEvent.changeEventForLocalUpdate( - ctx.namespace, - doc1["_id"], - ChangeEvent.UpdateDescription( - BsonDocument("count", BsonInt32(2)), - listOf() - ), - BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)), - true) - expectedEvent2 = ChangeEvent.changeEventForLocalUpdate( - ctx.namespace, - doc2["_id"], - ChangeEvent.UpdateDescription( - BsonDocument("count", BsonInt32(2)), - listOf() - ), - BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)), - true) + val expectedDocAfterUpdate1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)).append("_id", doc1["_id"]) + val expectedDocAfterUpdate2 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)).append("_id", doc2["_id"]) ctx.verifyChangeEventListenerCalledForActiveDoc( 5, expectedEvent1, - expectedEvent2) + expectedEvent2, + expectedEvent3, + ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + doc1["_id"], + ChangeEvent.UpdateDescription( + BsonDocument("count", BsonInt32(2)), + listOf() + ), + expectedDocAfterUpdate1, + true), + ChangeEvent.changeEventForLocalUpdate( + ctx.namespace, + doc2["_id"], + ChangeEvent.UpdateDescription( + BsonDocument("count", BsonInt32(2)), + listOf() + ), + expectedDocAfterUpdate2, + true)) + + assertEquals( + expectedDocAfterUpdate1, + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) + assertEquals( + expectedDocAfterUpdate2, + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc2["_id"])).first()) + assertEquals( + doc3, + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc3["_id"])).first()) + } + + @Test + fun testUpsertMany() { + val ctx = harness.freshTestContext() + + ctx.reconfigure() + + val doc1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(1)) + + var result = ctx.dataSynchronizer.updateMany( + ctx.namespace, + BsonDocument("name", BsonString("philip")), + BsonDocument("\$inc", BsonDocument("count", BsonInt32(1))), + UpdateOptions().upsert(true)) + + assertEquals(0, result.matchedCount) + assertEquals(0, result.modifiedCount) + assertNotNull(result.upsertedId) + val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1.append("_id", result.upsertedId), true) + + ctx.waitForEvents(amount = 1) + + ctx.verifyChangeEventListenerCalledForActiveDoc( + 1, + expectedEvent1) + + assertEquals( + doc1, + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) + + result = ctx.dataSynchronizer.updateMany( + ctx.namespace, + BsonDocument("name", BsonString("philip")), + BsonDocument("\$inc", BsonDocument("count", BsonInt32(1))), + UpdateOptions().upsert(true)) + + assertEquals(1, result.matchedCount) + assertEquals(1, result.modifiedCount) + assertNull(result.upsertedId) + + ctx.waitForEvents(amount = 2) + + val expectedDocAfterUpdate1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)).append("_id", doc1["_id"]) + + assertEquals( + expectedDocAfterUpdate1, + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) } @Test @@ -988,6 +1158,30 @@ class DataSynchronizerUnitTests { assertNull(ctx.findTestDocumentFromLocalCollection()) } + @Test + fun testDeleteMany() { + val ctx = harness.freshTestContext() + + ctx.reconfigure() + + var result = ctx.dataSynchronizer.deleteMany(ctx.namespace, BsonDocument()) + assertEquals(0, result.deletedCount) + assertEquals(0, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument())) + + val doc1 = BsonDocument("hello", BsonString("world")) + val doc2 = BsonDocument("goodbye", BsonString("computer")) + + ctx.dataSynchronizer.insertManyAndSync(ctx.namespace, listOf(doc1, doc2)) + + assertEquals(2, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument())) + + result = ctx.dataSynchronizer.deleteMany(ctx.namespace, BsonDocument()) + + assertEquals(2, result.deletedCount) + assertEquals(0, ctx.dataSynchronizer.count(ctx.namespace, BsonDocument())) + assertNull(ctx.dataSynchronizer.find(ctx.namespace, BsonDocument()).firstOrNull()) + } + @Test fun testConfigure() { val ctx = harness.freshTestContext(false) diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt index cafd8a1b9..8b69b222e 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt @@ -47,6 +47,7 @@ import java.lang.Exception import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.Random +import java.util.concurrent.locks.ReentrantLock class SyncUnitTestHarness : Closeable { companion object { @@ -130,28 +131,7 @@ class SyncUnitTestHarness : Closeable { } } - val eventAccumulator = mutableListOf>() - var totalEventsToAccumulate = 1 - - private open class TestChangeEventListener( - private val expectedEvent: ChangeEvent?, - private val emitEventSemaphore: Semaphore? - ) : ChangeEventListener { - override fun onEvent(documentId: BsonValue?, actualEvent: ChangeEvent) { - eventAccumulator.add(actualEvent) - try { - if (expectedEvent != null) { - compareEvents(expectedEvent, actualEvent) - Assert.assertEquals(expectedEvent.id, documentId) - } - } finally { - if (eventAccumulator.size == totalEventsToAccumulate) { - eventAccumulator.clear() - emitEventSemaphore?.release() - } - } - } - } + val waitLock = ReentrantLock() fun newDoc(key: String = "hello", value: BsonValue = BsonString("world")): BsonDocument { return BsonDocument("_id", BsonObjectId()).append(key, value) @@ -227,13 +207,37 @@ class SyncUnitTestHarness : Closeable { private fun newChangeEventListener( emitEventSemaphore: Semaphore? = null, expectedEvent: ChangeEvent? = null - ): ChangeEventListener { - return Mockito.spy(TestChangeEventListener(expectedEvent, emitEventSemaphore)) + ): DataSynchronizerTestContextImpl.TestChangeEventListener { + return Mockito.spy(DataSynchronizerTestContextImpl.TestChangeEventListener(expectedEvent, emitEventSemaphore)) } } @Suppress("UNCHECKED_CAST") private class DataSynchronizerTestContextImpl(shouldPreconfigure: Boolean = true) : DataSynchronizerTestContext { + open class TestChangeEventListener( + private val expectedEvent: ChangeEvent?, + var emitEventSemaphore: Semaphore? + ) : ChangeEventListener { + val eventAccumulator = mutableListOf>() + var totalEventsToAccumulate = 0 + + override fun onEvent(documentId: BsonValue?, actualEvent: ChangeEvent?) { + waitLock.lock() + try { + eventAccumulator.add(actualEvent!!) + if (expectedEvent != null) { + compareEvents(expectedEvent, actualEvent) + Assert.assertEquals(expectedEvent.id, documentId) + } + } finally { + if (eventAccumulator.size >= totalEventsToAccumulate) { + emitEventSemaphore?.release() + } + waitLock.unlock() + } + } + } + override val collectionMock: CoreRemoteMongoCollectionImpl = Mockito.mock(CoreRemoteMongoCollectionImpl::class.java) as CoreRemoteMongoCollectionImpl @@ -363,8 +367,15 @@ class SyncUnitTestHarness : Closeable { } override fun waitForEvents(amount: Int) { - totalEventsToAccumulate = amount - assertTrue(eventSemaphore?.tryAcquire(10, TimeUnit.SECONDS) ?: true) + waitLock.lock() + changeEventListener.totalEventsToAccumulate = amount + if (changeEventListener.totalEventsToAccumulate > changeEventListener.eventAccumulator.size) { + // means sem has been called and we need to wait for more events + eventSemaphore = Semaphore(0) + changeEventListener.emitEventSemaphore = eventSemaphore + } + waitLock.unlock() + assertTrue(changeEventListener.emitEventSemaphore?.tryAcquire(10, TimeUnit.SECONDS) ?: true) } override fun waitForError() { From e069aec2952e4635fd1ff1f98b037a3d49e721b8 Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 18:29:45 +0000 Subject: [PATCH 03/14] Reformat --- .../sync/internal/DataSynchronizer.java | 1236 ++++++++--------- 1 file changed, 618 insertions(+), 618 deletions(-) diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index a41cd12d1..a8c932a55 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -121,13 +121,13 @@ public class DataSynchronizer implements NetworkMonitor.StateListener { private ErrorListener errorListener; public DataSynchronizer( - final String instanceKey, - final CoreStitchServiceClient service, - final MongoClient localClient, - final CoreRemoteMongoClient remoteClient, - final NetworkMonitor networkMonitor, - final AuthMonitor authMonitor, - final Dispatcher eventDispatcher + final String instanceKey, + final CoreStitchServiceClient service, + final MongoClient localClient, + final CoreRemoteMongoClient remoteClient, + final NetworkMonitor networkMonitor, + final AuthMonitor authMonitor, + final Dispatcher eventDispatcher ) { this.service = service; this.localClient = localClient; @@ -141,39 +141,39 @@ public DataSynchronizer( // TODO: add back after SERVER-35421 // final MongoDatabase configDb = localClient.getDatabase("sync_config"); this.configDb = - localClient.getDatabase("sync_config" + instanceKey) - .withCodecRegistry(CodecRegistries.fromRegistries( - CodecRegistries.fromCodecs( - InstanceSynchronizationConfig.configCodec, - NamespaceSynchronizationConfig.configCodec, - CoreDocumentSynchronizationConfig.configCodec), - BsonUtils.DEFAULT_CODEC_REGISTRY)); + localClient.getDatabase("sync_config" + instanceKey) + .withCodecRegistry(CodecRegistries.fromRegistries( + CodecRegistries.fromCodecs( + InstanceSynchronizationConfig.configCodec, + NamespaceSynchronizationConfig.configCodec, + CoreDocumentSynchronizationConfig.configCodec), + BsonUtils.DEFAULT_CODEC_REGISTRY)); this.instancesColl = configDb - .getCollection("instances", InstanceSynchronizationConfig.class); + .getCollection("instances", InstanceSynchronizationConfig.class); if (instancesColl.countDocuments() != 0) { this.syncConfig = new InstanceSynchronizationConfig( - configDb, - instancesColl); + configDb, + instancesColl); instancesColl.insertOne(this.syncConfig); } else { this.syncConfig = new InstanceSynchronizationConfig( - configDb, - instancesColl, - instancesColl.find().first()); + configDb, + instancesColl, + instancesColl.find().first()); } this.instanceChangeStreamListener = new InstanceChangeStreamListenerImpl( - syncConfig, - service, - networkMonitor, - authMonitor); + syncConfig, + service, + networkMonitor, + authMonitor); for (final MongoNamespace ns : this.syncConfig.getSynchronizedNamespaces()) { this.instanceChangeStreamListener.addNamespace(ns); } this.logger = - Loggers.getLogger(String.format("DataSynchronizer-%s", instanceKey)); + Loggers.getLogger(String.format("DataSynchronizer-%s", instanceKey)); if (this.networkMonitor != null) { this.networkMonitor.addNetworkStateListener(this); } @@ -196,14 +196,14 @@ public void reloadConfig() { try { this.instanceChangeStreamListener.stop(); this.syncConfig = new InstanceSynchronizationConfig( - configDb, - instancesColl, - instancesColl.find().first()); + configDb, + instancesColl, + instancesColl.find().first()); this.instanceChangeStreamListener = new InstanceChangeStreamListenerImpl( - syncConfig, - service, - networkMonitor, - authMonitor + syncConfig, + service, + networkMonitor, + authMonitor ); this.isConfigured = false; this.stop(); @@ -219,18 +219,18 @@ public void configure(@Nonnull final MongoNamespace namespace, @Nonnull final Codec codec) { if (conflictHandler == null) { logger.warn( - "Invalid configuration: conflictHandler should not be null. " - + "The DataSynchronizer will not begin syncing until a ConflictHandler has been " - + "provided."); + "Invalid configuration: conflictHandler should not be null. " + + "The DataSynchronizer will not begin syncing until a ConflictHandler has been " + + "provided."); return; } this.errorListener = errorListener; this.syncConfig.getNamespaceConfig(namespace).configure( - conflictHandler, - changeEventListener, - codec + conflictHandler, + changeEventListener, + codec ); syncLock.lock(); @@ -260,9 +260,9 @@ public void start() { instanceChangeStreamListener.start(); if (syncThread == null) { syncThread = new Thread(new DataSynchronizerRunner( - new WeakReference<>(this), - networkMonitor, - logger)); + new WeakReference<>(this), + networkMonitor, + logger)); } if (syncThreadEnabled && !isRunning) { syncThread.start(); @@ -359,21 +359,21 @@ public boolean doSyncPass() { logicalT++; logger.info(String.format( - Locale.US, - "t='%d': doSyncPass START", - logicalT)); + Locale.US, + "t='%d': doSyncPass START", + logicalT)); if (networkMonitor == null || !networkMonitor.isConnected()) { logger.info(String.format( - Locale.US, - "t='%d': doSyncPass END - Network disconnected", - logicalT)); + Locale.US, + "t='%d': doSyncPass END - Network disconnected", + logicalT)); return false; } if (authMonitor == null || !authMonitor.isLoggedIn()) { logger.info(String.format( - Locale.US, - "t='%d': doSyncPass END - Logged out", - logicalT)); + Locale.US, + "t='%d': doSyncPass END - Logged out", + logicalT)); return false; } @@ -386,9 +386,9 @@ public boolean doSyncPass() { } logger.info(String.format( - Locale.US, - "t='%d': doSyncPass END", - logicalT)); + Locale.US, + "t='%d': doSyncPass END", + logicalT)); } finally { syncLock.unlock(); } @@ -405,18 +405,18 @@ public boolean doSyncPass() { */ private void syncRemoteToLocal() { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteToLocal START", - logicalT)); + Locale.US, + "t='%d': syncRemoteToLocal START", + logicalT)); // 2. Run remote to local (R2L) sync routine for (final NamespaceSynchronizationConfig nsConfig : syncConfig) { final Map> remoteChangeEvents = - getEventsForNamespace(nsConfig.getNamespace()); + getEventsForNamespace(nsConfig.getNamespace()); final Set unseenIds = nsConfig.getStaleDocumentIds(); final Set latestDocumentsFromStale = - getLatestDocumentsForStaleFromRemote(nsConfig, unseenIds); + getLatestDocumentsForStaleFromRemote(nsConfig, unseenIds); final Map latestDocumentMap = new HashMap<>(); for (final BsonDocument latestDocument : latestDocumentsFromStale) { @@ -425,16 +425,16 @@ private void syncRemoteToLocal() { // a. For each unprocessed change event for (final Map.Entry> eventEntry : - remoteChangeEvents.entrySet()) { + remoteChangeEvents.entrySet()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteToLocal consuming event of type: %s", - logicalT, - eventEntry.getValue().getOperationType())); + Locale.US, + "t='%d': syncRemoteToLocal consuming event of type: %s", + logicalT, + eventEntry.getValue().getOperationType())); // i. Find the corresponding local document config. final CoreDocumentSynchronizationConfig docConfig = - nsConfig.getSynchronizedDocument(eventEntry.getKey().asDocument().get("_id")); + nsConfig.getSynchronizedDocument(eventEntry.getKey().asDocument().get("_id")); if (docConfig == null || docConfig.isPaused()) { // Not interested in this event. @@ -451,7 +451,7 @@ private void syncRemoteToLocal() { // remote copy. for (final BsonValue docId : unseenIds) { final CoreDocumentSynchronizationConfig docConfig = - nsConfig.getSynchronizedDocument(docId); + nsConfig.getSynchronizedDocument(docId); if (docConfig == null || docConfig.isPaused()) { // means we aren't actually synchronizing on this remote doc continue; @@ -459,14 +459,14 @@ private void syncRemoteToLocal() { if (latestDocumentMap.containsKey(docId)) { syncRemoteChangeEventToLocal( - nsConfig, - docConfig, - changeEventForLocalReplace( - nsConfig.getNamespace(), - docId, - latestDocumentMap.get(docId), - false - )); + nsConfig, + docConfig, + changeEventForLocalReplace( + nsConfig.getNamespace(), + docId, + latestDocumentMap.get(docId), + false + )); docConfig.setStale(false); } @@ -478,22 +478,22 @@ private void syncRemoteToLocal() { unseenIds.removeAll(latestDocumentMap.keySet()); for (final BsonValue unseenId : unseenIds) { final CoreDocumentSynchronizationConfig docConfig = - nsConfig.getSynchronizedDocument(unseenId); + nsConfig.getSynchronizedDocument(unseenId); if (docConfig == null - || docConfig.getLastKnownRemoteVersion() == null - || docConfig.isPaused()) { + || docConfig.getLastKnownRemoteVersion() == null + || docConfig.isPaused()) { // means we aren't actually synchronizing on this remote doc continue; } syncRemoteChangeEventToLocal( - nsConfig, - docConfig, - changeEventForLocalDelete( - nsConfig.getNamespace(), - unseenId, - docConfig.hasUncommittedWrites() - )); + nsConfig, + docConfig, + changeEventForLocalDelete( + nsConfig.getNamespace(), + unseenId, + docConfig.hasUncommittedWrites() + )); docConfig.setStale(false); } @@ -501,9 +501,9 @@ private void syncRemoteToLocal() { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteToLocal END", - logicalT)); + Locale.US, + "t='%d': syncRemoteToLocal END", + logicalT)); } /** @@ -514,61 +514,61 @@ private void syncRemoteToLocal() { * @param remoteChangeEvent the remote change event to synchronize into the local database. */ private void syncRemoteChangeEventToLocal( - final NamespaceSynchronizationConfig nsConfig, - final CoreDocumentSynchronizationConfig docConfig, - final ChangeEvent remoteChangeEvent + final NamespaceSynchronizationConfig nsConfig, + final CoreDocumentSynchronizationConfig docConfig, + final ChangeEvent remoteChangeEvent ) { if (docConfig.hasUncommittedWrites() && docConfig.getLastResolution() == logicalT) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal have writes for %s but happened at same t; " - + "waiting until next pass", - logicalT, - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal have writes for %s but happened at same t; " + + "waiting until next pass", + logicalT, + docConfig.getDocumentId())); return; } logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s processing operation='%s'", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - remoteChangeEvent.getOperationType().toString())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s processing operation='%s'", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + remoteChangeEvent.getOperationType().toString())); final DocumentVersionInfo currentRemoteVersionInfo; try { currentRemoteVersionInfo = DocumentVersionInfo - .getRemoteVersionInfo(remoteChangeEvent.getFullDocument()); + .getRemoteVersionInfo(remoteChangeEvent.getFullDocument()); } catch (final Exception e) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } if (currentRemoteVersionInfo.hasVersion() - && currentRemoteVersionInfo.getVersion().getSyncProtocolVersion() != 1) { + && currentRemoteVersionInfo.getVersion().getSyncProtocolVersion() != 1) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document with an unsupported synchronization protocol version " - + "%d; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document with an unsupported synchronization protocol version " + + "%d; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); return; } @@ -580,12 +580,12 @@ private void syncRemoteChangeEventToLocal( if (docConfig.hasCommittedVersion(currentRemoteVersionInfo)) { // Skip this event since we generated it. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event was " - + "generated by us; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event was " + + "generated by us; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -598,41 +598,41 @@ private void syncRemoteChangeEventToLocal( case UPDATE: case INSERT: logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s replacing local with " - + "remote document with new version as there are no local pending writes: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - remoteChangeEvent.getFullDocument())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s replacing local with " + + "remote document with new version as there are no local pending writes: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + remoteChangeEvent.getFullDocument())); replaceOrUpsertOneFromRemote( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - remoteChangeEvent.getFullDocument(), - DocumentVersionInfo.getDocumentVersionDoc(remoteChangeEvent.getFullDocument())); + nsConfig.getNamespace(), + docConfig.getDocumentId(), + remoteChangeEvent.getFullDocument(), + DocumentVersionInfo.getDocumentVersionDoc(remoteChangeEvent.getFullDocument())); return; case DELETE: logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s deleting local as " - + "there are no local pending writes", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s deleting local as " + + "there are no local pending writes", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); deleteOneFromRemote( - nsConfig.getNamespace(), - docConfig.getDocumentId()); + nsConfig.getNamespace(), + docConfig.getDocumentId()); return; default: emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s unknown operation type " - + "occurred on the document: %s; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - remoteChangeEvent.getOperationType().toString())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s unknown operation type " + + "occurred on the document: %s; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + remoteChangeEvent.getOperationType().toString())); return; } } @@ -644,19 +644,19 @@ private void syncRemoteChangeEventToLocal( // iv. Otherwise, check if the version info of the incoming remote change event is different // from the version of the local document. final DocumentVersionInfo lastKnownLocalVersionInfo = DocumentVersionInfo - .getLocalVersionInfo(docConfig); + .getLocalVersionInfo(docConfig); // 1. If both the local document version and the remote change event version are empty, drop // the event. The absence of a version is effectively a version, and the pending write will // set a version on the next L2R pass if it’s not a delete. if (!lastKnownLocalVersionInfo.hasVersion() && !currentRemoteVersionInfo.hasVersion()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " - + "empty version but a write is pending; waiting for next L2R pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " + + "empty version but a write is pending; waiting for next L2R pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -666,12 +666,12 @@ private void syncRemoteChangeEventToLocal( // adhering to the mobile sync protocol. if (!lastKnownLocalVersionInfo.hasVersion() || !currentRemoteVersionInfo.hasVersion()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " - + "empty version but a write is pending; waiting for next L2R pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " + + "empty version but a write is pending; waiting for next L2R pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict(nsConfig.getNamespace(), docConfig, remoteChangeEvent); return; } @@ -686,28 +686,28 @@ private void syncRemoteChangeEventToLocal( // i. drop the event if the version counter of the remote event less than or equal to the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " - + "is stale; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " + + "is stale; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } else { // ii. raise a conflict if the version counter of the remote event is greater than the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "has higher counter than local version but a write is pending; " - + "raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "has higher counter than local version but a write is pending; " + + "raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); return; } } @@ -716,27 +716,27 @@ private void syncRemoteChangeEventToLocal( // fetch the latest version (this is to guard against the case where the unprocessed // change event is stale). final BsonDocument newestRemoteDocument = this.getRemoteCollection(nsConfig.getNamespace()) - .find(new Document("_id", docConfig.getDocumentId())).first(); + .find(new Document("_id", docConfig.getDocumentId())).first(); if (newestRemoteDocument == null) { // i. If the document is not found with a remote lookup, this means the document was // deleted remotely, so raise a conflict using a synthesized delete event as the remote // change event. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "stale and latest document lookup indicates a remote delete occurred, but " - + "a write is pending; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "stale and latest document lookup indicates a remote delete occurred, but " + + "a write is pending; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( + nsConfig.getNamespace(), + docConfig, + changeEventForLocalDelete( nsConfig.getNamespace(), - docConfig, - changeEventForLocalDelete( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - docConfig.hasUncommittedWrites())); + docConfig.getDocumentId(), + docConfig.hasUncommittedWrites())); return; } @@ -744,18 +744,18 @@ private void syncRemoteChangeEventToLocal( final DocumentVersionInfo newestRemoteVersionInfo; try { newestRemoteVersionInfo = DocumentVersionInfo - .getRemoteVersionInfo(newestRemoteDocument); + .getRemoteVersionInfo(newestRemoteDocument); } catch (final Exception e) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -763,16 +763,16 @@ private void syncRemoteChangeEventToLocal( // to the GUID of the local document, drop the event. We’re believed to be behind in // the change stream at this point. if (newestRemoteVersionInfo.hasVersion() - && newestRemoteVersionInfo.getVersion().getInstanceId() - .equals(localVersion.instanceId)) { + && newestRemoteVersionInfo.getVersion().getInstanceId() + .equals(localVersion.instanceId)) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates that this is a stale event; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates that this is a stale event; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -782,21 +782,21 @@ private void syncRemoteChangeEventToLocal( // event. This means the remote document is a legitimately new document and we should // handle the conflict. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates a remote replace occurred, but a local write is pending; raising " - + "conflict with synthesized replace event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates a remote replace occurred, but a local write is pending; raising " + + "conflict with synthesized replace event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( + nsConfig.getNamespace(), + docConfig, + changeEventForLocalReplace( nsConfig.getNamespace(), - docConfig, - changeEventForLocalReplace( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - newestRemoteDocument, - docConfig.hasUncommittedWrites())); + docConfig.getDocumentId(), + newestRemoteDocument, + docConfig.hasUncommittedWrites())); } /** @@ -806,15 +806,15 @@ private void syncRemoteChangeEventToLocal( */ private void syncLocalToRemote() { logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote START", - logicalT)); + Locale.US, + "t='%d': syncLocalToRemote START", + logicalT)); // 1. Run local to remote (L2R) sync routine // Search for modifications in each namespace. for (final NamespaceSynchronizationConfig nsConfig : syncConfig) { final CoreRemoteMongoCollection remoteColl = - getRemoteCollection(nsConfig.getNamespace()); + getRemoteCollection(nsConfig.getNamespace()); // a. For each document that has local writes pending for (final CoreDocumentSynchronizationConfig docConfig : nsConfig) { @@ -823,25 +823,25 @@ private void syncLocalToRemote() { } if (docConfig.getLastResolution() == logicalT) { logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s has writes from current logicalT; " - + "waiting until next pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s has writes from current logicalT; " + + "waiting until next pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); continue; } // i. Retrieve the change event for this local document in the local config metadata final ChangeEvent localChangeEvent = - docConfig.getLastUncommittedChangeEvent(); + docConfig.getLastUncommittedChangeEvent(); logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s processing operation='%s'", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - localChangeEvent.getOperationType().toString())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s processing operation='%s'", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + localChangeEvent.getOperationType().toString())); final BsonDocument localDoc = localChangeEvent.getFullDocument(); final BsonDocument docFilter = getDocumentIdFilter(docConfig.getDocumentId()); @@ -854,32 +854,32 @@ private void syncLocalToRemote() { boolean remoteDocumentFetched = false; final DocumentVersionInfo localVersionInfo = - DocumentVersionInfo.getLocalVersionInfo(docConfig); + DocumentVersionInfo.getLocalVersionInfo(docConfig); final BsonDocument nextVersion; // ii. Check if the internal remote change stream listener has an unprocessed event for // this document. final ChangeEvent unprocessedRemoteEvent = - instanceChangeStreamListener.getUnprocessedEventForDocumentId( - nsConfig.getNamespace(), - docConfig.getDocumentId()); + instanceChangeStreamListener.getUnprocessedEventForDocumentId( + nsConfig.getNamespace(), + docConfig.getDocumentId()); if (unprocessedRemoteEvent != null) { final DocumentVersionInfo unprocessedEventVersion; try { unprocessedEventVersion = DocumentVersionInfo - .getRemoteVersionInfo(unprocessedRemoteEvent.getFullDocument()); + .getRemoteVersionInfo(unprocessedRemoteEvent.getFullDocument()); } catch (final Exception e) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -889,12 +889,12 @@ private void syncLocalToRemote() { if (!docConfig.hasCommittedVersion(unprocessedEventVersion)) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "unprocessed change event for document; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "unprocessed change event for document; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } // 2. Otherwise, the unprocessed event can be safely dropped and ignored in future R2L @@ -914,31 +914,31 @@ private void syncLocalToRemote() { // a. Insert document into remote database try { remoteColl.insertOne( - withNewVersion(localChangeEvent.getFullDocument(), nextVersion)); + withNewVersion(localChangeEvent.getFullDocument(), nextVersion)); } catch (final StitchServiceException ex) { // b. If an error happens: // i. That is not a duplicate key exception, report an error to the error listener. if (ex.getErrorCode() != StitchServiceErrorCode.MONGODB_ERROR - || !ex.getMessage().contains("E11000")) { + || !ex.getMessage().contains("E11000")) { this.emitError(docConfig, String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), ex); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), ex); continue; } // ii. Otherwise record that a conflict has occurred. logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s duplicate key exception on " - + "insert; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s duplicate key exception on " + + "insert; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); isConflicted = true; } break; @@ -949,12 +949,12 @@ private void syncLocalToRemote() { case REPLACE: { if (localDoc == null) { final IllegalStateException illegalStateException = new IllegalStateException( - "expected document to exist for local replace change event: %s"); + "expected document to exist for local replace change event: %s"); emitError( - docConfig, - illegalStateException.getMessage(), - illegalStateException + docConfig, + illegalStateException.getMessage(), + illegalStateException ); continue; } @@ -967,21 +967,21 @@ private void syncLocalToRemote() { final RemoteUpdateResult result; try { result = remoteColl.updateOne( - localVersionInfo.getFilter(), - nextDoc); + localVersionInfo.getFilter(), + nextDoc); } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. this.emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "replacing: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + "replacing: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -989,12 +989,12 @@ private void syncLocalToRemote() { if (result.getMatchedCount() == 0) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "replaced document or document deleted; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "replaced document or document deleted; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } break; } @@ -1003,11 +1003,11 @@ private void syncLocalToRemote() { case UPDATE: { if (localDoc == null) { final IllegalStateException illegalStateException = new IllegalStateException( - "expected document to exist for local update change event"); + "expected document to exist for local update change event"); emitError( - docConfig, - illegalStateException.getMessage(), - illegalStateException + docConfig, + illegalStateException.getMessage(), + illegalStateException ); continue; } @@ -1021,7 +1021,7 @@ private void syncLocalToRemote() { if (!localChangeEvent.getUpdateDescription().getUpdatedFields().isEmpty()) { final BsonDocument sets = new BsonDocument(); for (final Map.Entry fieldValue : - localChangeEvent.getUpdateDescription().getUpdatedFields().entrySet()) { + localChangeEvent.getUpdateDescription().getUpdatedFields().entrySet()) { sets.put(fieldValue.getKey(), fieldValue.getValue()); } sets.put(DOCUMENT_VERSION_FIELD, nextVersion); @@ -1030,7 +1030,7 @@ private void syncLocalToRemote() { if (!localChangeEvent.getUpdateDescription().getRemovedFields().isEmpty()) { final BsonDocument unsets = new BsonDocument(); for (final String field : - localChangeEvent.getUpdateDescription().getRemovedFields()) { + localChangeEvent.getUpdateDescription().getRemovedFields()) { unsets.put(field, BsonBoolean.TRUE); } translatedUpdate.put("$unset", unsets); @@ -1039,21 +1039,21 @@ private void syncLocalToRemote() { final RemoteUpdateResult result; try { result = remoteColl.updateOne( - localVersionInfo.getFilter(), - translatedUpdate.isEmpty() ? nextDoc : translatedUpdate); + localVersionInfo.getFilter(), + translatedUpdate.isEmpty() ? nextDoc : translatedUpdate); } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "updating: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + "updating: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -1061,12 +1061,12 @@ private void syncLocalToRemote() { // c. If no documents are matched, record that a conflict has occurred. isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "updated document or document deleted; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "updated document or document deleted; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } break; } @@ -1081,16 +1081,16 @@ private void syncLocalToRemote() { } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + " deleting: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + " deleting: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -1101,12 +1101,12 @@ private void syncLocalToRemote() { if (remoteDocument != null) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "removed document; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "removed document; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } else { // d. Desynchronize the document if there is no conflict, or if fetching a remote // document after the conflict is raised returns no remote document. @@ -1120,15 +1120,15 @@ private void syncLocalToRemote() { default: emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s unknown operation " - + "type occurred on the document: %s; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - localChangeEvent.getOperationType().toString()) + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s unknown operation " + + "type occurred on the document: %s; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + localChangeEvent.getOperationType().toString()) ); continue; } @@ -1138,12 +1138,12 @@ private void syncLocalToRemote() { logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s conflict=%s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - isConflicted)); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s conflict=%s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + isConflicted)); if (!isConflicted) { // iv. If no conflict has occurred, move on to the remote to local sync routine. @@ -1151,15 +1151,15 @@ private void syncLocalToRemote() { // TODO(STITCH-1972): This event may contain old version info. We should be filtering out // the version anyway from local and remote events. final ChangeEvent committedEvent = - docConfig.getLastUncommittedChangeEvent(); + docConfig.getLastUncommittedChangeEvent(); emitEvent(docConfig.getDocumentId(), new ChangeEvent<>( - committedEvent.getId(), - committedEvent.getOperationType(), - committedEvent.getFullDocument(), - committedEvent.getNamespace(), - committedEvent.getDocumentKey(), - committedEvent.getUpdateDescription(), - false)); + committedEvent.getId(), + committedEvent.getOperationType(), + committedEvent.getFullDocument(), + committedEvent.getNamespace(), + committedEvent.getDocumentKey(), + committedEvent.getUpdateDescription(), + false)); docConfig.setPendingWritesComplete(nextVersion); } else { @@ -1169,27 +1169,27 @@ private void syncLocalToRemote() { final ChangeEvent remoteChangeEvent; if (!remoteDocumentFetched) { remoteChangeEvent = - getSynthesizedRemoteChangeEventForDocument(remoteColl, docConfig.getDocumentId()); + getSynthesizedRemoteChangeEventForDocument(remoteColl, docConfig.getDocumentId()); } else { remoteChangeEvent = - getSynthesizedRemoteChangeEventForDocument( - remoteColl.getNamespace(), - docConfig.getDocumentId(), - remoteDocument); + getSynthesizedRemoteChangeEventForDocument( + remoteColl.getNamespace(), + docConfig.getDocumentId(), + remoteDocument); } resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); } } } logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote END", - logicalT)); + Locale.US, + "t='%d': syncLocalToRemote END", + logicalT)); // 3. If there are still local writes pending for the document, it will go through the L2R // phase on a subsequent pass and try to commit changes again. @@ -1223,7 +1223,7 @@ public Object call() { this.logger.error(msg); this.logger.error( - String.format("Setting document %s to frozen", docConfig.getDocumentId())); + String.format("Setting document %s to frozen", docConfig.getDocumentId())); } @@ -1238,64 +1238,64 @@ public Object call() { * @param remoteEvent the remote change event that is conflicting. */ private void resolveConflict( - final MongoNamespace namespace, - final CoreDocumentSynchronizationConfig docConfig, - final ChangeEvent remoteEvent + final MongoNamespace namespace, + final CoreDocumentSynchronizationConfig docConfig, + final ChangeEvent remoteEvent ) { if (this.syncConfig.getNamespaceConfig(namespace).getConflictHandler() == null) { logger.warn(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s no conflict resolver set; cannot " - + "resolve yet", - logicalT, - namespace, - docConfig.getDocumentId())); + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s no conflict resolver set; cannot " + + "resolve yet", + logicalT, + namespace, + docConfig.getDocumentId())); return; } logger.info(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s resolving conflict between localOp=%s " - + "remoteOp=%s", - logicalT, - namespace, - docConfig.getDocumentId(), - docConfig.getLastUncommittedChangeEvent().getOperationType(), - remoteEvent.getOperationType())); + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s resolving conflict between localOp=%s " + + "remoteOp=%s", + logicalT, + namespace, + docConfig.getDocumentId(), + docConfig.getLastUncommittedChangeEvent().getOperationType(), + remoteEvent.getOperationType())); // 2. Based on the result of the handler determine the next state of the document. final Object resolvedDocument; final ChangeEvent transformedRemoteEvent; try { final ChangeEvent transformedLocalEvent = ChangeEvent.transformChangeEventForUser( - docConfig.getLastUncommittedChangeEvent(), - syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); + docConfig.getLastUncommittedChangeEvent(), + syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); transformedRemoteEvent = - ChangeEvent.transformChangeEventForUser( - remoteEvent, - syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); + ChangeEvent.transformChangeEventForUser( + remoteEvent, + syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); resolvedDocument = resolveConflictWithResolver( - this.syncConfig.getNamespaceConfig(namespace).getConflictHandler(), - docConfig.getDocumentId(), - transformedLocalEvent, - transformedRemoteEvent); + this.syncConfig.getNamespaceConfig(namespace).getConflictHandler(), + docConfig.getDocumentId(), + transformedLocalEvent, + transformedRemoteEvent); } catch (final Exception ex) { logger.error(String.format( + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s resolution exception: %s", + logicalT, + namespace, + docConfig.getDocumentId(), + ex)); + emitError(docConfig, + String.format( Locale.US, "t='%d': resolveConflict ns=%s documentId=%s resolution exception: %s", logicalT, namespace, docConfig.getDocumentId(), - ex)); - emitError(docConfig, - String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s resolution exception: %s", - logicalT, - namespace, - docConfig.getDocumentId(), - ex), - ex); + ex), + ex); return; } @@ -1308,37 +1308,37 @@ private void resolveConflict( } else { try { final DocumentVersionInfo remoteVersionInfo = DocumentVersionInfo - .getRemoteVersionInfo(remoteEvent.getFullDocument()); + .getRemoteVersionInfo(remoteEvent.getFullDocument()); remoteVersion = remoteVersionInfo.getVersionDoc(); } catch (final Exception e) { desyncDocumentFromRemote(namespace, docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - namespace, - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + namespace, + docConfig.getDocumentId())); return; } } final boolean acceptRemote = - (remoteEvent.getFullDocument() == null && resolvedDocument == null) - || (remoteEvent.getFullDocument() != null - && transformedRemoteEvent.getFullDocument().equals(resolvedDocument)); + (remoteEvent.getFullDocument() == null && resolvedDocument == null) + || (remoteEvent.getFullDocument() != null + && transformedRemoteEvent.getFullDocument().equals(resolvedDocument)); // a. If the resolved document is null: if (resolvedDocument == null) { logger.info(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s deleting local and remote with remote " - + "version acknowledged", - logicalT, - namespace, - docConfig.getDocumentId())); + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s deleting local and remote with remote " + + "version acknowledged", + logicalT, + namespace, + docConfig.getDocumentId())); if (acceptRemote) { // i. If the remote event was a DELETE, delete the document locally, desynchronize the @@ -1355,37 +1355,37 @@ private void resolveConflict( // Update the document locally which will keep the pending writes but with // a new version next time around. @SuppressWarnings("unchecked") final BsonDocument docForStorage = - BsonUtils.documentToBsonDocument( - resolvedDocument, - syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); + BsonUtils.documentToBsonDocument( + resolvedDocument, + syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); logger.info(String.format( - Locale.US, - "t='%d': resolveConflict ns=%s documentId=%s replacing local with resolved document " - + "with remote version acknowledged: %s", - logicalT, - namespace, - docConfig.getDocumentId(), - docForStorage.toJson())); + Locale.US, + "t='%d': resolveConflict ns=%s documentId=%s replacing local with resolved document " + + "with remote version acknowledged: %s", + logicalT, + namespace, + docConfig.getDocumentId(), + docForStorage.toJson())); if (acceptRemote) { // i. If the remote document is equal to the resolved document, replace the document // locally, mark the document as having no pending writes, and emit a REPLACE change // event if the document had not existed prior, or UPDATE if it had. replaceOrUpsertOneFromRemote( - namespace, - docConfig.getDocumentId(), - docForStorage, - remoteVersion); + namespace, + docConfig.getDocumentId(), + docForStorage, + remoteVersion); } else { // ii. Otherwise, replace the local document with the resolved document locally, mark that // there are pending writes for this document, and emit an UPDATE change event, or a // DELETE change event (if the remoteEvent's operation type was DELETE). updateOrUpsertOneFromResolution( - namespace, - docConfig.getDocumentId(), - docForStorage, - remoteVersion, - remoteEvent); + namespace, + docConfig.getDocumentId(), + docForStorage, + remoteVersion, + remoteEvent); } } } @@ -1402,15 +1402,15 @@ private void resolveConflict( */ @SuppressWarnings("unchecked") private static Object resolveConflictWithResolver( - final ConflictHandler conflictResolver, - final BsonValue documentId, - final ChangeEvent localEvent, - final ChangeEvent remoteEvent + final ConflictHandler conflictResolver, + final BsonValue documentId, + final ChangeEvent localEvent, + final ChangeEvent remoteEvent ) { return conflictResolver.resolveConflict( - documentId, - localEvent, - remoteEvent); + documentId, + localEvent, + remoteEvent); } /** @@ -1421,13 +1421,13 @@ private static Object resolveConflictWithResolver( * @return a synthesized change event for a remote document. */ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( - final CoreRemoteMongoCollection remoteColl, - final BsonValue documentId + final CoreRemoteMongoCollection remoteColl, + final BsonValue documentId ) { return getSynthesizedRemoteChangeEventForDocument( - remoteColl.getNamespace(), - documentId, - remoteColl.find(getDocumentIdFilter(documentId)).first()); + remoteColl.getNamespace(), + documentId, + remoteColl.find(getDocumentIdFilter(documentId)).first()); } /** @@ -1439,9 +1439,9 @@ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( * @return a synthesized change event for a remote document. */ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( - final MongoNamespace ns, - final BsonValue documentId, - final BsonDocument document + final MongoNamespace ns, + final BsonValue documentId, + final BsonDocument document ) { // a. When the document is looked up, if it cannot be found the synthesized change event is a // DELETE, otherwise it's a REPLACE. @@ -1486,7 +1486,7 @@ public Set getSynchronizedNamespaces() { * @return the set of synchronized documents in a namespace. */ public Set getSynchronizedDocuments( - final MongoNamespace namespace + final MongoNamespace namespace ) { return this.syncConfig.getSynchronizedDocuments(namespace); } @@ -1512,7 +1512,7 @@ public Set getPausedDocumentIds(final MongoNamespace namespace) { final Set pausedDocumentIds = new HashSet<>(); for (final CoreDocumentSynchronizationConfig config : - this.syncConfig.getSynchronizedDocuments(namespace)) { + this.syncConfig.getSynchronizedDocuments(namespace)) { if (config.isPaused()) { pausedDocumentIds.add(config.getDocumentId()); } @@ -1529,8 +1529,8 @@ public Set getPausedDocumentIds(final MongoNamespace namespace) { * @param documentId the _id of the document. */ public void syncDocumentFromRemote( - final MongoNamespace namespace, - final BsonValue documentId + final MongoNamespace namespace, + final BsonValue documentId ) { syncConfig.addSynchronizedDocument(namespace, documentId); triggerListeningToNamespace(namespace); @@ -1544,8 +1544,8 @@ public void syncDocumentFromRemote( * @param documentId the _id of the document. */ public void desyncDocumentFromRemote( - final MongoNamespace namespace, - final BsonValue documentId + final MongoNamespace namespace, + final BsonValue documentId ) { syncConfig.removeSynchronizedDocument(namespace, documentId); getLocalCollection(namespace).deleteOne(getDocumentIdFilter(documentId)); @@ -1565,8 +1565,8 @@ public void desyncDocumentFromRemote( * could not be found or there was an error resuming */ boolean resumeSyncForDocument( - final MongoNamespace namespace, - final BsonValue documentId + final MongoNamespace namespace, + final BsonValue documentId ) { if (namespace == null || documentId == null) { return false; @@ -1576,7 +1576,7 @@ boolean resumeSyncForDocument( final CoreDocumentSynchronizationConfig config; if ((namespaceSynchronizationConfig = syncConfig.getNamespaceConfig(namespace)) == null - || (config = namespaceSynchronizationConfig.getSynchronizedDocument(documentId)) == null) { + || (config = namespaceSynchronizationConfig.getSynchronizedDocument(documentId)) == null) { return false; } @@ -1615,29 +1615,29 @@ long count(final MongoNamespace namespace, final Bson filter, final CountOptions } Collection find( - final MongoNamespace namespace, - final BsonDocument filter + final MongoNamespace namespace, + final BsonDocument filter ) { return getLocalCollection(namespace) - .find(filter) - .into(new ArrayList<>()); + .find(filter) + .into(new ArrayList<>()); } public Collection find( - final MongoNamespace namespace, - final BsonDocument filter, - final int limit, - final BsonDocument projection, - final BsonDocument sort, - final Class resultClass, - final CodecRegistry codecRegistry + final MongoNamespace namespace, + final BsonDocument filter, + final int limit, + final BsonDocument projection, + final BsonDocument sort, + final Class resultClass, + final CodecRegistry codecRegistry ) { return getLocalCollection(namespace, resultClass, codecRegistry) - .find(filter) - .limit(limit) - .projection(projection) - .sort(sort) - .into(new ArrayList()); + .find(filter) + .limit(limit) + .projection(projection) + .sort(sort) + .into(new ArrayList()); } /** @@ -1647,8 +1647,8 @@ public Collection find( * @return an iterable containing the result of the aggregation operation */ AggregateIterable aggregate( - final MongoNamespace namespace, - final List pipeline) { + final MongoNamespace namespace, + final List pipeline) { return aggregate(namespace, pipeline, BsonDocument.class); } @@ -1661,9 +1661,9 @@ AggregateIterable aggregate( * @return an iterable containing the result of the aggregation operation */ AggregateIterable aggregate( - final MongoNamespace namespace, - final List pipeline, - final Class resultClass) { + final MongoNamespace namespace, + final List pipeline, + final Class resultClass) { return getLocalCollection(namespace).aggregate(pipeline, resultClass); } @@ -1679,8 +1679,8 @@ void insertOneAndSync(final MongoNamespace namespace, final BsonDocument documen final BsonValue documentId = BsonUtils.getDocumentId(document); final ChangeEvent event = changeEventForLocalInsert(namespace, document, true); final CoreDocumentSynchronizationConfig config = syncConfig.addSynchronizedDocument( - namespace, - documentId + namespace, + documentId ); config.setSomePendingWrites(logicalT, event); triggerListeningToNamespace(namespace); @@ -1699,8 +1699,8 @@ void insertManyAndSync(final MongoNamespace namespace, final BsonValue documentId = BsonUtils.getDocumentId(document); final ChangeEvent event = changeEventForLocalInsert(namespace, document, true); final CoreDocumentSynchronizationConfig config = syncConfig.addSynchronizedDocument( - namespace, - documentId + namespace, + documentId ); config.setSomePendingWrites(logicalT, event); emitEvent(documentId, event); @@ -1730,10 +1730,10 @@ UpdateResult updateOne(final MongoNamespace namespace, final Bson filter, final * @return the result of the update one operation */ UpdateResult updateOne( - final MongoNamespace namespace, - final Bson filter, - final Bson update, - final UpdateOptions updateOptions) { + final MongoNamespace namespace, + final Bson filter, + final Bson update, + final UpdateOptions updateOptions) { final MongoCollection localCollection = getLocalCollection(namespace); final BsonDocument documentBeforeUpdate = getLocalCollection(namespace).find(filter).first(); @@ -1743,14 +1743,14 @@ UpdateResult updateOne( } final BsonDocument documentAfterUpdate = localCollection.findOneAndUpdate( - filter, - update, - new FindOneAndUpdateOptions() - .collation(updateOptions.getCollation()) - .upsert(updateOptions.isUpsert()) - .bypassDocumentValidation(updateOptions.getBypassDocumentValidation()) - .arrayFilters(updateOptions.getArrayFilters()) - .returnDocument(ReturnDocument.AFTER)); + filter, + update, + new FindOneAndUpdateOptions() + .collation(updateOptions.getCollation()) + .upsert(updateOptions.isUpsert()) + .bypassDocumentValidation(updateOptions.getBypassDocumentValidation()) + .arrayFilters(updateOptions.getArrayFilters()) + .returnDocument(ReturnDocument.AFTER)); if (documentAfterUpdate == null) { return UpdateResult.acknowledged(0, 0L, null); @@ -1766,11 +1766,11 @@ UpdateResult updateOne( } else { config = syncConfig.getSynchronizedDocument(namespace, documentId); event = changeEventForLocalUpdate( - namespace, - BsonUtils.getDocumentId(documentAfterUpdate), - ChangeEvent.UpdateDescription.diff(documentBeforeUpdate, documentAfterUpdate), - documentAfterUpdate, - true); + namespace, + BsonUtils.getDocumentId(documentAfterUpdate), + ChangeEvent.UpdateDescription.diff(documentBeforeUpdate, documentAfterUpdate), + documentAfterUpdate, + true); } config.setSomePendingWrites(logicalT, event); @@ -1802,22 +1802,22 @@ UpdateResult updateMany(final MongoNamespace namespace, * @return the result of the update many operation */ UpdateResult updateMany( - final MongoNamespace namespace, - final Bson filter, - final Bson update, - final UpdateOptions updateOptions) { + final MongoNamespace namespace, + final Bson filter, + final Bson update, + final UpdateOptions updateOptions) { Map idToBeforeDocumentMap = new HashMap<>(); this.getLocalCollection(namespace) - .find(filter) - .forEach(new Block() { - @Override - public void apply(@NonNull final BsonDocument bsonDocument) { - idToBeforeDocumentMap.put(BsonUtils.getDocumentId(bsonDocument), bsonDocument); - } - }); + .find(filter) + .forEach(new Block() { + @Override + public void apply(@NonNull final BsonDocument bsonDocument) { + idToBeforeDocumentMap.put(BsonUtils.getDocumentId(bsonDocument), bsonDocument); + } + }); final UpdateResult result = this.getLocalCollection(namespace) - .updateMany(filter, update, updateOptions); + .updateMany(filter, update, updateOptions); this.getLocalCollection(namespace).find(filter).forEach(new Block() { @Override @@ -1826,7 +1826,7 @@ public void apply(@NonNull final BsonDocument afterDocument) { final BsonValue documentId = BsonUtils.getDocumentId(afterDocument); if ((beforeDocument = idToBeforeDocumentMap.get(documentId)) == null - && !updateOptions.isUpsert()) { + && !updateOptions.isUpsert()) { return; } @@ -1839,11 +1839,11 @@ public void apply(@NonNull final BsonDocument afterDocument) { } else { config = syncConfig.getSynchronizedDocument(namespace, documentId); event = changeEventForLocalUpdate( - namespace, - documentId, - ChangeEvent.UpdateDescription.diff(beforeDocument, afterDocument), - afterDocument, - true); + namespace, + documentId, + ChangeEvent.UpdateDescription.diff(beforeDocument, afterDocument), + afterDocument, + true); } config.setSomePendingWrites(logicalT, event); @@ -1863,40 +1863,40 @@ public void apply(@NonNull final BsonDocument afterDocument) { * @param document the replacement document. */ private void updateOrUpsertOneFromResolution( - final MongoNamespace namespace, - final BsonValue documentId, - final BsonDocument document, - final BsonDocument atVersion, - final ChangeEvent remoteEvent + final MongoNamespace namespace, + final BsonValue documentId, + final BsonDocument document, + final BsonDocument atVersion, + final ChangeEvent remoteEvent ) { // TODO: lock down id final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return; } final BsonDocument documentAfterUpdate = getLocalCollection(namespace) - .findOneAndReplace( - getDocumentIdFilter(documentId), - document, - new FindOneAndReplaceOptions().upsert(true).returnDocument(ReturnDocument.AFTER)); + .findOneAndReplace( + getDocumentIdFilter(documentId), + document, + new FindOneAndReplaceOptions().upsert(true).returnDocument(ReturnDocument.AFTER)); final ChangeEvent event; if (remoteEvent.getOperationType() == ChangeEvent.OperationType.DELETE) { event = changeEventForLocalInsert(namespace, documentAfterUpdate, true); } else { event = changeEventForLocalUpdate( - namespace, - documentId, - ChangeEvent.UpdateDescription.diff(remoteEvent.getFullDocument(), documentAfterUpdate), - document, - true); + namespace, + documentId, + ChangeEvent.UpdateDescription.diff(remoteEvent.getFullDocument(), documentAfterUpdate), + document, + true); } config.setSomePendingWrites( - logicalT, - atVersion, - event); + logicalT, + atVersion, + event); emitEvent(documentId, event); } @@ -1909,23 +1909,23 @@ private void updateOrUpsertOneFromResolution( * @param document the replacement document. */ private void replaceOrUpsertOneFromRemote( - final MongoNamespace namespace, - final BsonValue documentId, - final BsonDocument document, - final BsonDocument atVersion + final MongoNamespace namespace, + final BsonValue documentId, + final BsonDocument document, + final BsonDocument atVersion ) { // TODO: lock down id final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return; } getLocalCollection(namespace) - .findOneAndReplace( - getDocumentIdFilter(documentId), - document, - new FindOneAndReplaceOptions().upsert(true)); + .findOneAndReplace( + getDocumentIdFilter(documentId), + document, + new FindOneAndReplaceOptions().upsert(true)); config.setPendingWritesComplete(atVersion); emitEvent(documentId, changeEventForLocalReplace(namespace, documentId, document, false)); @@ -1942,8 +1942,8 @@ private void replaceOrUpsertOneFromRemote( DeleteResult deleteOne(final MongoNamespace namespace, final Bson filter) { final MongoCollection localCollection = getLocalCollection(namespace); final BsonDocument docToDelete = localCollection - .find(filter) - .first(); + .find(filter) + .first(); if (docToDelete == null) { return DeleteResult.acknowledged(0); @@ -1951,7 +1951,7 @@ DeleteResult deleteOne(final MongoNamespace namespace, final Bson filter) { final BsonValue documentId = BsonUtils.getDocumentId(docToDelete); final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return DeleteResult.acknowledged(0); @@ -1962,8 +1962,8 @@ DeleteResult deleteOne(final MongoNamespace namespace, final Bson filter) { // this block is to trigger coalescence for a delete after insert if (config.getLastUncommittedChangeEvent() != null - && config.getLastUncommittedChangeEvent().getOperationType() - == ChangeEvent.OperationType.INSERT) { + && config.getLastUncommittedChangeEvent().getOperationType() + == ChangeEvent.OperationType.INSERT) { desyncDocumentFromRemote(config.getNamespace(), config.getDocumentId()); return result; } @@ -1984,39 +1984,39 @@ DeleteResult deleteMany(final MongoNamespace namespace, final Bson filter) { final MongoCollection localCollection = getLocalCollection(namespace); final Set idsToDelete = - localCollection - .find(filter) - .map(new Function() { - @Override - @NonNull - public BsonValue apply(@NonNull BsonDocument bsonDocument) { - return BsonUtils.getDocumentId(bsonDocument); - } - }).into(new HashSet<>()); + localCollection + .find(filter) + .map(new Function() { + @Override + @NonNull + public BsonValue apply(@NonNull BsonDocument bsonDocument) { + return BsonUtils.getDocumentId(bsonDocument); + } + }).into(new HashSet<>()); final DeleteResult result = getLocalCollection(namespace).deleteMany(filter); for (BsonValue documentId : idsToDelete) { final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { continue; } final ChangeEvent event = - changeEventForLocalDelete(namespace, documentId, true); + changeEventForLocalDelete(namespace, documentId, true); // this block is to trigger coalescence for a delete after insert if (config.getLastUncommittedChangeEvent() != null - && config.getLastUncommittedChangeEvent().getOperationType() - == ChangeEvent.OperationType.INSERT) { + && config.getLastUncommittedChangeEvent().getOperationType() + == ChangeEvent.OperationType.INSERT) { desyncDocumentFromRemote(config.getNamespace(), config.getDocumentId()); return result; } config.setSomePendingWrites( - logicalT, event); + logicalT, event); emitEvent(documentId, event); } @@ -2031,23 +2031,23 @@ public BsonValue apply(@NonNull BsonDocument bsonDocument) { * @param documentId the _id of the document. */ private void deleteOneFromResolution( - final MongoNamespace namespace, - final BsonValue documentId, - final BsonDocument atVersion + final MongoNamespace namespace, + final BsonValue documentId, + final BsonDocument atVersion ) { // TODO: lock down id final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return; } getLocalCollection(namespace) - .deleteOne(getDocumentIdFilter(documentId)); + .deleteOne(getDocumentIdFilter(documentId)); final ChangeEvent event = - changeEventForLocalDelete(namespace, documentId, true); + changeEventForLocalDelete(namespace, documentId, true); config.setSomePendingWrites( - logicalT, atVersion, event); + logicalT, atVersion, event); emitEvent(documentId, event); } @@ -2059,12 +2059,12 @@ private void deleteOneFromResolution( * @param documentId the _id of the document. */ private void deleteOneFromRemote( - final MongoNamespace namespace, - final BsonValue documentId + final MongoNamespace namespace, + final BsonValue documentId ) { // TODO: lock down id final CoreDocumentSynchronizationConfig config = - syncConfig.getSynchronizedDocument(namespace, documentId); + syncConfig.getSynchronizedDocument(namespace, documentId); if (config == null) { return; } @@ -2089,11 +2089,11 @@ private void triggerListeningToNamespace(final MongoNamespace namespace) { instanceChangeStreamListener.start(namespace); } catch (final Exception ex) { logger.error(String.format( - Locale.US, - "t='%d': triggerListeningToNamespace ns=%s exception: %s", - logicalT, - namespace, - ex)); + Locale.US, + "t='%d': triggerListeningToNamespace ns=%s exception: %s", + logicalT, + namespace, + ex)); } finally { syncLock.unlock(); } @@ -2127,13 +2127,13 @@ private void emitEvent(final BsonValue documentId, final ChangeEvent() { @Override @@ -2142,17 +2142,17 @@ public Object call() { try { if (namespaceListener.getEventListener() != null) { namespaceListener.getEventListener().onEvent( - documentId, - ChangeEvent.transformChangeEventForUser( - event, namespaceListener.getDocumentCodec())); + documentId, + ChangeEvent.transformChangeEventForUser( + event, namespaceListener.getDocumentCodec())); } } catch (final Exception ex) { logger.error(String.format( - Locale.US, - "emitEvent ns=%s documentId=%s emit exception: %s", - event.getNamespace(), - documentId, - ex), ex); + Locale.US, + "emitEvent ns=%s documentId=%s emit exception: %s", + event.getNamespace(), + documentId, + ex), ex); } return null; } @@ -2172,14 +2172,14 @@ public Object call() { * @return the local collection representing the given namespace. */ private MongoCollection getLocalCollection( - final MongoNamespace namespace, - final Class resultClass, - final CodecRegistry codecRegistry + final MongoNamespace namespace, + final Class resultClass, + final CodecRegistry codecRegistry ) { return localClient - .getDatabase(String.format("sync_user_%s", namespace.getDatabaseName())) - .getCollection(namespace.getCollectionName(), resultClass) - .withCodecRegistry(codecRegistry); + .getDatabase(String.format("sync_user_%s", namespace.getDatabaseName())) + .getCollection(namespace.getCollectionName(), resultClass) + .withCodecRegistry(codecRegistry); } /** @@ -2190,9 +2190,9 @@ private MongoCollection getLocalCollection( */ private MongoCollection getLocalCollection(final MongoNamespace namespace) { return getLocalCollection( - namespace, - BsonDocument.class, - MongoClientSettings.getDefaultCodecRegistry()); + namespace, + BsonDocument.class, + MongoClientSettings.getDefaultCodecRegistry()); } /** @@ -2204,12 +2204,12 @@ private MongoCollection getLocalCollection(final MongoNamespace na * @return the remote collection representing the given namespace. */ private CoreRemoteMongoCollection getRemoteCollection( - final MongoNamespace namespace, - final Class resultClass + final MongoNamespace namespace, + final Class resultClass ) { return remoteClient - .getDatabase(namespace.getDatabaseName()) - .getCollection(namespace.getCollectionName(), resultClass); + .getDatabase(namespace.getDatabaseName()) + .getCollection(namespace.getCollectionName(), resultClass); } /** @@ -2219,14 +2219,14 @@ private CoreRemoteMongoCollection getRemoteCollection( * @return the remote collection representing the given namespace for raw document operations. */ private CoreRemoteMongoCollection getRemoteCollection( - final MongoNamespace namespace + final MongoNamespace namespace ) { return getRemoteCollection(namespace, BsonDocument.class); } private Set getLatestDocumentsForStaleFromRemote( - final NamespaceSynchronizationConfig nsConfig, - final Set staleIds) { + final NamespaceSynchronizationConfig nsConfig, + final Set staleIds) { final BsonArray ids = new BsonArray(); for (final BsonValue bsonValue : staleIds) { ids.add(new BsonDocument("_id", bsonValue)); @@ -2237,7 +2237,7 @@ private Set getLatestDocumentsForStaleFromRemote( } return this.getRemoteCollection(nsConfig.getNamespace()).find( - new Document("$or", ids) + new Document("$or", ids) ).into(new HashSet()); } @@ -2267,8 +2267,8 @@ private static BsonDocument getDocumentIdFilter(final BsonValue documentId) { * @return a document with a new version to the given document. */ private static BsonDocument withNewVersion( - final BsonDocument document, - final BsonDocument newVersion + final BsonDocument document, + final BsonDocument newVersion ) { final BsonDocument newDocument = BsonUtils.copyOfDocument(document); newDocument.put(DOCUMENT_VERSION_FIELD, newVersion); From 457971bf5dfd9011d07230ec36c0e2f764dfecb1 Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 18:31:29 +0000 Subject: [PATCH 04/14] Figuring out formatting --- .../remote/sync/internal/DataSynchronizer.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index a8c932a55..2573081a3 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -544,13 +544,13 @@ private void syncRemoteChangeEventToLocal( desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } From 2c428ec24c0a48db90ec3e622bdf4ee52c4f51cb Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 18:33:19 +0000 Subject: [PATCH 05/14] More formatting --- .../sync/internal/DataSynchronizer.java | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 2573081a3..5e7c750f9 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -543,14 +543,14 @@ private void syncRemoteChangeEventToLocal( } catch (final Exception e) { desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document that could not have its version info parsed " - + "; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document that could not have its version info parsed " + + "; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -560,15 +560,15 @@ private void syncRemoteChangeEventToLocal( desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document with an unsupported synchronization protocol version " - + "%d; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document with an unsupported synchronization protocol version " + + "%d; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); return; } @@ -644,7 +644,7 @@ private void syncRemoteChangeEventToLocal( // iv. Otherwise, check if the version info of the incoming remote change event is different // from the version of the local document. final DocumentVersionInfo lastKnownLocalVersionInfo = DocumentVersionInfo - .getLocalVersionInfo(docConfig); + .getLocalVersionInfo(docConfig); // 1. If both the local document version and the remote change event version are empty, drop // the event. The absence of a version is effectively a version, and the pending write will @@ -686,28 +686,28 @@ private void syncRemoteChangeEventToLocal( // i. drop the event if the version counter of the remote event less than or equal to the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " - + "is stale; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " + + "is stale; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } else { // ii. raise a conflict if the version counter of the remote event is greater than the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "has higher counter than local version but a write is pending; " - + "raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "has higher counter than local version but a write is pending; " + + "raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); return; } } From 26841cd3f4261bc38c9404d3dfa7c1722d0c5695 Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 18:35:02 +0000 Subject: [PATCH 06/14] Need to reformat --- .../sync/internal/DataSynchronizer.java | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 5e7c750f9..1d21ba007 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -560,15 +560,15 @@ private void syncRemoteChangeEventToLocal( desyncDocumentFromRemote(nsConfig.getNamespace(), docConfig.getDocumentId()); emitError(docConfig, - String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document with an unsupported synchronization protocol version " - + "%d; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); + String.format( + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document with an unsupported synchronization protocol version " + + "%d; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); return; } @@ -666,12 +666,12 @@ private void syncRemoteChangeEventToLocal( // adhering to the mobile sync protocol. if (!lastKnownLocalVersionInfo.hasVersion() || !currentRemoteVersionInfo.hasVersion()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " - + "empty version but a write is pending; waiting for next L2R pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " + + "empty version but a write is pending; waiting for next L2R pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict(nsConfig.getNamespace(), docConfig, remoteChangeEvent); return; } @@ -686,28 +686,28 @@ private void syncRemoteChangeEventToLocal( // i. drop the event if the version counter of the remote event less than or equal to the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " - + "is stale; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote change event " + + "is stale; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } else { // ii. raise a conflict if the version counter of the remote event is greater than the // version counter of the local document logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "has higher counter than local version but a write is pending; " - + "raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "has higher counter than local version but a write is pending; " + + "raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); return; } } From a2baa49b9ce3a5271c8bdf39d178ec30bf9d80a7 Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 18:40:14 +0000 Subject: [PATCH 07/14] More formatting --- .../sync/internal/DataSynchronizer.java | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 1d21ba007..54d87c9ec 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -561,14 +561,14 @@ private void syncRemoteChangeEventToLocal( emitError(docConfig, String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " - + "document with an unsupported synchronization protocol version " - + "%d; dropping the event, and desyncing the document", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s got a remote " + + "document with an unsupported synchronization protocol version " + + "%d; dropping the event, and desyncing the document", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + currentRemoteVersionInfo.getVersion().getSyncProtocolVersion())); return; } @@ -666,12 +666,12 @@ private void syncRemoteChangeEventToLocal( // adhering to the mobile sync protocol. if (!lastKnownLocalVersionInfo.hasVersion() || !currentRemoteVersionInfo.hasVersion()) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " - + "empty version but a write is pending; waiting for next L2R pass", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote and local have same " + + "empty version but a write is pending; waiting for next L2R pass", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict(nsConfig.getNamespace(), docConfig, remoteChangeEvent); return; } @@ -716,27 +716,27 @@ private void syncRemoteChangeEventToLocal( // fetch the latest version (this is to guard against the case where the unprocessed // change event is stale). final BsonDocument newestRemoteDocument = this.getRemoteCollection(nsConfig.getNamespace()) - .find(new Document("_id", docConfig.getDocumentId())).first(); + .find(new Document("_id", docConfig.getDocumentId())).first(); if (newestRemoteDocument == null) { // i. If the document is not found with a remote lookup, this means the document was // deleted remotely, so raise a conflict using a synthesized delete event as the remote // change event. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " - + "stale and latest document lookup indicates a remote delete occurred, but " - + "a write is pending; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s remote event version " + + "stale and latest document lookup indicates a remote delete occurred, but " + + "a write is pending; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - changeEventForLocalDelete( nsConfig.getNamespace(), - docConfig.getDocumentId(), - docConfig.hasUncommittedWrites())); + docConfig, + changeEventForLocalDelete( + nsConfig.getNamespace(), + docConfig.getDocumentId(), + docConfig.hasUncommittedWrites())); return; } From c8f8d7c755cf5602497b16697899a6f4dff6a7f1 Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 18:48:38 +0000 Subject: [PATCH 08/14] More cleanup --- .../sync/internal/DataSynchronizer.java | 232 +++++++++--------- 1 file changed, 116 insertions(+), 116 deletions(-) diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 54d87c9ec..cd07571b9 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -782,21 +782,21 @@ private void syncRemoteChangeEventToLocal( // event. This means the remote document is a legitimately new document and we should // handle the conflict. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates a remote replace occurred, but a local write is pending; raising " - + "conflict with synthesized replace event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates a remote replace occurred, but a local write is pending; raising " + + "conflict with synthesized replace event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - changeEventForLocalReplace( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - newestRemoteDocument, - docConfig.hasUncommittedWrites())); + nsConfig.getNamespace(), + docConfig, + changeEventForLocalReplace( + nsConfig.getNamespace(), + docConfig.getDocumentId(), + newestRemoteDocument, + docConfig.hasUncommittedWrites())); } /** @@ -860,9 +860,9 @@ private void syncLocalToRemote() { // ii. Check if the internal remote change stream listener has an unprocessed event for // this document. final ChangeEvent unprocessedRemoteEvent = - instanceChangeStreamListener.getUnprocessedEventForDocumentId( - nsConfig.getNamespace(), - docConfig.getDocumentId()); + instanceChangeStreamListener.getUnprocessedEventForDocumentId( + nsConfig.getNamespace(), + docConfig.getDocumentId()); if (unprocessedRemoteEvent != null) { final DocumentVersionInfo unprocessedEventVersion; @@ -889,12 +889,12 @@ private void syncLocalToRemote() { if (!docConfig.hasCommittedVersion(unprocessedEventVersion)) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "unprocessed change event for document; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "unprocessed change event for document; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } // 2. Otherwise, the unprocessed event can be safely dropped and ignored in future R2L @@ -914,7 +914,7 @@ private void syncLocalToRemote() { // a. Insert document into remote database try { remoteColl.insertOne( - withNewVersion(localChangeEvent.getFullDocument(), nextVersion)); + withNewVersion(localChangeEvent.getFullDocument(), nextVersion)); } catch (final StitchServiceException ex) { // b. If an error happens: @@ -922,23 +922,23 @@ private void syncLocalToRemote() { if (ex.getErrorCode() != StitchServiceErrorCode.MONGODB_ERROR || !ex.getMessage().contains("E11000")) { this.emitError(docConfig, String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), ex); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), ex); continue; } // ii. Otherwise record that a conflict has occurred. logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s duplicate key exception on " - + "insert; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s duplicate key exception on " + + "insert; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); isConflicted = true; } break; @@ -949,12 +949,12 @@ private void syncLocalToRemote() { case REPLACE: { if (localDoc == null) { final IllegalStateException illegalStateException = new IllegalStateException( - "expected document to exist for local replace change event: %s"); + "expected document to exist for local replace change event: %s"); emitError( - docConfig, - illegalStateException.getMessage(), - illegalStateException + docConfig, + illegalStateException.getMessage(), + illegalStateException ); continue; } @@ -967,21 +967,21 @@ private void syncLocalToRemote() { final RemoteUpdateResult result; try { result = remoteColl.updateOne( - localVersionInfo.getFilter(), - nextDoc); + localVersionInfo.getFilter(), + nextDoc); } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. this.emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "replacing: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + "replacing: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -989,12 +989,12 @@ private void syncLocalToRemote() { if (result.getMatchedCount() == 0) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "replaced document or document deleted; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "replaced document or document deleted; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } break; } @@ -1003,11 +1003,11 @@ private void syncLocalToRemote() { case UPDATE: { if (localDoc == null) { final IllegalStateException illegalStateException = new IllegalStateException( - "expected document to exist for local update change event"); + "expected document to exist for local update change event"); emitError( - docConfig, - illegalStateException.getMessage(), - illegalStateException + docConfig, + illegalStateException.getMessage(), + illegalStateException ); continue; } @@ -1021,7 +1021,7 @@ private void syncLocalToRemote() { if (!localChangeEvent.getUpdateDescription().getUpdatedFields().isEmpty()) { final BsonDocument sets = new BsonDocument(); for (final Map.Entry fieldValue : - localChangeEvent.getUpdateDescription().getUpdatedFields().entrySet()) { + localChangeEvent.getUpdateDescription().getUpdatedFields().entrySet()) { sets.put(fieldValue.getKey(), fieldValue.getValue()); } sets.put(DOCUMENT_VERSION_FIELD, nextVersion); @@ -1039,21 +1039,21 @@ private void syncLocalToRemote() { final RemoteUpdateResult result; try { result = remoteColl.updateOne( - localVersionInfo.getFilter(), + localVersionInfo.getFilter(), translatedUpdate.isEmpty() ? nextDoc : translatedUpdate); } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "updating: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + "updating: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -1061,12 +1061,12 @@ private void syncLocalToRemote() { // c. If no documents are matched, record that a conflict has occurred. isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "updated document or document deleted; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "updated document or document deleted; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } break; } @@ -1081,16 +1081,16 @@ private void syncLocalToRemote() { } catch (final StitchServiceException ex) { // b. If an error happens, report an error to the error listener. emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + " deleting: %s", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - ex), - ex + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s exception " + + " deleting: %s", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + ex), + ex ); continue; } @@ -1101,12 +1101,12 @@ private void syncLocalToRemote() { if (remoteDocument != null) { isConflicted = true; logger.info(String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " - + "removed document; raising conflict", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s version different on " + + "removed document; raising conflict", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); } else { // d. Desynchronize the document if there is no conflict, or if fetching a remote // document after the conflict is raised returns no remote document. @@ -1120,15 +1120,15 @@ private void syncLocalToRemote() { default: emitError( - docConfig, - String.format( - Locale.US, - "t='%d': syncLocalToRemote ns=%s documentId=%s unknown operation " - + "type occurred on the document: %s; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId(), - localChangeEvent.getOperationType().toString()) + docConfig, + String.format( + Locale.US, + "t='%d': syncLocalToRemote ns=%s documentId=%s unknown operation " + + "type occurred on the document: %s; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId(), + localChangeEvent.getOperationType().toString()) ); continue; } @@ -1151,15 +1151,15 @@ private void syncLocalToRemote() { // TODO(STITCH-1972): This event may contain old version info. We should be filtering out // the version anyway from local and remote events. final ChangeEvent committedEvent = - docConfig.getLastUncommittedChangeEvent(); + docConfig.getLastUncommittedChangeEvent(); emitEvent(docConfig.getDocumentId(), new ChangeEvent<>( - committedEvent.getId(), - committedEvent.getOperationType(), - committedEvent.getFullDocument(), - committedEvent.getNamespace(), - committedEvent.getDocumentKey(), - committedEvent.getUpdateDescription(), - false)); + committedEvent.getId(), + committedEvent.getOperationType(), + committedEvent.getFullDocument(), + committedEvent.getNamespace(), + committedEvent.getDocumentKey(), + committedEvent.getUpdateDescription(), + false)); docConfig.setPendingWritesComplete(nextVersion); } else { @@ -1179,9 +1179,9 @@ private void syncLocalToRemote() { } resolveConflict( - nsConfig.getNamespace(), - docConfig, - remoteChangeEvent); + nsConfig.getNamespace(), + docConfig, + remoteChangeEvent); } } } From cb4189826cbc9e35707000bbfb551294d7b63e7b Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Tue, 30 Oct 2018 18:59:46 +0000 Subject: [PATCH 09/14] More formatting issues --- .../sync/internal/DataSynchronizer.java | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index cd07571b9..5802da68f 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -734,9 +734,9 @@ private void syncRemoteChangeEventToLocal( nsConfig.getNamespace(), docConfig, changeEventForLocalDelete( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - docConfig.hasUncommittedWrites())); + nsConfig.getNamespace(), + docConfig.getDocumentId(), + docConfig.hasUncommittedWrites())); return; } @@ -763,16 +763,16 @@ private void syncRemoteChangeEventToLocal( // to the GUID of the local document, drop the event. We’re believed to be behind in // the change stream at this point. if (newestRemoteVersionInfo.hasVersion() - && newestRemoteVersionInfo.getVersion().getInstanceId() - .equals(localVersion.instanceId)) { + && newestRemoteVersionInfo.getVersion().getInstanceId() + .equals(localVersion.instanceId)) { logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates that this is a stale event; dropping the event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates that this is a stale event; dropping the event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); return; } @@ -782,21 +782,21 @@ private void syncRemoteChangeEventToLocal( // event. This means the remote document is a legitimately new document and we should // handle the conflict. logger.info(String.format( - Locale.US, - "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates a remote replace occurred, but a local write is pending; raising " - + "conflict with synthesized replace event", - logicalT, - nsConfig.getNamespace(), - docConfig.getDocumentId())); + Locale.US, + "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " + + "indicates a remote replace occurred, but a local write is pending; raising " + + "conflict with synthesized replace event", + logicalT, + nsConfig.getNamespace(), + docConfig.getDocumentId())); resolveConflict( - nsConfig.getNamespace(), - docConfig, - changeEventForLocalReplace( - nsConfig.getNamespace(), - docConfig.getDocumentId(), - newestRemoteDocument, - docConfig.hasUncommittedWrites())); + nsConfig.getNamespace(), + docConfig, + changeEventForLocalReplace( + nsConfig.getNamespace(), + docConfig.getDocumentId(), + newestRemoteDocument, + docConfig.hasUncommittedWrites())); } /** @@ -860,9 +860,9 @@ private void syncLocalToRemote() { // ii. Check if the internal remote change stream listener has an unprocessed event for // this document. final ChangeEvent unprocessedRemoteEvent = - instanceChangeStreamListener.getUnprocessedEventForDocumentId( - nsConfig.getNamespace(), - docConfig.getDocumentId()); + instanceChangeStreamListener.getUnprocessedEventForDocumentId( + nsConfig.getNamespace(), + docConfig.getDocumentId()); if (unprocessedRemoteEvent != null) { final DocumentVersionInfo unprocessedEventVersion; @@ -920,7 +920,7 @@ private void syncLocalToRemote() { // i. That is not a duplicate key exception, report an error to the error listener. if (ex.getErrorCode() != StitchServiceErrorCode.MONGODB_ERROR - || !ex.getMessage().contains("E11000")) { + || !ex.getMessage().contains("E11000")) { this.emitError(docConfig, String.format( Locale.US, "t='%d': syncLocalToRemote ns=%s documentId=%s exception inserting: %s", @@ -976,7 +976,7 @@ private void syncLocalToRemote() { String.format( Locale.US, "t='%d': syncLocalToRemote ns=%s documentId=%s exception " - + "replacing: %s", + + "replacing: %s", logicalT, nsConfig.getNamespace(), docConfig.getDocumentId(), @@ -1030,7 +1030,7 @@ private void syncLocalToRemote() { if (!localChangeEvent.getUpdateDescription().getRemovedFields().isEmpty()) { final BsonDocument unsets = new BsonDocument(); for (final String field : - localChangeEvent.getUpdateDescription().getRemovedFields()) { + localChangeEvent.getUpdateDescription().getRemovedFields()) { unsets.put(field, BsonBoolean.TRUE); } translatedUpdate.put("$unset", unsets); @@ -1223,7 +1223,7 @@ public Object call() { this.logger.error(msg); this.logger.error( - String.format("Setting document %s to frozen", docConfig.getDocumentId())); + String.format("Setting document %s to frozen", docConfig.getDocumentId())); } @@ -1246,7 +1246,7 @@ private void resolveConflict( logger.warn(String.format( Locale.US, "t='%d': resolveConflict ns=%s documentId=%s no conflict resolver set; cannot " - + "resolve yet", + + "resolve yet", logicalT, namespace, docConfig.getDocumentId())); @@ -1362,7 +1362,7 @@ private void resolveConflict( logger.info(String.format( Locale.US, "t='%d': resolveConflict ns=%s documentId=%s replacing local with resolved document " - + "with remote version acknowledged: %s", + + "with remote version acknowledged: %s", logicalT, namespace, docConfig.getDocumentId(), @@ -1455,12 +1455,12 @@ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( * Queues up a callback to be removed and invoked on the next change event. */ public void addWatcher(final MongoNamespace namespace, - final Callback, Object> watcher) { + final Callback, Object> watcher) { instanceChangeStreamListener.addWatcher(namespace, watcher); } public void removeWatcher(final MongoNamespace namespace, - final Callback, Object> watcher) { + final Callback, Object> watcher) { instanceChangeStreamListener.removeWatcher(namespace, watcher); } From bbb3c1ab879fdd711260b4ffad0bf717062f2d6e Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Wed, 31 Oct 2018 00:02:24 +0000 Subject: [PATCH 10/14] Write tests for datasync, coresync, int; clean up; fix bulk update logic; add count op; clean up --- .../examples/todosync/TodoListActivity.java | 19 +- .../internal/SyncMongoClientIntTests.kt | 53 ++++- .../android/services/mongodb/remote/Sync.java | 25 ++- .../mongodb/remote/SyncAggregateIterable.java | 21 ++ .../internal/SyncAggregateIterableImpl.java | 18 +- .../mongodb/remote/internal/SyncImpl.java | 34 ++-- .../mongodb/remote/sync/CoreSync.java | 46 +++-- .../sync/CoreSyncAggregateIterable.java | 21 ++ .../mongodb/remote/sync/SyncCountOptions.java | 23 +++ .../mongodb/remote/sync/SyncDeleteResult.java | 24 +++ .../remote/sync/SyncInsertManyResult.java | 29 ++- .../remote/sync/SyncInsertOneResult.java | 24 +++ .../remote/sync/SyncUpdateOptions.java | 23 +++ .../mongodb/remote/sync/SyncUpdateResult.java | 27 +++ .../sync/internal/AggregateOperation.java | 28 ++- .../CoreSyncAggregateIterableImpl.java | 23 ++- .../remote/sync/internal/CoreSyncImpl.java | 12 +- .../remote/sync/internal/CountOperation.java | 49 +++++ .../sync/internal/DataSynchronizer.java | 51 ++++- .../sync/internal/DeleteManyOperation.java | 20 +- .../sync/internal/DeleteOneOperation.java | 2 - .../internal/InsertManyAndSyncOperation.java | 22 ++- .../remote/sync/internal/SyncOperations.java | 38 ++-- .../sync/internal/UpdateManyOperation.java | 20 +- .../sync/internal/ChangeEventUnitTests.kt | 2 +- .../remote/sync/internal/CoreSyncUnitTests.kt | 164 +++++++++++++++- .../internal/DataSynchronizerUnitTests.kt | 17 +- .../core/testutils/sync/ProxySyncMethods.kt | 69 ++++++- .../core/testutils/sync/SyncIntTestProxy.kt | 185 ++++++++++++++++++ .../core/testutils/sync/SyncIntTestRunner.kt | 12 ++ .../server/services/mongodb/remote/Sync.java | 50 ++--- .../mongodb/remote/SyncAggregateIterable.java | 21 ++ .../internal/SyncAggregateIterableImpl.java | 16 ++ .../mongodb/remote/internal/SyncImpl.java | 30 +-- .../internal/SyncMongoClientIntTests.kt | 53 ++++- 35 files changed, 1103 insertions(+), 168 deletions(-) create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CountOperation.java diff --git a/android/examples/todo-sync/src/main/java/com/mongodb/stitch/android/examples/todosync/TodoListActivity.java b/android/examples/todo-sync/src/main/java/com/mongodb/stitch/android/examples/todosync/TodoListActivity.java index 15bb20d32..fd3c0974b 100644 --- a/android/examples/todo-sync/src/main/java/com/mongodb/stitch/android/examples/todosync/TodoListActivity.java +++ b/android/examples/todo-sync/src/main/java/com/mongodb/stitch/android/examples/todosync/TodoListActivity.java @@ -39,9 +39,9 @@ import com.mongodb.stitch.android.services.mongodb.remote.RemoteMongoCollection; import com.mongodb.stitch.core.auth.providers.serverapikey.ServerApiKeyCredential; import com.mongodb.stitch.core.internal.common.BsonUtils; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.DefaultSyncConflictResolvers; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent; import java.util.ArrayList; @@ -53,7 +53,6 @@ import org.bson.BsonObjectId; import org.bson.BsonRegularExpression; -import org.bson.BsonString; import org.bson.BsonValue; import org.bson.Document; import org.bson.codecs.configuration.CodecRegistries; @@ -127,7 +126,7 @@ public void updateChecked(final ObjectId itemId, final boolean isChecked) { updateDoc.append("$unset", new Document(TodoItem.Fields.DONE_DATE, "")); } - items.sync().updateOneById(new BsonObjectId(itemId), updateDoc); + items.sync().updateOne(new Document("_id", itemId), updateDoc); } @Override @@ -358,8 +357,8 @@ private void addTodoItem(final String task) { private void updateTodoItemTask(final ObjectId itemId, final String newTask) { final BsonObjectId docId = new BsonObjectId(itemId); - items.sync().updateOneById( - docId, + items.sync().updateOne( + new Document("_id", docId), new Document("$set", new Document(TodoItem.Fields.TASK, newTask))) .addOnSuccessListener(result -> { items.sync().find(new Document("_id", docId)).first() @@ -375,11 +374,11 @@ private void updateTodoItemTask(final ObjectId itemId, final String newTask) { } private void clearCheckedTodoItems() { - final List> tasks = new ArrayList<>(); + final List> tasks = new ArrayList<>(); getItems().addOnSuccessListener(todoItems -> { for (final TodoItem item : todoItems) { if (item.isChecked()) { - tasks.add(items.sync().deleteOneById(new BsonObjectId(item.getId()))); + tasks.add(items.sync().deleteOne(new Document("_id", item.getId()))); } } Tasks.whenAllComplete(tasks) @@ -388,10 +387,10 @@ private void clearCheckedTodoItems() { } private void clearAllTodoItems() { - final List> tasks = new ArrayList<>(); + final List> tasks = new ArrayList<>(); getItems().addOnSuccessListener(todoItems -> { for (final TodoItem item : todoItems) { - tasks.add(items.sync().deleteOneById(new BsonObjectId(item.getId()))); + tasks.add(items.sync().deleteOne(new Document("_id", item.getId()))); } Tasks.whenAllComplete(tasks) .addOnCompleteListener(task -> todoAdapter.clearItems()); @@ -399,6 +398,6 @@ private void clearAllTodoItems() { } private void touchList() { - lists.sync().updateOneById(new BsonString(userId), new Document("$inc", new Document("i", 1))); + lists.sync().updateOne(new Document("_id", userId), new Document("$inc", new Document("i", 1))); } } diff --git a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt index 1dbf542ed..4419c6cea 100644 --- a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt +++ b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt @@ -20,6 +20,11 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer import com.mongodb.stitch.core.testutils.BaseStitchIntTest import com.mongodb.stitch.core.testutils.sync.ProxyRemoteMethods @@ -71,18 +76,38 @@ class SyncMongoClientIntTests : BaseStitchAndroidIntTest(), SyncIntTestRunner { sync.syncOne(id) } - override fun insertOneAndSync(document: Document): RemoteInsertOneResult { + override fun count(filter: Bson): Long { + return Tasks.await(sync.count()) + } + + override fun aggregate(pipeline: List): Iterable { + return Tasks.await(sync.aggregate(pipeline).into(mutableListOf())) + } + + override fun insertOneAndSync(document: Document): SyncInsertOneResult { return Tasks.await(sync.insertOneAndSync(document)) } - override fun updateOne(filter: Bson, update: Bson): RemoteUpdateResult { - return Tasks.await(sync.updateOne(filter, update)) + override fun insertManyAndSync(documents: List): SyncInsertManyResult { + return Tasks.await(sync.insertManyAndSync(documents)) } - override fun deleteOne(filter: Bson): RemoteDeleteResult { + override fun updateOne(filter: Bson, update: Bson, updateOptions: SyncUpdateOptions): SyncUpdateResult { + return Tasks.await(sync.updateOne(filter, update, updateOptions)) + } + + override fun updateMany(filter: Bson, update: Bson, updateOptions: SyncUpdateOptions): SyncUpdateResult { + return Tasks.await(sync.updateMany(filter, update, updateOptions)) + } + + override fun deleteOne(filter: Bson): SyncDeleteResult { return Tasks.await(sync.deleteOne(filter)) } + override fun deleteMany(filter: Bson): SyncDeleteResult { + return Tasks.await(sync.deleteMany(filter)) + } + override fun desyncOne(id: BsonValue) { sync.desyncOne(id) } @@ -312,6 +337,26 @@ class SyncMongoClientIntTests : BaseStitchAndroidIntTest(), SyncIntTestRunner { testProxy.testResumeSyncForDocumentResumesSync() } + @Test + override fun testReadsBeforeAndAfterSync() { + testProxy.testReadsBeforeAndAfterSync() + } + + @Test + override fun testInsertManyNoConflicts() { + testProxy.testInsertManyNoConflicts() + } + + @Test + override fun testUpdateManyNoConflicts() { + testProxy.testUpdateManyNoConflicts() + } + + @Test + override fun testDeleteManyNoConflicts() { + testProxy.testDeleteManyNoConflicts() + } + /** * Get the uri for where mongodb is running locally. */ diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java index 6dd606bba..454289ec4 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java @@ -20,13 +20,8 @@ import android.support.annotation.Nullable; import com.google.android.gms.tasks.Task; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; -import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; -import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncFindIterable; import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; @@ -119,7 +114,8 @@ void configure(@NonNull final ConflictHandler conflictHandler, Task count(); /** - * Counts the number of documents in the collection according to the given options. + * Counts the number of documents in the collection that have been synchronized from the remote + * according to the given options. * * @param filter the query filter * @return the number of documents in the collection @@ -127,7 +123,8 @@ void configure(@NonNull final ConflictHandler conflictHandler, Task count(final Bson filter); /** - * Counts the number of documents in the collection according to the given options. + * Counts the number of documents in the collection that have been synchronized from the remote + * according to the given options. * * @param filter the query filter * @param options the options describing the count @@ -136,14 +133,14 @@ void configure(@NonNull final ConflictHandler conflictHandler, Task count(final Bson filter, final SyncCountOptions options); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @return the find iterable interface */ SyncFindIterable find(); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param resultClass the class to decode each document into * @param the target document type of the iterable. @@ -152,7 +149,7 @@ void configure(@NonNull final ConflictHandler conflictHandler, SyncFindIterable find(final Class resultClass); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param filter the query filter * @return the find iterable interface @@ -160,7 +157,7 @@ void configure(@NonNull final ConflictHandler conflictHandler, SyncFindIterable find(final Bson filter); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param filter the query filter * @param resultClass the class to decode each document into @@ -173,7 +170,8 @@ SyncFindIterable find( /** - * Aggregates documents according to the specified aggregation pipeline. + * Aggregates documents that have been synchronized from the remote + * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline * @return an iterable containing the result of the aggregation operation @@ -181,7 +179,8 @@ SyncFindIterable find( SyncAggregateIterable aggregate(final List pipeline); /** - * Aggregates documents according to the specified aggregation pipeline. + * Aggregates documents that have been synchronized from the remote + * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline * @param resultClass the class to decode each document into diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/SyncAggregateIterable.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/SyncAggregateIterable.java index 2a838fed9..d9708841a 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/SyncAggregateIterable.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/SyncAggregateIterable.java @@ -1,4 +1,25 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.android.services.mongodb.remote; +/** + * Iterable for aggregate. + * + * @param The type of the result. + */ public interface SyncAggregateIterable extends RemoteMongoIterable { } diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncAggregateIterableImpl.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncAggregateIterableImpl.java index 6ee995e05..8f71e568a 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncAggregateIterableImpl.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncAggregateIterableImpl.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.android.services.mongodb.remote.internal; import com.mongodb.stitch.android.core.internal.common.TaskDispatcher; @@ -9,7 +25,7 @@ class SyncAggregateIterableImpl implements SyncAggregateIterable { SyncAggregateIterableImpl( final CoreSyncAggregateIterable iterable, - TaskDispatcher dispatcher + final TaskDispatcher dispatcher ) { super(iterable, dispatcher); } diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncImpl.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncImpl.java index b6c3f8783..5cce6d436 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncImpl.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncImpl.java @@ -101,12 +101,12 @@ public Task count() { } @Override - public Task count(Bson filter) { + public Task count(final Bson filter) { return this.count(filter, new SyncCountOptions()); } @Override - public Task count(Bson filter, SyncCountOptions options) { + public Task count(final Bson filter, final SyncCountOptions options) { return this.dispatcher.dispatchTask(new Callable() { @Override public Long call() throws Exception { @@ -116,13 +116,13 @@ public Long call() throws Exception { } @Override - public SyncAggregateIterable aggregate(List pipeline) { + public SyncAggregateIterable aggregate(final List pipeline) { return new SyncAggregateIterableImpl<>(this.proxy.aggregate(pipeline), dispatcher); } @Override - public SyncAggregateIterable aggregate(List pipeline, - Class resultClass) { + public SyncAggregateIterable aggregate(final List pipeline, + final Class resultClass) { return new SyncAggregateIterableImpl<>(this.proxy.aggregate(pipeline, resultClass), dispatcher); } @@ -148,7 +148,7 @@ public SyncFindIterable find(final Bson filter, } @Override - public Task insertOneAndSync(DocumentT document) { + public Task insertOneAndSync(final DocumentT document) { return this.dispatcher.dispatchTask(new Callable() { @Override public SyncInsertOneResult call() throws Exception { @@ -158,7 +158,7 @@ public SyncInsertOneResult call() throws Exception { } @Override - public Task insertManyAndSync(List documents) { + public Task insertManyAndSync(final List documents) { return this.dispatcher.dispatchTask(new Callable() { @Override public SyncInsertManyResult call() throws Exception { @@ -168,12 +168,16 @@ public SyncInsertManyResult call() throws Exception { } @Override - public Task updateOne(Bson filter, Bson update) { + public Task updateOne(final Bson filter, final Bson update) { return this.updateOne(filter, update, new SyncUpdateOptions()); } @Override - public Task updateOne(Bson filter, Bson update, SyncUpdateOptions updateOptions) { + public Task updateOne( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions + ) { return this.dispatcher.dispatchTask(new Callable() { @Override public SyncUpdateResult call() throws Exception { @@ -183,12 +187,16 @@ public SyncUpdateResult call() throws Exception { } @Override - public Task updateMany(Bson filter, Bson update) { + public Task updateMany(final Bson filter, final Bson update) { return this.updateMany(filter, update, new SyncUpdateOptions()); } @Override - public Task updateMany(Bson filter, Bson update, SyncUpdateOptions updateOptions) { + public Task updateMany( + final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions + ) { return this.dispatcher.dispatchTask(new Callable() { @Override public SyncUpdateResult call() throws Exception { @@ -198,7 +206,7 @@ public SyncUpdateResult call() throws Exception { } @Override - public Task deleteOne(Bson filter) { + public Task deleteOne(final Bson filter) { return this.dispatcher.dispatchTask(new Callable() { @Override public SyncDeleteResult call() throws Exception { @@ -208,7 +216,7 @@ public SyncDeleteResult call() throws Exception { } @Override - public Task deleteMany(Bson filter) { + public Task deleteMany(final Bson filter) { return this.dispatcher.dispatchTask(new Callable() { @Override public SyncDeleteResult call() throws Exception { diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java index 302fce83a..255cc11a4 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java @@ -96,14 +96,15 @@ void configure(@Nonnull final ConflictHandler conflictHandler, boolean resumeSyncForDocument(final BsonValue documentId); /** - * Counts the number of documents in the collection. + * Counts the number of documents in the collection that have been synchronized from the remote. * * @return the number of documents in the collection */ long count(); /** - * Counts the number of documents in the collection according to the given options. + * Counts the number of documents in the collection that have been synchronized from the remote + * according to the given options. * * @param filter the query filter * @return the number of documents in the collection @@ -111,7 +112,8 @@ void configure(@Nonnull final ConflictHandler conflictHandler, long count(final Bson filter); /** - * Counts the number of documents in the collection according to the given options. + * Counts the number of documents in the collection that have been synchronized from the remote + * according to the given options. * * @param filter the query filter * @param options the options describing the count @@ -120,14 +122,14 @@ void configure(@Nonnull final ConflictHandler conflictHandler, long count(final Bson filter, final SyncCountOptions options); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @return the find iterable interface */ CoreSyncFindIterable find(); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param resultClass the class to decode each document into * @param the target document type of the iterable. @@ -136,7 +138,7 @@ void configure(@Nonnull final ConflictHandler conflictHandler, CoreSyncFindIterable find(final Class resultClass); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param filter the query filter * @return the find iterable interface @@ -144,7 +146,7 @@ void configure(@Nonnull final ConflictHandler conflictHandler, CoreSyncFindIterable find(final Bson filter); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param filter the query filter * @param resultClass the class to decode each document into @@ -157,7 +159,8 @@ CoreSyncFindIterable find( /** - * Aggregates documents according to the specified aggregation pipeline. + * Aggregates documents that have been synchronized from the remote + * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline * @return an iterable containing the result of the aggregation operation @@ -165,7 +168,8 @@ CoreSyncFindIterable find( CoreSyncAggregateIterable aggregate(final List pipeline); /** - * Aggregates documents according to the specified aggregation pipeline. + * Aggregates documents that have been synchronized from the remote + * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline * @param resultClass the class to decode each document into @@ -178,7 +182,7 @@ CoreSyncAggregateIterable aggregate( /** * Inserts the provided document. If the document is missing an identifier, the client should - * generate one. + * generate one. Syncs the newly inserted document against the remote. * * @param document the document to insert * @return the result of the insert one operation @@ -186,7 +190,7 @@ CoreSyncAggregateIterable aggregate( SyncInsertOneResult insertOneAndSync(final DocumentT document); /** - * Inserts one or more documents. + * Inserts one or more documents. Syncs the newly inserted documents against the remote. * * @param documents the documents to insert * @return the result of the insert many operation @@ -194,8 +198,8 @@ CoreSyncAggregateIterable aggregate( SyncInsertManyResult insertManyAndSync(final List documents); /** - * Removes at most one document from the collection that matches the given filter. If no - * documents match, the collection is not + * Removes at most one document from the collection that has been synchronized from the remote + * that matches the given filter. If no documents match, the collection is not * modified. * * @param filter the query filter to apply the the delete operation @@ -204,8 +208,8 @@ CoreSyncAggregateIterable aggregate( SyncDeleteResult deleteOne(final Bson filter); /** - * Removes all documents from the collection that match the given query filter. If no documents - * match, the collection is not modified. + * Removes all documents from the collection that have been synchronized from the remote + * that match the given query filter. If no documents match, the collection is not modified. * * @param filter the query filter to apply the the delete operation * @return the result of the remove many operation @@ -213,7 +217,8 @@ CoreSyncAggregateIterable aggregate( SyncDeleteResult deleteMany(final Bson filter); /** - * Update a single document in the collection according to the specified arguments. + * Update a single document in the collection that have been synchronized from the remote + * according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -223,7 +228,8 @@ CoreSyncAggregateIterable aggregate( SyncUpdateResult updateOne(final Bson filter, final Bson update); /** - * Update a single document in the collection according to the specified arguments. + * Update a single document in the collection that has been synchronized from the remote + * according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -237,7 +243,8 @@ SyncUpdateResult updateOne( final SyncUpdateOptions updateOptions); /** - * Update all documents in the collection according to the specified arguments. + * Update all documents in the collection that have been synchronized from the remote + * according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -247,7 +254,8 @@ SyncUpdateResult updateOne( SyncUpdateResult updateMany(final Bson filter, final Bson update); /** - * Update all documents in the collection according to the specified arguments. + * Update all documents in the collection that have been synchronized from the remote + * according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSyncAggregateIterable.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSyncAggregateIterable.java index 107045512..95a245d88 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSyncAggregateIterable.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSyncAggregateIterable.java @@ -1,6 +1,27 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync; import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteAggregateIterable; +/** + * Iterable for aggregate. + * + * @param The type of the result. + */ public interface CoreSyncAggregateIterable extends CoreRemoteAggregateIterable { } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncCountOptions.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncCountOptions.java index 1d6d59014..cb7eed6c8 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncCountOptions.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncCountOptions.java @@ -1,6 +1,29 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync; import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; +/** + * The options for a count operation. + */ public class SyncCountOptions extends RemoteCountOptions { + @Override + public SyncCountOptions limit(final int limit) { + return (SyncCountOptions)super.limit(limit); + } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncDeleteResult.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncDeleteResult.java index 15a2750e0..96f6c215a 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncDeleteResult.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncDeleteResult.java @@ -1,8 +1,32 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; +/** + * The result of a delete operation. + */ public class SyncDeleteResult extends RemoteDeleteResult { + /** + * Constructs a result. + * + * @param deletedCount the number of documents deleted. + */ public SyncDeleteResult(final long deletedCount) { super(deletedCount); } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertManyResult.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertManyResult.java index 4c8b9f0af..9d1d254b9 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertManyResult.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertManyResult.java @@ -1,12 +1,37 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; -import org.bson.BsonValue; - import java.util.Map; +import org.bson.BsonValue; + +/** + * The result of an insert many operation. + */ public class SyncInsertManyResult extends RemoteInsertManyResult { + /** + * Constructs a result. + * + * @param insertedIds the _ids of the inserted documents arranged by the index of the document + * from the operation and its corresponding id. + */ public SyncInsertManyResult(final Map insertedIds) { super(insertedIds); } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertOneResult.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertOneResult.java index c1e07ba1d..0721a51d7 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertOneResult.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncInsertOneResult.java @@ -1,10 +1,34 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; import org.bson.BsonValue; +/** + * The result of an insert one operation. + */ public class SyncInsertOneResult extends RemoteInsertOneResult { + /** + * Constructs a result. + * + * @param insertedId the _id of the inserted document. + */ public SyncInsertOneResult(final BsonValue insertedId) { super(insertedId); } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateOptions.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateOptions.java index e473748ca..575bf4fca 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateOptions.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateOptions.java @@ -1,6 +1,29 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions; +/** + * The options to apply when updating documents. + */ public class SyncUpdateOptions extends RemoteUpdateOptions { + @Override + public SyncUpdateOptions upsert(final boolean upsert) { + return (SyncUpdateOptions)super.upsert(upsert); + } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateResult.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateResult.java index 0b08bba47..04a0467d4 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateResult.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/SyncUpdateResult.java @@ -1,10 +1,37 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import org.bson.BsonValue; +/** + * The result of an update operation. + */ public class SyncUpdateResult extends RemoteUpdateResult { + /** + * Constructs a result. + * + * @param matchedCount the number of documents matched by the query. + * @param modifiedCount the number of documents modified. + * @param upsertedId the _id of the inserted document if the replace resulted in an inserted + * document, otherwise null. + */ public SyncUpdateResult( final long matchedCount, final long modifiedCount, diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/AggregateOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/AggregateOperation.java index 35e23eb78..11f655ce7 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/AggregateOperation.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/AggregateOperation.java @@ -1,21 +1,33 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync.internal; import com.mongodb.MongoNamespace; -import com.mongodb.client.result.DeleteResult; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; -import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; -import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; - -import org.bson.conversions.Bson; import java.util.ArrayList; import java.util.Collection; -import java.util.HashSet; import java.util.List; import javax.annotation.Nullable; +import org.bson.conversions.Bson; + class AggregateOperation implements Operation> { private final MongoNamespace namespace; private final DataSynchronizer dataSynchronizer; @@ -35,7 +47,9 @@ class AggregateOperation implements Operation> { } public Collection execute(@Nullable final CoreStitchServiceClient service) { - return this.dataSynchronizer.aggregate(namespace, pipeline, resultClass).into(new ArrayList<>()); + return this.dataSynchronizer.aggregate( + namespace, pipeline, resultClass + ).into(new ArrayList<>()); } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncAggregateIterableImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncAggregateIterableImpl.java index 59015c83e..1b9c62c3c 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncAggregateIterableImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncAggregateIterableImpl.java @@ -1,17 +1,30 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync.internal; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; -import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteAggregateIterable; -import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoIterableImpl; import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; -import com.mongodb.stitch.core.services.mongodb.remote.internal.Operations; import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; -import org.bson.conversions.Bson; - import java.util.Collection; import java.util.List; +import org.bson.conversions.Bson; + class CoreSyncAggregateIterableImpl extends CoreSyncMongoIterableImpl, ResultT> implements CoreSyncAggregateIterable { diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncImpl.java index c22c594b9..7f62d8293 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncImpl.java @@ -17,7 +17,6 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; import com.mongodb.MongoNamespace; -import com.mongodb.client.model.CountOptions; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; @@ -125,8 +124,7 @@ public long count(final Bson filter) { @Override public long count(final Bson filter, final SyncCountOptions options) { - CountOptions countOptions = new CountOptions().limit(options.getLimit()); - return this.dataSynchronizer.count(this.namespace, filter, countOptions); + return syncOperations.count(filter, options).execute(service); } @Override @@ -173,22 +171,22 @@ public SyncUpdateResult updateMany( } @Override - public SyncInsertOneResult insertOneAndSync(DocumentT document) { + public SyncInsertOneResult insertOneAndSync(final DocumentT document) { return syncOperations.insertOneAndSync(document).execute(service); } @Override - public SyncInsertManyResult insertManyAndSync(List documents) { + public SyncInsertManyResult insertManyAndSync(final List documents) { return syncOperations.insertManyAndSync(documents).execute(service); } @Override - public SyncDeleteResult deleteOne(Bson filter) { + public SyncDeleteResult deleteOne(final Bson filter) { return syncOperations.deleteOne(filter).execute(service); } @Override - public SyncDeleteResult deleteMany(Bson filter) { + public SyncDeleteResult deleteMany(final Bson filter) { return syncOperations.deleteMany(filter).execute(service); } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CountOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CountOperation.java new file mode 100644 index 000000000..58a9ef27d --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CountOperation.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync.internal; + +import com.mongodb.MongoNamespace; +import com.mongodb.client.model.CountOptions; +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; + +import javax.annotation.Nullable; + +import org.bson.conversions.Bson; + +class CountOperation implements Operation { + private final MongoNamespace namespace; + private final DataSynchronizer dataSynchronizer; + private final Bson filter; + private final CountOptions countOptions; + + CountOperation( + final MongoNamespace namespace, + final DataSynchronizer dataSynchronizer, + final Bson filter, + final CountOptions countOptions + ) { + this.namespace = namespace; + this.dataSynchronizer = dataSynchronizer;; + this.filter = filter; + this.countOptions = countOptions; + } + + public Long execute(@Nullable final CoreStitchServiceClient service) { + return this.dataSynchronizer.count(namespace, filter, countOptions); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 5802da68f..643203f96 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -784,8 +784,8 @@ private void syncRemoteChangeEventToLocal( logger.info(String.format( Locale.US, "t='%d': syncRemoteChangeEventToLocal ns=%s documentId=%s latest document lookup " - + "indicates a remote replace occurred, but a local write is pending; raising " - + "conflict with synthesized replace event", + + "indicates a remote replace occurred, but a local write is pending; raising " + + "conflict with synthesized replace event", logicalT, nsConfig.getNamespace(), docConfig.getDocumentId())); @@ -1734,14 +1734,19 @@ UpdateResult updateOne( final Bson filter, final Bson update, final UpdateOptions updateOptions) { + // read the local collection final MongoCollection localCollection = getLocalCollection(namespace); + // fetch the document prior to updating final BsonDocument documentBeforeUpdate = getLocalCollection(namespace).find(filter).first(); + // if there was no document prior and this is not an upsert, + // do not acknowledge the update if (!updateOptions.isUpsert() && documentBeforeUpdate == null) { return UpdateResult.acknowledged(0, 0L, null); } + // find and update the single document, returning the document post-update final BsonDocument documentAfterUpdate = localCollection.findOneAndUpdate( filter, update, @@ -1752,6 +1757,8 @@ UpdateResult updateOne( .arrayFilters(updateOptions.getArrayFilters()) .returnDocument(ReturnDocument.AFTER)); + // if the document was deleted between our earlier check and now, it will not have + // been updated. do not acknowledge the update if (documentAfterUpdate == null) { return UpdateResult.acknowledged(0, 0L, null); } @@ -1760,8 +1767,12 @@ UpdateResult updateOne( final CoreDocumentSynchronizationConfig config; final BsonValue documentId = BsonUtils.getDocumentId(documentAfterUpdate); + // if there was no document prior and this was an upsert, + // treat this as an insert. + // else this is an update if (documentBeforeUpdate == null && updateOptions.isUpsert()) { config = syncConfig.addSynchronizedDocument(namespace, documentId); + triggerListeningToNamespace(namespace); event = changeEventForLocalInsert(namespace, documentAfterUpdate, true); } else { config = syncConfig.getSynchronizedDocument(namespace, documentId); @@ -1806,20 +1817,35 @@ UpdateResult updateMany( final Bson filter, final Bson update, final UpdateOptions updateOptions) { - Map idToBeforeDocumentMap = new HashMap<>(); + // fetch all of the documents that this filter will match + final Map idToBeforeDocumentMap = new HashMap<>(); + final BsonArray ids = new BsonArray(); this.getLocalCollection(namespace) .find(filter) .forEach(new Block() { @Override public void apply(@NonNull final BsonDocument bsonDocument) { - idToBeforeDocumentMap.put(BsonUtils.getDocumentId(bsonDocument), bsonDocument); + final BsonValue documentId = BsonUtils.getDocumentId(bsonDocument); + ids.add(documentId); + idToBeforeDocumentMap.put(documentId, bsonDocument); } }); + // do the bulk write final UpdateResult result = this.getLocalCollection(namespace) .updateMany(filter, update, updateOptions); - this.getLocalCollection(namespace).find(filter).forEach(new Block() { + // if this was an upsert, create the post-update filter using + // the upserted id. + // else, use the matched ids from prior to create a new filter + final BsonDocument updatedFilter; + if (result.getUpsertedId() != null) { + updatedFilter = getDocumentIdFilter(result.getUpsertedId()); + } else { + updatedFilter = new BsonDocument("_id", new BsonDocument("$in", ids)); + } + + this.getLocalCollection(namespace).find(updatedFilter).forEach(new Block() { @Override public void apply(@NonNull final BsonDocument afterDocument) { final BsonDocument beforeDocument; @@ -1830,11 +1856,22 @@ public void apply(@NonNull final BsonDocument afterDocument) { return; } + // because we are looking up a bulk write, we may have queried documents + // that match the updated state, but were not actually modified. + // if the document before the update is the same as the updated doc, + // assume it was not modified and take no further action + if (afterDocument.equals(beforeDocument)) { + return; + } final CoreDocumentSynchronizationConfig config; final ChangeEvent event; + // if there was no earlier document and this was an upsert, + // treat the upsert as an insert, as far as sync is concerned + // else treat it as a standard update if (beforeDocument == null && updateOptions.isUpsert()) { config = syncConfig.addSynchronizedDocument(namespace, documentId); + triggerListeningToNamespace(namespace); event = changeEventForLocalInsert(namespace, afterDocument, true); } else { config = syncConfig.getSynchronizedDocument(namespace, documentId); @@ -1989,14 +2026,14 @@ DeleteResult deleteMany(final MongoNamespace namespace, .map(new Function() { @Override @NonNull - public BsonValue apply(@NonNull BsonDocument bsonDocument) { + public BsonValue apply(@NonNull final BsonDocument bsonDocument) { return BsonUtils.getDocumentId(bsonDocument); } }).into(new HashSet<>()); final DeleteResult result = getLocalCollection(namespace).deleteMany(filter); - for (BsonValue documentId : idsToDelete) { + for (final BsonValue documentId : idsToDelete) { final CoreDocumentSynchronizationConfig config = syncConfig.getSynchronizedDocument(namespace, documentId); diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteManyOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteManyOperation.java index ba56a244b..3732b1d94 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteManyOperation.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteManyOperation.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync.internal; import com.mongodb.MongoNamespace; @@ -6,10 +22,10 @@ import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; -import org.bson.conversions.Bson; - import javax.annotation.Nullable; +import org.bson.conversions.Bson; + class DeleteManyOperation implements Operation { private final MongoNamespace namespace; private final Bson filter; diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneOperation.java index e6978ac7c..7127995bd 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneOperation.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DeleteOneOperation.java @@ -19,12 +19,10 @@ import com.mongodb.MongoNamespace; import com.mongodb.client.result.DeleteResult; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; import javax.annotation.Nullable; -import org.bson.BsonValue; import org.bson.conversions.Bson; class DeleteOneOperation implements Operation { diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertManyAndSyncOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertManyAndSyncOperation.java index 859772952..788cf6cff 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertManyAndSyncOperation.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InsertManyAndSyncOperation.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync.internal; import com.mongodb.MongoNamespace; @@ -6,15 +22,15 @@ import com.mongodb.stitch.core.services.mongodb.remote.internal.Operation; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult; -import org.bson.BsonDocument; -import org.bson.BsonValue; - import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; +import org.bson.BsonDocument; +import org.bson.BsonValue; + class InsertManyAndSyncOperation implements Operation { private final MongoNamespace namespace; diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncOperations.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncOperations.java index 31bad39da..cd98f3aaa 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncOperations.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncOperations.java @@ -22,18 +22,20 @@ import static com.mongodb.stitch.core.internal.common.BsonUtils.toBsonDocument; import com.mongodb.MongoNamespace; +import com.mongodb.client.model.CountOptions; import com.mongodb.stitch.core.internal.common.BsonUtils; import com.mongodb.stitch.core.services.mongodb.remote.RemoteFindOptions; +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; +import java.util.ArrayList; +import java.util.List; + import org.bson.BsonDocument; import org.bson.codecs.CollectibleCodec; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; -import java.util.ArrayList; -import java.util.List; - public class SyncOperations { private final MongoNamespace namespace; @@ -94,6 +96,14 @@ private SyncFindOperation createSyncFindOperation( .sort(sortDoc); } + CountOperation count(final Bson filter, final SyncCountOptions countOptions) { + return new CountOperation( + namespace, + dataSynchronizer, + filter, + new CountOptions().limit(countOptions.getLimit())); + } + /** * Aggregates documents according to the specified aggregation pipeline. * @@ -153,17 +163,17 @@ public InsertOneAndSyncOperation insertOneAndSync(final DocumentT document) { InsertManyAndSyncOperation insertManyAndSync(final List documents) { final List bsonDocuments = new ArrayList<>(); - for (final DocumentT document : documents) { - if (getCodec(codecRegistry, documentClass) instanceof CollectibleCodec) { - bsonDocuments.add( - documentToBsonDocument( - ((CollectibleCodec) getCodec(codecRegistry, documentClass)) - .generateIdIfAbsentFromDocument(document), - codecRegistry - ) - ); - } else { - bsonDocuments.add(documentToBsonDocument(document, codecRegistry)); + for (final DocumentT document : documents) { + if (getCodec(codecRegistry, documentClass) instanceof CollectibleCodec) { + bsonDocuments.add( + documentToBsonDocument( + ((CollectibleCodec) getCodec(codecRegistry, documentClass)) + .generateIdIfAbsentFromDocument(document), + codecRegistry + ) + ); + } else { + bsonDocuments.add(documentToBsonDocument(document, codecRegistry)); } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateManyOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateManyOperation.java index 677dbf147..50e4de80b 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateManyOperation.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/UpdateManyOperation.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.core.services.mongodb.remote.sync.internal; import com.mongodb.MongoNamespace; @@ -8,11 +24,11 @@ import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult; +import javax.annotation.Nullable; + import org.bson.BsonDocument; import org.bson.conversions.Bson; -import javax.annotation.Nullable; - class UpdateManyOperation implements Operation { private final MongoNamespace namespace; diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEventUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEventUnitTests.kt index 6675e7adc..5624b625b 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEventUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEventUnitTests.kt @@ -342,7 +342,7 @@ class ChangeEventUnitTests { assertEquals(removedFields, updateDoc["\$unset"]?.asDocument()?.entries?.map { it.key }) } - fun testDiff( + private fun testDiff( collection: MongoCollection, beforeDocument: BsonDocument, expectedUpdateDocument: BsonDocument, diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt index a49a2acca..df6b7f5de 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt @@ -1,20 +1,25 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal +import com.mongodb.MongoBulkWriteException import com.mongodb.MongoWriteException import com.mongodb.stitch.core.services.mongodb.remote.RemoteFindOptions +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory +import org.bson.BsonArray +import org.bson.BsonBoolean import org.bson.BsonDocument import org.bson.BsonInt32 +import org.bson.BsonString import org.junit.After import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Assert.fail import org.junit.Test import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatcher -import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.times @@ -59,6 +64,24 @@ class CoreSyncUnitTests { eq(ctx.testDocumentId)) } + @Test + fun testCount() { + val ctx = harness.freshTestContext() + val (coreSync, _) = harness.createCoreSyncWithContext(ctx) + + val doc1 = BsonDocument("a", BsonString("b")) + val doc2 = BsonDocument("c", BsonString("d")) + + coreSync.insertManyAndSync(listOf(doc1, doc2)) + + assertEquals(2, coreSync.count()) + assertEquals(1, coreSync.count(BsonDocument("_id", doc1["_id"]))) + + assertEquals(1, coreSync.count(BsonDocument(), SyncCountOptions().limit(1))) + + verify(ctx.dataSynchronizer, times(3)).count(eq(ctx.namespace), any(), any()) + } + @Test fun testFind() { val ctx = harness.freshTestContext() @@ -119,11 +142,51 @@ class CoreSyncUnitTests { } @Test - fun testUpdateOneById() { + fun testAggregate() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + val doc1 = BsonDocument("a", BsonString("b")).append("c", BsonString("d")) + val doc2 = BsonDocument("a", BsonString("b")).append("c", BsonString("d")) + val doc3 = BsonDocument("a", BsonString("b")).append("c", BsonString("q")) + val doc4 = BsonDocument("a", BsonString("b")).append("c", BsonString("d")) + val doc5 = BsonDocument("e", BsonString("f")).append("g", BsonString("h")) + + coreSync.insertManyAndSync(listOf(doc1, doc2, doc3, doc4, doc5)) + + val pipeline = listOf( + BsonDocument( + "\$match", BsonDocument("_id", BsonDocument("\$in", BsonArray( + listOf(doc1["_id"], doc2["_id"], doc4["_id"]) + )))), + BsonDocument( + "\$project", + BsonDocument("c", BsonInt32(0)) + )) + + val agg = coreSync.aggregate(pipeline).toList() + + assertEquals(3, agg.size) + agg.forEach { assertFalse(it.containsKey("c")) } + val ids = agg.map { it["_id"] } + assertTrue(ids.contains(doc1["_id"])) + assertTrue(ids.contains(doc2["_id"])) + assertTrue(ids.contains(doc4["_id"])) + + verify(syncOperations, times(1)).aggregate(eq(pipeline), eq(BsonDocument::class.java)) + + verify(ctx.dataSynchronizer, times(1)).aggregate( + eq(ctx.namespace), eq(pipeline), eq(BsonDocument::class.java)) + } + + @Test + fun testUpdateOne() { val ctx = harness.freshTestContext() val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) - var result = coreSync.updateOne(ctx.testDocumentFilter, ctx.updateDocument) + var result = coreSync.updateOne(ctx.testDocumentFilter, + ctx.updateDocument, + SyncUpdateOptions().upsert(false)) assertEquals(0, result.matchedCount) assertEquals(0, result.modifiedCount) assertNull(result.upsertedId) @@ -143,6 +206,41 @@ class CoreSyncUnitTests { eq(ctx.namespace), eq(ctx.testDocumentFilter), eq(ctx.updateDocument), any()) } + @Test + fun testUpdateMany() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + val doc1 = BsonDocument("a", BsonString("b")) + val doc2 = BsonDocument("c", BsonString("d")) + val doc3 = BsonDocument("a", BsonString("r")) + + val insertResult = coreSync.insertManyAndSync(listOf(doc1, doc2, doc3)) + + assertEquals(3, insertResult.insertedIds.size) + + val updateFilter = BsonDocument("a", BsonDocument("\$exists", BsonBoolean(true))) + val updateDoc = BsonDocument("\$set", BsonDocument("a", BsonString("z"))) + val updateResult = coreSync.updateMany(updateFilter, updateDoc) + + assertEquals(2, updateResult.matchedCount) + assertEquals(2, updateResult.modifiedCount) + assertNull(updateResult.upsertedId) + + assertEquals(BsonDocument("a", BsonString("z")).append("_id", doc1["_id"]), + coreSync.find(BsonDocument("_id", doc1["_id"])).first()) + assertEquals(BsonDocument("c", BsonString("d")).append("_id", doc2["_id"]), + coreSync.find(BsonDocument("_id", doc2["_id"])).first()) + assertEquals(BsonDocument("a", BsonString("z")).append("_id", doc3["_id"]), + coreSync.find(BsonDocument("_id", doc3["_id"])).first()) + + verify(syncOperations, times(1)).updateMany( + eq(updateFilter), eq(updateDoc), any()) + + verify(ctx.dataSynchronizer, times(1)).updateMany( + eq(ctx.namespace), eq(updateFilter), eq(updateDoc), any()) + } + @Test fun testInsertOneAndSync() { val ctx = harness.freshTestContext() @@ -168,7 +266,35 @@ class CoreSyncUnitTests { } @Test - fun testDeleteOneById() { + fun testInsertManyAndSync() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + val doc1 = BsonDocument("a", BsonString("b")) + val doc2 = BsonDocument("c", BsonString("d")) + + val result = coreSync.insertManyAndSync(listOf(doc1, doc2)) + + assertEquals(doc1["_id"], result.insertedIds[0]) + assertEquals(doc2["_id"], result.insertedIds[1]) + + try { + coreSync.insertManyAndSync(listOf(doc1, doc2)) + fail("should have received duplicate key error index") + } catch (e: MongoBulkWriteException) { + assertNotNull(e.writeErrors[0]) + assertTrue(e.writeErrors[0].message.contains("E11000")) + } + + verify(syncOperations, times(2)).insertManyAndSync( + eq(listOf(doc1, doc2))) + + verify(ctx.dataSynchronizer, times(2)).insertManyAndSync( + eq(ctx.namespace), eq(listOf(doc1, doc2))) + } + + @Test + fun testDeleteOne() { val ctx = harness.freshTestContext() val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) @@ -188,4 +314,32 @@ class CoreSyncUnitTests { verify(ctx.dataSynchronizer, times(2)).deleteOne( eq(ctx.namespace), eq(ctx.testDocumentFilter)) } + + @Test + fun testDeleteMany() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + + val doc1 = BsonDocument("a", BsonString("b")) + val doc2 = BsonDocument("c", BsonString("d")) + val doc3 = BsonDocument("e", BsonString("f")) + + var deleteResult = coreSync.deleteMany(BsonDocument()) + assertEquals(0, deleteResult.deletedCount) + + val result = coreSync.insertManyAndSync(listOf(doc1, doc2, doc3)) + + assertEquals(3, coreSync.count()) + deleteResult = coreSync.deleteMany(BsonDocument("_id", BsonDocument("\$in", BsonArray(result.insertedIds.map { + it.value + })))) + + assertEquals(3, deleteResult.deletedCount) + + verify(syncOperations, times(1)).deleteMany( + eq(BsonDocument())) + + verify(ctx.dataSynchronizer, times(1)).deleteMany( + eq(ctx.namespace), eq(BsonDocument())) + } } diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt index 2556f417b..4b7dc0e59 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt @@ -1012,8 +1012,9 @@ class DataSynchronizerUnitTests { val result = ctx.dataSynchronizer.updateMany( ctx.namespace, BsonDocument("name", BsonString("philip")), - BsonDocument("\$inc", BsonDocument("count", BsonInt32(1)))) + BsonDocument("\$set", BsonDocument("count", BsonInt32(2)))) + ctx.findTestDocumentFromLocalCollection() assertEquals(2, result.modifiedCount) assertEquals(2, result.matchedCount) assertNull(result.upsertedId) @@ -1064,17 +1065,18 @@ class DataSynchronizerUnitTests { ctx.reconfigure() - val doc1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(1)) + val doc1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)) var result = ctx.dataSynchronizer.updateMany( ctx.namespace, BsonDocument("name", BsonString("philip")), - BsonDocument("\$inc", BsonDocument("count", BsonInt32(1))), + BsonDocument("\$set", BsonDocument("count", BsonInt32(2))), UpdateOptions().upsert(true)) assertEquals(0, result.matchedCount) assertEquals(0, result.modifiedCount) assertNotNull(result.upsertedId) + val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1.append("_id", result.upsertedId), true) ctx.waitForEvents(amount = 1) @@ -1087,16 +1089,21 @@ class DataSynchronizerUnitTests { doc1, ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) + val doc2 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(1)) + + ctx.dataSynchronizer.insertOneAndSync(ctx.namespace, doc2) + result = ctx.dataSynchronizer.updateMany( ctx.namespace, BsonDocument("name", BsonString("philip")), - BsonDocument("\$inc", BsonDocument("count", BsonInt32(1))), + BsonDocument("\$set", BsonDocument("count", BsonInt32(2))), UpdateOptions().upsert(true)) - assertEquals(1, result.matchedCount) + assertEquals(2, result.matchedCount) assertEquals(1, result.modifiedCount) assertNull(result.upsertedId) + // there should only be 2 events instead of 3 since only 1 document was modified ctx.waitForEvents(amount = 2) val expectedDocAfterUpdate1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)).append("_id", doc1["_id"]) diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt index 3b610e11b..89f85c94f 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt @@ -1,11 +1,13 @@ package com.mongodb.stitch.core.testutils.sync -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult -import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult -import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult import org.bson.BsonDocument import org.bson.BsonValue import org.bson.Document @@ -58,6 +60,24 @@ interface ProxySyncMethods { */ fun find(filter: Bson = BsonDocument()): Iterable + /** + * Aggregates documents that have been synchronized from the remote + * according to the specified aggregation pipeline. + * + * @param pipeline the aggregation pipeline + * @return an iterable containing the result of the aggregation operation + */ + fun aggregate(pipeline: List): Iterable + + /** + * Counts the number of documents in the collection that have been synchronized from the remote + * according to the given options. + * + * @param filter the query filter + * @return the number of documents in the collection + */ + fun count(filter: Bson = BsonDocument()): Long + /** * Updates a document by the given id. It is first searched for in the local synchronized cache * and if not found and there is internet connectivity, it is searched for remotely. @@ -66,7 +86,27 @@ interface ProxySyncMethods { * @param update the update specifier. * @return the result of the local or remote update. */ - fun updateOne(filter: Bson, update: Bson): RemoteUpdateResult + fun updateOne( + filter: Bson, + update: Bson, + updateOptions: SyncUpdateOptions = SyncUpdateOptions() + ): SyncUpdateResult + + /** + * Update all documents that have been synchronized from the remote + * in the collection according to the specified arguments. + * + * @param filter a document describing the query filter, which may not be null. + * @param update a document describing the update, which may not be null. The update to + * apply must include only update operators. + * @param updateOptions the options to apply to the update operation + * @return the result of the update many operation + */ + fun updateMany( + filter: Bson, + update: Bson, + updateOptions: SyncUpdateOptions = SyncUpdateOptions() + ): SyncUpdateResult /** * Inserts a single document and begins to synchronize it. @@ -74,7 +114,15 @@ interface ProxySyncMethods { * @param document the document to insert and synchronize. * @return the result of the insertion. */ - fun insertOneAndSync(document: Document): RemoteInsertOneResult + fun insertOneAndSync(document: Document): SyncInsertOneResult + + /** + * Inserts one or more documents. Begin synchronizing on the documents' ids. + * + * @param documents the documents to insert + * @return the result of the insert many operation + */ + fun insertManyAndSync(documents: List): SyncInsertManyResult /** * Deletes a single document by the given id. It is first searched for in the local synchronized @@ -83,7 +131,16 @@ interface ProxySyncMethods { * @param filter the query filter * @return the result of the local or remote update. */ - fun deleteOne(filter: Bson): RemoteDeleteResult + fun deleteOne(filter: Bson): SyncDeleteResult + + /** + * Removes all documents from the collection that have been synchronized from the remote + * that match the given query filter. If no documents match, the collection is not modified. + * + * @param filter the query filter to apply the the delete operation + * @return the result of the remove many operation + */ + fun deleteMany(filter: Bson): SyncDeleteResult /** * Return the set of synchronized document _ids in a namespace diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt index 02f868f04..eecb459f9 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestProxy.kt @@ -11,6 +11,7 @@ import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler import com.mongodb.stitch.core.services.mongodb.remote.sync.DefaultSyncConflictResolvers import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent import org.bson.BsonBoolean import org.bson.BsonDocument @@ -21,6 +22,8 @@ import org.bson.Document import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Ignore import org.junit.Test @@ -1343,6 +1346,188 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { } } + @Test + fun testReadsBeforeAndAfterSync() { + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + coll.configure(failingConflictHandler, null, null) + + val doc1 = Document("hello", "world") + val doc2 = Document("hello", "friend") + val doc3 = Document("hello", "goodbye") + + val insertResult = remoteColl.insertMany(listOf(doc1, doc2, doc3)) + assertEquals(3, insertResult.insertedIds.size) + + assertEquals(0, coll.count()) + assertEquals(0, coll.find().toList().size) + assertEquals(0, coll.aggregate(listOf(Document(mapOf( + "\$match" to mapOf("_id" to mapOf("\$in" to insertResult.insertedIds.map { + it.value })) + )))).toList().size) + + insertResult.insertedIds.forEach { coll.syncOne(it.value) } + streamAndSync() + + assertEquals(3, coll.count()) + assertEquals(3, coll.find().toList().size) + assertEquals(3, coll.aggregate(listOf(Document(mapOf( + "\$match" to mapOf("_id" to mapOf("\$in" to insertResult.insertedIds.map { + it.value })) + )))).toList().size) + + insertResult.insertedIds.forEach { coll.desyncOne(it.value) } + streamAndSync() + + assertEquals(0, coll.count()) + assertEquals(0, coll.find().toList().size) + assertEquals(0, coll.aggregate(listOf(Document(mapOf( + "\$match" to mapOf("_id" to mapOf("\$in" to insertResult.insertedIds.map { + it.value })) + )))).toList().size) + } + + @Test + fun testInsertManyNoConflicts() { + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + coll.configure(failingConflictHandler, null, null) + + val doc1 = Document("hello", "world") + val doc2 = Document("hello", "friend") + val doc3 = Document("hello", "goodbye") + + val insertResult = coll.insertManyAndSync(listOf(doc1, doc2, doc3)) + assertEquals(3, insertResult.insertedIds.size) + + assertEquals(3, coll.count()) + assertEquals(3, coll.find().toList().size) + assertEquals(3, coll.aggregate(listOf(Document(mapOf( + "\$match" to mapOf("_id" to mapOf("\$in" to insertResult.insertedIds.map { + it.value })) + )))).toList().size) + + assertEquals(0, remoteColl.find(Document()).toList().size) + streamAndSync() + + assertEquals(3, remoteColl.find(Document()).toList().size) + assertEquals(doc1, withoutSyncVersion(remoteColl.find(Document("_id", doc1["_id"])).first()!!)) + assertEquals(doc2, withoutSyncVersion(remoteColl.find(Document("_id", doc2["_id"])).first()!!)) + assertEquals(doc3, withoutSyncVersion(remoteColl.find(Document("_id", doc3["_id"])).first()!!)) + } + + @Test + fun testUpdateManyNoConflicts() { + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + coll.configure(failingConflictHandler, null, null) + + var updateResult = coll.updateMany( + Document(mapOf( + "fish" to listOf("one", "two", "red", "blue") + )), + Document("\$set", mapOf( + "fish" to listOf("black", "blue", "old", "new") + ))) + + assertEquals(0, updateResult.modifiedCount) + assertEquals(0, updateResult.matchedCount) + assertNull(updateResult.upsertedId) + + updateResult = coll.updateMany( + Document(mapOf( + "fish" to listOf("one", "two", "red", "blue") + )), + Document("\$set", mapOf( + "fish" to listOf("black", "blue", "old", "new") + )), + SyncUpdateOptions().upsert(true)) + + assertEquals(0, updateResult.modifiedCount) + assertEquals(0, updateResult.matchedCount) + assertNotNull(updateResult.upsertedId) + + val doc1 = Document(mapOf( + "hello" to "world", + "fish" to listOf("one", "two", "red", "blue") + )) + val doc2 = Document("hello", "friend") + val doc3 = Document("hello", "goodbye") + + val insertResult = coll.insertManyAndSync(listOf(doc1, doc2, doc3)) + assertEquals(3, insertResult.insertedIds.size) + + streamAndSync() + + assertEquals(4, remoteColl.find(Document()).toList().size) + + updateResult = coll.updateMany( + Document("fish", Document("\$exists", true)), + Document("\$set", Document("fish", listOf("trout", "mackerel", "cod", "hake"))) + ) + + assertEquals(2, updateResult.modifiedCount) + assertEquals(2, updateResult.matchedCount) + assertNull(updateResult.upsertedId) + + assertEquals(4, coll.count()) + + var localFound = coll.find(Document("fish", Document("\$exists", true))) + assertEquals(2, localFound.toList().size) + localFound.forEach { assertEquals(listOf("trout", "mackerel", "cod", "hake"), it!!["fish"]) } + + streamAndSync() + + val remoteFound = remoteColl.find(Document("fish", Document("\$exists", true))) + localFound = coll.find(Document("fish", Document("\$exists", true))) + + assertEquals(2, localFound.toList().size) + assertEquals(2, remoteFound.toList().size) + localFound.forEach { assertEquals(listOf("trout", "mackerel", "cod", "hake"), it!!["fish"]) } + remoteFound.forEach { assertEquals(listOf("trout", "mackerel", "cod", "hake"), it!!["fish"]) } + } + + @Test + fun testDeleteManyNoConflicts() { + val coll = syncTestRunner.syncMethods() + val remoteColl = syncTestRunner.remoteMethods() + + coll.configure(failingConflictHandler, null, null) + + val doc1 = Document("hello", "world") + val doc2 = Document("hello", "friend") + val doc3 = Document("hello", "goodbye") + + val insertResult = coll.insertManyAndSync(listOf(doc1, doc2, doc3)) + assertEquals(3, insertResult.insertedIds.size) + + assertEquals(3, coll.count()) + assertEquals(3, coll.find().toList().size) + assertEquals(3, coll.aggregate(listOf(Document(mapOf( + "\$match" to mapOf("_id" to mapOf("\$in" to insertResult.insertedIds.map { + it.value })) + )))).toList().size) + + assertEquals(0, remoteColl.find(Document()).toList().size) + streamAndSync() + + assertEquals(3, remoteColl.find(Document()).toList().size) + coll.deleteMany(Document(mapOf( + "_id" to mapOf("\$in" to insertResult.insertedIds.map { + it.value })))) + + assertEquals(3, remoteColl.find(Document()).toList().size) + assertEquals(0, coll.find(Document()).toList().size) + + streamAndSync() + + assertEquals(0, remoteColl.find(Document()).toList().size) + assertEquals(0, coll.find(Document()).toList().size) + } + private fun watchForEvents(namespace: MongoNamespace, n: Int = 1): Semaphore { println("watching for $n change event(s) ns=$namespace") val waitFor = AtomicInteger(n) diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt index 61be07097..a24e628e7 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/SyncIntTestRunner.kt @@ -135,4 +135,16 @@ interface SyncIntTestRunner { @Test fun testResumeSyncForDocumentResumesSync() + + @Test + fun testReadsBeforeAndAfterSync() + + @Test + fun testInsertManyNoConflicts() + + @Test + fun testUpdateManyNoConflicts() + + @Test + fun testDeleteManyNoConflicts() } diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java index d05ebf5b6..a762ffa95 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java @@ -16,13 +16,8 @@ package com.mongodb.stitch.server.services.mongodb.remote; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; -import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; -import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; -import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncFindIterable; import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; @@ -112,14 +107,15 @@ void configure(@Nonnull final ConflictHandler conflictResolver, boolean resumeSyncForDocument(@Nonnull final BsonValue documentId); /** - * Counts the number of documents in the collection. + * Counts the number of documents in the collection that have been synchronized from the remote. * * @return the number of documents in the collection */ long count(); /** - * Counts the number of documents in the collection according to the given options. + * Counts the number of documents in the collection that have been synchronized from the remote + * according to the given options. * * @param filter the query filter * @return the number of documents in the collection @@ -127,7 +123,8 @@ void configure(@Nonnull final ConflictHandler conflictResolver, long count(final Bson filter); /** - * Counts the number of documents in the collection according to the given options. + * Counts the number of documents in the collection that have been synchronized from the remote + * according to the given options. * * @param filter the query filter * @param options the options describing the count @@ -136,14 +133,14 @@ void configure(@Nonnull final ConflictHandler conflictResolver, long count(final Bson filter, final SyncCountOptions options); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @return the find iterable interface */ SyncFindIterable find(); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param resultClass the class to decode each document into * @param the target document type of the iterable. @@ -152,7 +149,7 @@ void configure(@Nonnull final ConflictHandler conflictResolver, SyncFindIterable find(final Class resultClass); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param filter the query filter * @return the find iterable interface @@ -160,7 +157,7 @@ void configure(@Nonnull final ConflictHandler conflictResolver, SyncFindIterable find(final Bson filter); /** - * Finds all documents in the collection. + * Finds all documents in the collection that have been synchronized from the remote. * * @param filter the query filter * @param resultClass the class to decode each document into @@ -173,7 +170,8 @@ SyncFindIterable find( /** - * Aggregates documents according to the specified aggregation pipeline. + * Aggregates documents that have been synchronized from the remote + * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline * @return an iterable containing the result of the aggregation operation @@ -181,7 +179,8 @@ SyncFindIterable find( SyncAggregateIterable aggregate(final List pipeline); /** - * Aggregates documents according to the specified aggregation pipeline. + * Aggregates documents that have been synchronized from the remote + * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline * @param resultClass the class to decode each document into @@ -194,7 +193,7 @@ SyncAggregateIterable aggregate( /** * Inserts the provided document. If the document is missing an identifier, the client should - * generate one. + * generate one. Begin syncing the document against the remote. * * @param document the document to insert * @return the result of the insert one operation @@ -202,7 +201,7 @@ SyncAggregateIterable aggregate( SyncInsertOneResult insertOneAndSync(final DocumentT document); /** - * Inserts one or more documents. + * Inserts one or more documents. Begin syncing the documents against the remote. * * @param documents the documents to insert * @return the result of the insert many operation @@ -210,8 +209,8 @@ SyncAggregateIterable aggregate( SyncInsertManyResult insertManyAndSync(final List documents); /** - * Removes at most one document from the collection that matches the given filter. If no - * documents match, the collection is not + * Removes at most one document from the collection that has been synchronized from the remote + * and matches the given filter. If no documents match, the collection is not * modified. * * @param filter the query filter to apply the the delete operation @@ -220,7 +219,8 @@ SyncAggregateIterable aggregate( SyncDeleteResult deleteOne(final Bson filter); /** - * Removes all documents from the collection that match the given query filter. If no documents + * Removes all documents from the collection that have been synchronized from the remote + * and match the given query filter. If no documents * match, the collection is not modified. * * @param filter the query filter to apply the the delete operation @@ -229,7 +229,8 @@ SyncAggregateIterable aggregate( SyncDeleteResult deleteMany(final Bson filter); /** - * Update a single document in the collection according to the specified arguments. + * Update a single document in the collection that has been synchronized from the remote + * according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -239,7 +240,8 @@ SyncAggregateIterable aggregate( SyncUpdateResult updateOne(final Bson filter, final Bson update); /** - * Update a single document in the collection according to the specified arguments. + * Update a single document that has been synchronized from the remote + * in the collection according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -253,7 +255,8 @@ SyncUpdateResult updateOne( final SyncUpdateOptions updateOptions); /** - * Update all documents in the collection according to the specified arguments. + * Update all documents in the collection that have been synchronized from the remote + * according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -263,7 +266,8 @@ SyncUpdateResult updateOne( SyncUpdateResult updateMany(final Bson filter, final Bson update); /** - * Update all documents in the collection according to the specified arguments. + * Update all documents in the collection that have been synchronized from the remote + * according to the specified arguments. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/SyncAggregateIterable.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/SyncAggregateIterable.java index eb81cff35..0e6c2ea6a 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/SyncAggregateIterable.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/SyncAggregateIterable.java @@ -1,4 +1,25 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.server.services.mongodb.remote; +/** + * Iterable for aggregate. + * + * @param The type of the result. + */ public interface SyncAggregateIterable extends RemoteMongoIterable { } diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncAggregateIterableImpl.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncAggregateIterableImpl.java index fc026458f..8c7fdffea 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncAggregateIterableImpl.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncAggregateIterableImpl.java @@ -1,3 +1,19 @@ +/* + * Copyright 2018-present MongoDB, Inc. + * + * 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.mongodb.stitch.server.services.mongodb.remote.internal; import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSyncAggregateIterable; diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncImpl.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncImpl.java index 392fe2127..791a6f793 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncImpl.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncImpl.java @@ -119,63 +119,67 @@ public long count() { } @Override - public long count(Bson filter) { + public long count(final Bson filter) { return this.count(filter, new SyncCountOptions()); } @Override - public long count(Bson filter, SyncCountOptions options) { + public long count(final Bson filter, final SyncCountOptions options) { return this.proxy.count(filter, options); } @Override - public SyncAggregateIterable aggregate(List pipeline) { + public SyncAggregateIterable aggregate(final List pipeline) { return new SyncAggregateIterableImpl<>(this.proxy.aggregate(pipeline)); } @Override - public SyncAggregateIterable aggregate(List pipeline, - Class resultClass) { + public SyncAggregateIterable aggregate(final List pipeline, + final Class resultClass) { return new SyncAggregateIterableImpl<>(this.proxy.aggregate(pipeline, resultClass)); } @Override - public SyncInsertOneResult insertOneAndSync(DocumentT document) { + public SyncInsertOneResult insertOneAndSync(final DocumentT document) { return proxy.insertOneAndSync(document); } @Override - public SyncInsertManyResult insertManyAndSync(List documents) { + public SyncInsertManyResult insertManyAndSync(final List documents) { return proxy.insertManyAndSync(documents); } @Override - public SyncUpdateResult updateOne(Bson filter, Bson update) { + public SyncUpdateResult updateOne(final Bson filter, final Bson update) { return proxy.updateOne(filter, update); } @Override - public SyncUpdateResult updateOne(Bson filter, Bson update, SyncUpdateOptions updateOptions) { + public SyncUpdateResult updateOne(final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions) { return proxy.updateOne(filter, update, updateOptions); } @Override - public SyncUpdateResult updateMany(Bson filter, Bson update) { + public SyncUpdateResult updateMany(final Bson filter, final Bson update) { return proxy.updateMany(filter, update); } @Override - public SyncUpdateResult updateMany(Bson filter, Bson update, SyncUpdateOptions updateOptions) { + public SyncUpdateResult updateMany(final Bson filter, + final Bson update, + final SyncUpdateOptions updateOptions) { return proxy.updateMany(filter, update, updateOptions); } @Override - public SyncDeleteResult deleteOne(Bson filter) { + public SyncDeleteResult deleteOne(final Bson filter) { return proxy.deleteOne(filter); } @Override - public SyncDeleteResult deleteMany(Bson filter) { + public SyncDeleteResult deleteMany(final Bson filter) { return proxy.deleteMany(filter); } } diff --git a/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt b/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt index 677fcde30..24c509177 100644 --- a/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt +++ b/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/internal/SyncMongoClientIntTests.kt @@ -15,6 +15,11 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler import com.mongodb.stitch.core.services.mongodb.remote.sync.ErrorListener +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertOneResult +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateOptions +import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer import com.mongodb.stitch.core.testutils.sync.ProxyRemoteMethods import com.mongodb.stitch.core.testutils.sync.ProxySyncMethods @@ -69,18 +74,30 @@ class SyncMongoClientIntTests : BaseStitchServerIntTest(), SyncIntTestRunner { sync.syncOne(id) } - override fun insertOneAndSync(document: Document): RemoteInsertOneResult { + override fun insertOneAndSync(document: Document): SyncInsertOneResult { return sync.insertOneAndSync(document) } - override fun updateOne(filter: Bson, update: Bson): RemoteUpdateResult { - return sync.updateOne(filter, update) + override fun insertManyAndSync(documents: List): SyncInsertManyResult { + return sync.insertManyAndSync(documents) } - override fun deleteOne(filter: Bson): RemoteDeleteResult { + override fun updateOne(filter: Bson, update: Bson, updateOptions: SyncUpdateOptions): SyncUpdateResult { + return sync.updateOne(filter, update, updateOptions) + } + + override fun updateMany(filter: Bson, update: Bson, updateOptions: SyncUpdateOptions): SyncUpdateResult { + return sync.updateMany(filter, update, updateOptions) + } + + override fun deleteOne(filter: Bson): SyncDeleteResult { return sync.deleteOne(filter) } + override fun deleteMany(filter: Bson): SyncDeleteResult { + return sync.deleteMany(filter) + } + override fun getSyncedIds(): Set { return sync.syncedIds } @@ -100,6 +117,14 @@ class SyncMongoClientIntTests : BaseStitchServerIntTest(), SyncIntTestRunner { override fun getPausedDocumentIds(): Set { return sync.pausedDocumentIds } + + override fun count(filter: Bson): Long { + return sync.count(filter) + } + + override fun aggregate(pipeline: List): Iterable { + return sync.aggregate(pipeline) + } } private val mongodbUriProp = "test.stitch.mongodbURI" @@ -312,6 +337,26 @@ class SyncMongoClientIntTests : BaseStitchServerIntTest(), SyncIntTestRunner { testProxy.testResumeSyncForDocumentResumesSync() } + @Test + override fun testReadsBeforeAndAfterSync() { + testProxy.testReadsBeforeAndAfterSync() + } + + @Test + override fun testInsertManyNoConflicts() { + testProxy.testInsertManyNoConflicts() + } + + @Test + override fun testUpdateManyNoConflicts() { + testProxy.testUpdateManyNoConflicts() + } + + @Test + override fun testDeleteManyNoConflicts() { + testProxy.testDeleteManyNoConflicts() + } + /** * Get the uri for where mongodb is running locally. */ From fb1c618c71ff8a4b0c2b65376ff42350d194ae6a Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Wed, 31 Oct 2018 17:11:18 +0000 Subject: [PATCH 11/14] Fix race in upsertMany; add check for triggerListeneredToNamespace in upsert tests --- .../remote/sync/internal/ChangeEvent.java | 16 ++++--- .../sync/internal/DataSynchronizer.java | 26 ++++++++---- .../internal/DataSynchronizerTestContext.kt | 8 +++- .../internal/DataSynchronizerUnitTests.kt | 42 +++++++++++++++++-- .../NamespaceChangeStreamListenerUnitTests.kt | 4 +- .../sync/internal/SyncUnitTestHarness.kt | 18 +++++--- 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java index 1bb8b0208..e89989650 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java @@ -28,6 +28,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; + +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.bson.BsonArray; import org.bson.BsonBoolean; @@ -195,8 +197,8 @@ BsonDocument toUpdateDocument() { * @return a description of the updated fields and removed keys between * the documents */ - private static UpdateDescription diff(final BsonDocument beforeDocument, - final BsonDocument afterDocument, + private static UpdateDescription diff(final @Nonnull BsonDocument beforeDocument, + final @Nonnull BsonDocument afterDocument, final @Nullable String onKey, final BsonDocument updatedFields, final List removedFields) { @@ -245,7 +247,7 @@ private static UpdateDescription diff(final BsonDocument beforeDocument, // it is a new key with a new value. // updatedFields will included keys that must // be newly created. - final String actualKey = onKey == null ? key : String.format("%s.%s", onKey, key);; + final String actualKey = onKey == null ? key : String.format("%s.%s", onKey, key); if (!beforeDocument.containsKey(key)) { updatedFields.put(actualKey, newValue); } @@ -266,8 +268,12 @@ private static UpdateDescription diff(final BsonDocument beforeDocument, * @return a description of the updated fields and removed keys between * the documents */ - static UpdateDescription diff(final BsonDocument beforeDocument, - final BsonDocument afterDocument) { + static UpdateDescription diff(@Nullable final BsonDocument beforeDocument, + @Nullable final BsonDocument afterDocument) { + if (beforeDocument == null || afterDocument == null) { + return new UpdateDescription(new BsonDocument(), new ArrayList<>()); + } + return UpdateDescription.diff( beforeDocument, afterDocument, diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 643203f96..314a9bc9b 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -1556,7 +1556,7 @@ public void desyncDocumentFromRemote( * A document that is paused no longer has remote updates applied to it. * Any local updates to this document cause it to be resumed. An example of pausing a document * is when a conflict is being resolved for that document and the handler throws an exception. - *

+ * * This method allows you to resume sync for a document. * * @param namespace namespace for the document @@ -1831,28 +1831,35 @@ public void apply(@NonNull final BsonDocument bsonDocument) { } }); + // use the matched ids from prior to create a new filter. + // this will prevent any race conditions if documents were + // inserted between the prior find + Bson updatedFilter = updateOptions.isUpsert() + ? filter : new BsonDocument("_id", new BsonDocument("$in", ids)); + // do the bulk write final UpdateResult result = this.getLocalCollection(namespace) - .updateMany(filter, update, updateOptions); + .updateMany(updatedFilter, update, updateOptions); // if this was an upsert, create the post-update filter using // the upserted id. - // else, use the matched ids from prior to create a new filter - final BsonDocument updatedFilter; if (result.getUpsertedId() != null) { updatedFilter = getDocumentIdFilter(result.getUpsertedId()); - } else { - updatedFilter = new BsonDocument("_id", new BsonDocument("$in", ids)); } + // iterate over the after-update docs using the updated filter this.getLocalCollection(namespace).find(updatedFilter).forEach(new Block() { @Override public void apply(@NonNull final BsonDocument afterDocument) { - final BsonDocument beforeDocument; + // get the id of the after-update document, and fetch the before-update + // document from the map we created from our pre-update `find` final BsonValue documentId = BsonUtils.getDocumentId(afterDocument); + final BsonDocument beforeDocument = idToBeforeDocumentMap.get(documentId); - if ((beforeDocument = idToBeforeDocumentMap.get(documentId)) == null - && !updateOptions.isUpsert()) { + // if there was no before-update document and this was not an upsert, + // a document that meets the filter criteria must have been + // inserted or upserted asynchronously between this find and the update. + if (beforeDocument == null && !updateOptions.isUpsert()) { return; } @@ -1863,6 +1870,7 @@ public void apply(@NonNull final BsonDocument afterDocument) { if (afterDocument.equals(beforeDocument)) { return; } + final CoreDocumentSynchronizationConfig config; final ChangeEvent event; diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt index bd4d927d9..c3098f25f 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerTestContext.kt @@ -10,6 +10,7 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollectionImpl import org.bson.BsonDocument import org.bson.BsonValue +import org.bson.Document import java.io.Closeable import java.lang.Exception @@ -113,7 +114,7 @@ interface DataSynchronizerTestContext : Closeable { /** * Verify the stream function was called. */ - fun verifyWatchFunctionCalled(times: Int, expectedArgs: List) + fun verifyWatchFunctionCalled(times: Int, expectedArgs: Document) /** * Verify dataSynchronizer.start() has been called. @@ -133,7 +134,10 @@ interface DataSynchronizerTestContext : Closeable { /** * Queue a pseudo-remote update event to be consumed during R2L. */ - fun queueConsumableRemoteUpdateEvent() + fun queueConsumableRemoteUpdateEvent( + id: BsonValue = testDocumentId, + document: BsonDocument = testDocument + ) /** * Queue a pseudo-remote delete event to be consumed during R2L. diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt index 4b7dc0e59..4488e3c28 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt @@ -11,6 +11,7 @@ import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedM import org.bson.BsonDocument import org.bson.BsonInt32 import org.bson.BsonString +import org.bson.Document import org.bson.codecs.BsonDocumentCodec import org.bson.codecs.configuration.CodecRegistries import org.junit.After @@ -971,6 +972,14 @@ class DataSynchronizerUnitTests { doc1, ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) + // assert that the stream was opened + ctx.verifyWatchFunctionCalled(1, expectedArgs = + Document(mapOf( + "database" to ctx.namespace.databaseName, + "collection" to ctx.namespace.collectionName, + "ids" to setOf(result.upsertedId) + ))) + ctx.dataSynchronizer.updateMany( ctx.namespace, BsonDocument("name", BsonString("philip")), @@ -1077,7 +1086,9 @@ class DataSynchronizerUnitTests { assertEquals(0, result.modifiedCount) assertNotNull(result.upsertedId) - val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1.append("_id", result.upsertedId), true) + val expectedEvent1 = ChangeEvent.changeEventForLocalInsert( + ctx.namespace, + doc1.append("_id", result.upsertedId), true) ctx.waitForEvents(amount = 1) @@ -1087,7 +1098,28 @@ class DataSynchronizerUnitTests { assertEquals( doc1, - ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", result.upsertedId)).first()) + + // assert that the stream was opened + ctx.verifyWatchFunctionCalled(1, expectedArgs = + Document(mapOf( + "database" to ctx.namespace.databaseName, + "collection" to ctx.namespace.collectionName, + "ids" to setOf(result.upsertedId) + ))) + + ctx.doSyncPass() + + ctx.queueConsumableRemoteUpdateEvent( + id = result.upsertedId!!, + document = BsonDocument( + "name", + BsonString("philip") + ).append("count", BsonInt32(3)).append("_id", result.upsertedId)) + ctx.doSyncPass() + assertEquals( + BsonDocument("name", BsonString("philip")).append("count", BsonInt32(3)).append("_id", result.upsertedId), + ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", result.upsertedId)).first()) val doc2 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(1)) @@ -1096,7 +1128,7 @@ class DataSynchronizerUnitTests { result = ctx.dataSynchronizer.updateMany( ctx.namespace, BsonDocument("name", BsonString("philip")), - BsonDocument("\$set", BsonDocument("count", BsonInt32(2))), + BsonDocument("\$set", BsonDocument("count", BsonInt32(3))), UpdateOptions().upsert(true)) assertEquals(2, result.matchedCount) @@ -1106,11 +1138,13 @@ class DataSynchronizerUnitTests { // there should only be 2 events instead of 3 since only 1 document was modified ctx.waitForEvents(amount = 2) - val expectedDocAfterUpdate1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)).append("_id", doc1["_id"]) + val expectedDocAfterUpdate1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(3)).append("_id", doc1["_id"]) assertEquals( expectedDocAfterUpdate1, ctx.dataSynchronizer.find(ctx.namespace, BsonDocument("_id", doc1["_id"])).first()) + + assertTrue(ctx.dataSynchronizer.areAllStreamsOpen()) } @Test diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListenerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListenerUnitTests.kt index e7d1a8143..72907e3f2 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListenerUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListenerUnitTests.kt @@ -3,6 +3,7 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal import com.mongodb.stitch.core.internal.net.Event import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory import org.bson.BsonObjectId +import org.bson.Document import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -12,7 +13,6 @@ import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.Mockito.verify -import java.util.Collections class NamespaceChangeStreamListenerUnitTests { private val harness = SyncUnitTestHarness() @@ -47,7 +47,7 @@ class NamespaceChangeStreamListenerUnitTests { // set the nsConfig to stale `when`(nsConfigMock.synchronizedDocumentIds).thenReturn(setOf(BsonObjectId())) assertTrue(namespaceChangeStreamListener.openStream()) - val expectedArgs = Collections.singletonList(mapOf( + val expectedArgs = Document(mapOf( "database" to ctx.namespace.databaseName, "collection" to ctx.namespace.collectionName, "ids" to nsConfigMock.synchronizedDocumentIds diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt index 8b69b222e..760a8a501 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt @@ -29,6 +29,7 @@ import org.bson.BsonInt32 import org.bson.BsonObjectId import org.bson.BsonString import org.bson.BsonValue +import org.bson.Document import org.bson.codecs.BsonDocumentCodec import org.bson.codecs.configuration.CodecRegistries import org.bson.types.ObjectId @@ -44,9 +45,10 @@ import org.mockito.Mockito.spy import org.mockito.Mockito.times import java.io.Closeable import java.lang.Exception +import java.util.Collections +import java.util.Random import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit -import java.util.Random import java.util.concurrent.locks.ReentrantLock class SyncUnitTestHarness : Closeable { @@ -427,10 +429,13 @@ class SyncUnitTestHarness : Closeable { mapOf()) } - override fun queueConsumableRemoteUpdateEvent() { + override fun queueConsumableRemoteUpdateEvent( + id: BsonValue, + document: BsonDocument + ) { `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( - mapOf(testDocument to ChangeEvent.changeEventForLocalUpdate( - namespace, testDocumentId, null, testDocument, false)), + mapOf(document to ChangeEvent.changeEventForLocalUpdate( + namespace, id, null, document, false)), mapOf()) } @@ -509,8 +514,9 @@ class SyncUnitTestHarness : Closeable { } } - override fun verifyWatchFunctionCalled(times: Int, expectedArgs: List) { - Mockito.verify(service, times(times)).streamFunction(eq("watch"), eq(expectedArgs), eq(ChangeEvent.changeEventCoder)) + override fun verifyWatchFunctionCalled(times: Int, expectedArgs: Document) { + Mockito.verify(service, times(times)).streamFunction( + eq("watch"), eq(Collections.singletonList(expectedArgs)), eq(ChangeEvent.changeEventCoder)) } override fun verifyStartCalled(times: Int) { From baf70eda20def55d202f247eb5b948d92ffb5acd Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Wed, 31 Oct 2018 19:14:36 +0000 Subject: [PATCH 12/14] Fix aggressive auto-refactor --- .../remote/internal/CoreRemoteMongoCollectionUnitTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionUnitTests.java b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionUnitTests.java index 103b37620..1c4a767ca 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionUnitTests.java +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionUnitTests.java @@ -355,7 +355,7 @@ public void testInsertOne() { funcArgsArg.capture(), resultClassArg.capture()); - assertEquals("insertManyAndSync", funcNameArg.getValue()); + assertEquals("insertMany", funcNameArg.getValue()); assertEquals(1, funcArgsArg.getValue().size()); final Document expectedArgs = new Document(); expectedArgs.put("database", "dbName1"); From ddcf6d6c2e8e919c34695b3b077d8e4fd2898de6 Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Thu, 1 Nov 2018 11:49:55 +0000 Subject: [PATCH 13/14] Added custom codec tests; address remarks; add extra upsert check --- .../internal/SyncMongoClientIntTests.kt | 2 +- .../android/services/mongodb/remote/Sync.java | 49 ++++---- .../mongodb/remote/sync/CoreSync.java | 42 ++++--- .../sync/internal/DataSynchronizer.java | 2 +- .../remote/sync/internal/CoreSyncUnitTests.kt | 111 ++++++++++++++++-- .../internal/DataSynchronizerUnitTests.kt | 14 ++- .../sync/internal/SyncUnitTestHarness.kt | 13 +- .../core/testutils/sync/ProxySyncMethods.kt | 99 ---------------- .../server/services/mongodb/remote/Sync.java | 49 ++++---- 9 files changed, 199 insertions(+), 182 deletions(-) diff --git a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt index 4419c6cea..956a769cf 100644 --- a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt +++ b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/internal/SyncMongoClientIntTests.kt @@ -77,7 +77,7 @@ class SyncMongoClientIntTests : BaseStitchAndroidIntTest(), SyncIntTestRunner { } override fun count(filter: Bson): Long { - return Tasks.await(sync.count()) + return Tasks.await(sync.count(filter)) } override fun aggregate(pipeline: List): Iterable { diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java index 454289ec4..fd16cb17c 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/Sync.java @@ -107,14 +107,14 @@ void configure(@NonNull final ConflictHandler conflictHandler, boolean resumeSyncForDocument(@NonNull final BsonValue documentId); /** - * Counts the number of documents in the collection. + * Counts the number of documents in the collection that have been synchronized with the remote. * * @return the number of documents in the collection */ Task count(); /** - * Counts the number of documents in the collection that have been synchronized from the remote + * Counts the number of documents in the collection that have been synchronized with the remote * according to the given options. * * @param filter the query filter @@ -123,7 +123,7 @@ void configure(@NonNull final ConflictHandler conflictHandler, Task count(final Bson filter); /** - * Counts the number of documents in the collection that have been synchronized from the remote + * Counts the number of documents in the collection that have been synchronized with the remote * according to the given options. * * @param filter the query filter @@ -133,14 +133,14 @@ void configure(@NonNull final ConflictHandler conflictHandler, Task count(final Bson filter, final SyncCountOptions options); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @return the find iterable interface */ SyncFindIterable find(); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param resultClass the class to decode each document into * @param the target document type of the iterable. @@ -149,7 +149,7 @@ void configure(@NonNull final ConflictHandler conflictHandler, SyncFindIterable find(final Class resultClass); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param filter the query filter * @return the find iterable interface @@ -157,7 +157,7 @@ void configure(@NonNull final ConflictHandler conflictHandler, SyncFindIterable find(final Bson filter); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param filter the query filter * @param resultClass the class to decode each document into @@ -170,7 +170,7 @@ SyncFindIterable find( /** - * Aggregates documents that have been synchronized from the remote + * Aggregates documents that have been synchronized with the remote * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline @@ -179,7 +179,7 @@ SyncFindIterable find( SyncAggregateIterable aggregate(final List pipeline); /** - * Aggregates documents that have been synchronized from the remote + * Aggregates documents that have been synchronized with the remote * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline @@ -192,8 +192,8 @@ SyncAggregateIterable aggregate( final Class resultClass); /** - * Inserts the provided document. If the document is missing an identifier, the client should - * generate one. Begin synchronizating on the document's id. + * Inserts the provided document. If the document is missing an identifier, one will be + * generated. Begin synchronizating on the document's id. * * @param document the document to insert * @return the result of the insert one operation @@ -201,7 +201,8 @@ SyncAggregateIterable aggregate( Task insertOneAndSync(final DocumentT document); /** - * Inserts one or more documents. Begin synchronizing on the documents' ids. + * Inserts one or more documents. If the documents are missing an identifier, they will be + * generated. Begin synchronizing on the documents' ids. * * @param documents the documents to insert * @return the result of the insert many operation @@ -209,7 +210,7 @@ SyncAggregateIterable aggregate( Task insertManyAndSync(final List documents); /** - * Removes at most one document that has been synchronized from the remote + * Removes at most one document that has been synchronized with the remote * from the collection that matches the given filter. If no * documents match, the collection is not modified. * @@ -219,7 +220,7 @@ SyncAggregateIterable aggregate( Task deleteOne(final Bson filter); /** - * Removes all documents from the collection that have been synchronized from the remote + * Removes all documents from the collection that have been synchronized with the remote * that match the given query filter. If no documents match, the collection is not modified. * * @param filter the query filter to apply the the delete operation @@ -228,8 +229,9 @@ SyncAggregateIterable aggregate( Task deleteMany(final Bson filter); /** - * Update a single document that has been synchronized from the remote - * in the collection according to the specified arguments. + * Update a single document that has been synchronized with the remote + * in the collection according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -239,8 +241,9 @@ SyncAggregateIterable aggregate( Task updateOne(final Bson filter, final Bson update); /** - * Update a single document that has been synchronized from the remote - * in the collection according to the specified arguments. + * Update a single document that has been synchronized with the remote + * in the collection according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -254,8 +257,9 @@ Task updateOne( final SyncUpdateOptions updateOptions); /** - * Update all documents that have been synchronized from the remote - * in the collection according to the specified arguments. + * Update all documents that have been synchronized with the remote + * in the collection according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -265,8 +269,9 @@ Task updateOne( Task updateMany(final Bson filter, final Bson update); /** - * Update all documents that have been synchronized from the remote - * in the collection according to the specified arguments. + * Update all documents that have been synchronized with the remote + * in the collection according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java index 255cc11a4..662b5cb9a 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/CoreSync.java @@ -96,14 +96,14 @@ void configure(@Nonnull final ConflictHandler conflictHandler, boolean resumeSyncForDocument(final BsonValue documentId); /** - * Counts the number of documents in the collection that have been synchronized from the remote. + * Counts the number of documents in the collection that have been synchronized with the remote. * * @return the number of documents in the collection */ long count(); /** - * Counts the number of documents in the collection that have been synchronized from the remote + * Counts the number of documents in the collection that have been synchronized with the remote * according to the given options. * * @param filter the query filter @@ -112,7 +112,7 @@ void configure(@Nonnull final ConflictHandler conflictHandler, long count(final Bson filter); /** - * Counts the number of documents in the collection that have been synchronized from the remote + * Counts the number of documents in the collection that have been synchronized with the remote * according to the given options. * * @param filter the query filter @@ -122,14 +122,14 @@ void configure(@Nonnull final ConflictHandler conflictHandler, long count(final Bson filter, final SyncCountOptions options); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @return the find iterable interface */ CoreSyncFindIterable find(); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param resultClass the class to decode each document into * @param the target document type of the iterable. @@ -138,7 +138,7 @@ void configure(@Nonnull final ConflictHandler conflictHandler, CoreSyncFindIterable find(final Class resultClass); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param filter the query filter * @return the find iterable interface @@ -146,7 +146,7 @@ void configure(@Nonnull final ConflictHandler conflictHandler, CoreSyncFindIterable find(final Bson filter); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param filter the query filter * @param resultClass the class to decode each document into @@ -159,7 +159,7 @@ CoreSyncFindIterable find( /** - * Aggregates documents that have been synchronized from the remote + * Aggregates documents that have been synchronized with the remote * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline @@ -168,7 +168,7 @@ CoreSyncFindIterable find( CoreSyncAggregateIterable aggregate(final List pipeline); /** - * Aggregates documents that have been synchronized from the remote + * Aggregates documents that have been synchronized with the remote * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline @@ -198,7 +198,7 @@ CoreSyncAggregateIterable aggregate( SyncInsertManyResult insertManyAndSync(final List documents); /** - * Removes at most one document from the collection that has been synchronized from the remote + * Removes at most one document from the collection that has been synchronized with the remote * that matches the given filter. If no documents match, the collection is not * modified. * @@ -208,7 +208,7 @@ CoreSyncAggregateIterable aggregate( SyncDeleteResult deleteOne(final Bson filter); /** - * Removes all documents from the collection that have been synchronized from the remote + * Removes all documents from the collection that have been synchronized with the remote * that match the given query filter. If no documents match, the collection is not modified. * * @param filter the query filter to apply the the delete operation @@ -217,8 +217,9 @@ CoreSyncAggregateIterable aggregate( SyncDeleteResult deleteMany(final Bson filter); /** - * Update a single document in the collection that have been synchronized from the remote - * according to the specified arguments. + * Update a single document in the collection that have been synchronized with the remote + * according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -228,8 +229,9 @@ CoreSyncAggregateIterable aggregate( SyncUpdateResult updateOne(final Bson filter, final Bson update); /** - * Update a single document in the collection that has been synchronized from the remote - * according to the specified arguments. + * Update a single document in the collection that has been synchronized with the remote + * according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -243,8 +245,9 @@ SyncUpdateResult updateOne( final SyncUpdateOptions updateOptions); /** - * Update all documents in the collection that have been synchronized from the remote - * according to the specified arguments. + * Update all documents in the collection that have been synchronized with the remote + * according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -254,8 +257,9 @@ SyncUpdateResult updateOne( SyncUpdateResult updateMany(final Bson filter, final Bson update); /** - * Update all documents in the collection that have been synchronized from the remote - * according to the specified arguments. + * Update all documents in the collection that have been synchronized with the remote + * according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java index 314a9bc9b..3277b1b9a 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizer.java @@ -400,7 +400,7 @@ public boolean doSyncPass() { * state of said documents. Utilizes change streams to get "recent" updates to documents of * interest. Documents that are being synchronized from the first time will be fetched via a * full document lookup. Documents that have gone stale will be updated via change events or - * latest documents from the remote. Any conflicts that occur will be resolved locally and + * latest documents with the remote. Any conflicts that occur will be resolved locally and * later relayed remotely on a subsequent iteration of {@link DataSynchronizer#doSyncPass()}. */ private void syncRemoteToLocal() { diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt index df6b7f5de..210ec9587 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt @@ -10,7 +10,13 @@ import org.bson.BsonArray import org.bson.BsonBoolean import org.bson.BsonDocument import org.bson.BsonInt32 +import org.bson.BsonReader import org.bson.BsonString +import org.bson.BsonWriter +import org.bson.Document +import org.bson.codecs.Codec +import org.bson.codecs.DecoderContext +import org.bson.codecs.EncoderContext import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -26,6 +32,44 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify class CoreSyncUnitTests { + companion object { + data class CustomCodecConsideredHarmful( + val consideredHarmful: Boolean, + val author: String + ) + + class CustomCodecConsideredHarmfulCodec: Codec { + override fun getEncoderClass(): Class { + return CustomCodecConsideredHarmful::class.java + } + + override fun encode( + writer: BsonWriter?, + value: CustomCodecConsideredHarmful?, + encoderContext: EncoderContext? + ) { + if (value != null && writer != null) { + writer.writeStartDocument() + writer.writeName("consideredHarmful") + writer.writeBoolean(value.consideredHarmful) + writer.writeName("author") + writer.writeString(value.author) + writer.writeEndDocument() + } + } + + override fun decode( + reader: BsonReader?, + decoderContext: DecoderContext? + ): CustomCodecConsideredHarmful? { + return if (reader == null) + null + else { + CustomCodecConsideredHarmful(reader.readBoolean(), reader.readString()) + } + } + } + } private val harness = SyncUnitTestHarness() @After @@ -38,7 +82,7 @@ class CoreSyncUnitTests { @Test fun testSyncOne() { val ctx = harness.freshTestContext() - val (coreSync, _) = harness.createCoreSyncWithContext(ctx) + val (coreSync, _) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) // assert that calling syncOne on coreSync proxies the appropriate call // to the data synchronizer. assert that the appropriate document is being synchronized coreSync.syncOne(ctx.testDocumentId) @@ -54,7 +98,7 @@ class CoreSyncUnitTests { @Test fun testSyncMany() { val ctx = harness.freshTestContext() - val (coreSync, _) = harness.createCoreSyncWithContext(ctx) + val (coreSync, _) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) // assert that calling syncMany on coreSync proxies the appropriate call to the data // synchronizer for each document being sync'd @@ -67,7 +111,7 @@ class CoreSyncUnitTests { @Test fun testCount() { val ctx = harness.freshTestContext() - val (coreSync, _) = harness.createCoreSyncWithContext(ctx) + val (coreSync, _) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) val doc1 = BsonDocument("a", BsonString("b")) val doc2 = BsonDocument("c", BsonString("d")) @@ -85,7 +129,7 @@ class CoreSyncUnitTests { @Test fun testFind() { val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) var findIterable = coreSync.find() @@ -144,7 +188,7 @@ class CoreSyncUnitTests { @Test fun testAggregate() { val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) val doc1 = BsonDocument("a", BsonString("b")).append("c", BsonString("d")) val doc2 = BsonDocument("a", BsonString("b")).append("c", BsonString("d")) @@ -182,7 +226,7 @@ class CoreSyncUnitTests { @Test fun testUpdateOne() { val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) var result = coreSync.updateOne(ctx.testDocumentFilter, ctx.updateDocument, @@ -209,7 +253,7 @@ class CoreSyncUnitTests { @Test fun testUpdateMany() { val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) val doc1 = BsonDocument("a", BsonString("b")) val doc2 = BsonDocument("c", BsonString("d")) @@ -244,7 +288,7 @@ class CoreSyncUnitTests { @Test fun testInsertOneAndSync() { val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) assertEquals( ctx.testDocumentId, @@ -265,10 +309,31 @@ class CoreSyncUnitTests { eq(ctx.namespace), eq(ctx.testDocument)) } + @Test + fun testInsertOneAndSyncCustomCodec() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext( + ctx, CustomCodecConsideredHarmful::class.java, CustomCodecConsideredHarmfulCodec()) + + val doc1 = CustomCodecConsideredHarmful(true, "Edsger Dijkstra") + + val result = coreSync.insertOneAndSync(doc1) + + val actualDoc1 = coreSync.find(Document(mapOf("consideredHarmful" to true)), BsonDocument::class.java).first() + + assertEquals(actualDoc1!!["_id"], result.insertedId) + + verify(syncOperations, times(1)).insertOneAndSync( + eq(doc1)) + + verify(ctx.dataSynchronizer, times(1)).insertOneAndSync( + eq(ctx.namespace), eq(actualDoc1)) + } + @Test fun testInsertManyAndSync() { val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) val doc1 = BsonDocument("a", BsonString("b")) val doc2 = BsonDocument("c", BsonString("d")) @@ -293,10 +358,34 @@ class CoreSyncUnitTests { eq(ctx.namespace), eq(listOf(doc1, doc2))) } + @Test + fun testInsertManyAndSyncCustomCodec() { + val ctx = harness.freshTestContext() + val (coreSync, syncOperations) = harness.createCoreSyncWithContext( + ctx, CustomCodecConsideredHarmful::class.java, CustomCodecConsideredHarmfulCodec()) + + val doc1 = CustomCodecConsideredHarmful(true, "Edsger Dijkstra") + val doc2 = CustomCodecConsideredHarmful(false, "Eric A. Meyer") + + val result = coreSync.insertManyAndSync(listOf(doc1, doc2)) + + val actualDoc1 = coreSync.find(Document(mapOf("consideredHarmful" to true)), BsonDocument::class.java).first() + val actualDoc2 = coreSync.find(Document(mapOf("consideredHarmful" to false)), BsonDocument::class.java).first() + + assertEquals(actualDoc1!!["_id"], result.insertedIds[0]) + assertEquals(actualDoc2!!["_id"], result.insertedIds[1]) + + verify(syncOperations, times(1)).insertManyAndSync( + eq(listOf(doc1, doc2))) + + verify(ctx.dataSynchronizer, times(1)).insertManyAndSync( + eq(ctx.namespace), eq(listOf(actualDoc1, actualDoc2))) + } + @Test fun testDeleteOne() { val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) var deleteResult = coreSync.deleteOne(ctx.testDocumentFilter) @@ -318,7 +407,7 @@ class CoreSyncUnitTests { @Test fun testDeleteMany() { val ctx = harness.freshTestContext() - val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx) + val (coreSync, syncOperations) = harness.createCoreSyncWithContext(ctx, BsonDocument::class.java) val doc1 = BsonDocument("a", BsonString("b")) val doc2 = BsonDocument("c", BsonString("d")) diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt index 4488e3c28..1b7e8925a 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt @@ -10,9 +10,14 @@ import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.SyncUnitTes import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory import org.bson.BsonDocument import org.bson.BsonInt32 +import org.bson.BsonReader import org.bson.BsonString +import org.bson.BsonWriter import org.bson.Document import org.bson.codecs.BsonDocumentCodec +import org.bson.codecs.Codec +import org.bson.codecs.DecoderContext +import org.bson.codecs.EncoderContext import org.bson.codecs.configuration.CodecRegistries import org.junit.After @@ -960,7 +965,8 @@ class DataSynchronizerUnitTests { assertEquals(1, result.modifiedCount) assertNotNull(result.upsertedId) - val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1.append("_id", result.upsertedId), true) + val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, + doc1.append("_id", result.upsertedId), true) ctx.waitForEvents(amount = 1) @@ -987,7 +993,8 @@ class DataSynchronizerUnitTests { ctx.waitForEvents(amount = 2) - val expectedDocAfterUpdate1 = BsonDocument("name", BsonString("philip")).append("count", BsonInt32(2)).append("_id", doc1["_id"]) + val expectedDocAfterUpdate1 = BsonDocument("name", BsonString("philip")) + .append("count", BsonInt32(2)).append("_id", doc1["_id"]) assertEquals( expectedDocAfterUpdate1, @@ -1021,7 +1028,8 @@ class DataSynchronizerUnitTests { val result = ctx.dataSynchronizer.updateMany( ctx.namespace, BsonDocument("name", BsonString("philip")), - BsonDocument("\$set", BsonDocument("count", BsonInt32(2)))) + BsonDocument("\$set", BsonDocument("count", BsonInt32(2))), + UpdateOptions().upsert(true)) // ensure there wasn't an unnecessary insert ctx.findTestDocumentFromLocalCollection() assertEquals(2, result.modifiedCount) diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt index 760a8a501..30303e98a 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt @@ -31,6 +31,8 @@ import org.bson.BsonString import org.bson.BsonValue import org.bson.Document import org.bson.codecs.BsonDocumentCodec +import org.bson.codecs.Codec +import org.bson.codecs.DocumentCodec import org.bson.codecs.configuration.CodecRegistries import org.bson.types.ObjectId import org.junit.Assert @@ -601,15 +603,18 @@ class SyncUnitTestHarness : Closeable { return namespaceChangeStreamListener to nsConfigMock } - internal fun createCoreSyncWithContext(context: DataSynchronizerTestContext): Pair, SyncOperations> { + internal fun createCoreSyncWithContext(context: DataSynchronizerTestContext, + resultClass: Class, + codec: Codec? = null): + Pair, SyncOperations> { val syncOperations = Mockito.spy(SyncOperations( context.namespace, - BsonDocument::class.java, + resultClass, context.dataSynchronizer, - CodecRegistries.fromCodecs(BsonDocumentCodec()))) + CodecRegistries.fromCodecs(codec ?: BsonDocumentCodec(), DocumentCodec()))) val coreSync = CoreSyncImpl( context.namespace, - BsonDocument::class.java, + resultClass, context.dataSynchronizer, (context as DataSynchronizerTestContextImpl).service, syncOperations) diff --git a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt index 89f85c94f..124bd0ca2 100644 --- a/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt +++ b/core/testutils/src/main/java/com/mongodb/stitch/core/testutils/sync/ProxySyncMethods.kt @@ -18,144 +18,45 @@ import org.bson.conversions.Bson * [com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSync]. */ interface ProxySyncMethods { - /** - * Set the conflict handler and and change event listener on this collection. - * @param conflictResolver the conflict resolver to invoke when a conflict happens between local - * and remote events. - * @param changeEventListener the event listener to invoke when a change event happens for the - * document. - * @param errorListener the error listener to invoke when an irrecoverable error occurs - */ fun configure( conflictResolver: ConflictHandler, changeEventListener: ChangeEventListener?, errorListener: ErrorListener? ) - /** - * Requests that the given document _id be synchronized. - * @param id the document _id to synchronize. - */ fun syncOne(id: BsonValue) - /** - * Stops synchronizing the given document _id. Any uncommitted writes will be lost. - * - * @param id the _id of the document to desynchronize. - */ fun desyncOne(id: BsonValue) - /** - * Returns the set of synchronized document ids in a namespace. - * - * @return the set of synchronized document ids in a namespace. - */ fun getSyncedIds(): Set - /** - * Finds all documents in the collection. - * - * @param filter the query filter - * @return the find iterable interface - */ fun find(filter: Bson = BsonDocument()): Iterable - /** - * Aggregates documents that have been synchronized from the remote - * according to the specified aggregation pipeline. - * - * @param pipeline the aggregation pipeline - * @return an iterable containing the result of the aggregation operation - */ fun aggregate(pipeline: List): Iterable - /** - * Counts the number of documents in the collection that have been synchronized from the remote - * according to the given options. - * - * @param filter the query filter - * @return the number of documents in the collection - */ fun count(filter: Bson = BsonDocument()): Long - /** - * Updates a document by the given id. It is first searched for in the local synchronized cache - * and if not found and there is internet connectivity, it is searched for remotely. - * - * @param filter the query filter - * @param update the update specifier. - * @return the result of the local or remote update. - */ fun updateOne( filter: Bson, update: Bson, updateOptions: SyncUpdateOptions = SyncUpdateOptions() ): SyncUpdateResult - /** - * Update all documents that have been synchronized from the remote - * in the collection according to the specified arguments. - * - * @param filter a document describing the query filter, which may not be null. - * @param update a document describing the update, which may not be null. The update to - * apply must include only update operators. - * @param updateOptions the options to apply to the update operation - * @return the result of the update many operation - */ fun updateMany( filter: Bson, update: Bson, updateOptions: SyncUpdateOptions = SyncUpdateOptions() ): SyncUpdateResult - /** - * Inserts a single document and begins to synchronize it. - * - * @param document the document to insert and synchronize. - * @return the result of the insertion. - */ fun insertOneAndSync(document: Document): SyncInsertOneResult - /** - * Inserts one or more documents. Begin synchronizing on the documents' ids. - * - * @param documents the documents to insert - * @return the result of the insert many operation - */ fun insertManyAndSync(documents: List): SyncInsertManyResult - /** - * Deletes a single document by the given id. It is first searched for in the local synchronized - * cache and if not found and there is internet connectivity, it is searched for remotely. - * - * @param filter the query filter - * @return the result of the local or remote update. - */ fun deleteOne(filter: Bson): SyncDeleteResult - /** - * Removes all documents from the collection that have been synchronized from the remote - * that match the given query filter. If no documents match, the collection is not modified. - * - * @param filter the query filter to apply the the delete operation - * @return the result of the remove many operation - */ fun deleteMany(filter: Bson): SyncDeleteResult - /** - * Return the set of synchronized document _ids in a namespace - * that have been paused due to an irrecoverable error. - * - * @return the set of paused document _ids in a namespace - */ fun getPausedDocumentIds(): Set - /** - * A document that is paused no longer has remote updates applied to it. - * Any local updates to this document cause it to be resumed. An example of pausing a document - * is when a conflict is being resolved for that document and the handler throws an exception. - * - * @param documentId the id of the document to resume syncing - */ fun resumeSyncForDocument(documentId: BsonValue): Boolean } diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java index a762ffa95..e9b30c990 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/Sync.java @@ -107,14 +107,14 @@ void configure(@Nonnull final ConflictHandler conflictResolver, boolean resumeSyncForDocument(@Nonnull final BsonValue documentId); /** - * Counts the number of documents in the collection that have been synchronized from the remote. + * Counts the number of documents in the collection that have been synchronized with the remote. * * @return the number of documents in the collection */ long count(); /** - * Counts the number of documents in the collection that have been synchronized from the remote + * Counts the number of documents in the collection that have been synchronized with the remote * according to the given options. * * @param filter the query filter @@ -123,7 +123,7 @@ void configure(@Nonnull final ConflictHandler conflictResolver, long count(final Bson filter); /** - * Counts the number of documents in the collection that have been synchronized from the remote + * Counts the number of documents in the collection that have been synchronized with the remote * according to the given options. * * @param filter the query filter @@ -133,14 +133,14 @@ void configure(@Nonnull final ConflictHandler conflictResolver, long count(final Bson filter, final SyncCountOptions options); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @return the find iterable interface */ SyncFindIterable find(); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param resultClass the class to decode each document into * @param the target document type of the iterable. @@ -149,7 +149,7 @@ void configure(@Nonnull final ConflictHandler conflictResolver, SyncFindIterable find(final Class resultClass); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param filter the query filter * @return the find iterable interface @@ -157,7 +157,7 @@ void configure(@Nonnull final ConflictHandler conflictResolver, SyncFindIterable find(final Bson filter); /** - * Finds all documents in the collection that have been synchronized from the remote. + * Finds all documents in the collection that have been synchronized with the remote. * * @param filter the query filter * @param resultClass the class to decode each document into @@ -170,7 +170,7 @@ SyncFindIterable find( /** - * Aggregates documents that have been synchronized from the remote + * Aggregates documents that have been synchronized with the remote * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline @@ -179,7 +179,7 @@ SyncFindIterable find( SyncAggregateIterable aggregate(final List pipeline); /** - * Aggregates documents that have been synchronized from the remote + * Aggregates documents that have been synchronized with the remote * according to the specified aggregation pipeline. * * @param pipeline the aggregation pipeline @@ -192,8 +192,8 @@ SyncAggregateIterable aggregate( final Class resultClass); /** - * Inserts the provided document. If the document is missing an identifier, the client should - * generate one. Begin syncing the document against the remote. + * Inserts the provided document. If the document is missing an identifier, one will be + * generated. Begin syncing the document against the remote. * * @param document the document to insert * @return the result of the insert one operation @@ -201,7 +201,8 @@ SyncAggregateIterable aggregate( SyncInsertOneResult insertOneAndSync(final DocumentT document); /** - * Inserts one or more documents. Begin syncing the documents against the remote. + * Inserts one or more documents. If the documents are missing an identifier, they will be + * generated. Begin syncing the documents against the remote. * * @param documents the documents to insert * @return the result of the insert many operation @@ -209,7 +210,7 @@ SyncAggregateIterable aggregate( SyncInsertManyResult insertManyAndSync(final List documents); /** - * Removes at most one document from the collection that has been synchronized from the remote + * Removes at most one document from the collection that has been synchronized with the remote * and matches the given filter. If no documents match, the collection is not * modified. * @@ -219,7 +220,7 @@ SyncAggregateIterable aggregate( SyncDeleteResult deleteOne(final Bson filter); /** - * Removes all documents from the collection that have been synchronized from the remote + * Removes all documents from the collection that have been synchronized with the remote * and match the given query filter. If no documents * match, the collection is not modified. * @@ -229,8 +230,9 @@ SyncAggregateIterable aggregate( SyncDeleteResult deleteMany(final Bson filter); /** - * Update a single document in the collection that has been synchronized from the remote - * according to the specified arguments. + * Update a single document in the collection that has been synchronized with the remote + * according to the specified arguments. If the update results in an upsert, the newly upserted + * document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -240,8 +242,9 @@ SyncAggregateIterable aggregate( SyncUpdateResult updateOne(final Bson filter, final Bson update); /** - * Update a single document that has been synchronized from the remote - * in the collection according to the specified arguments. + * Update a single document that has been synchronized with the remote + * in the collection according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -255,8 +258,9 @@ SyncUpdateResult updateOne( final SyncUpdateOptions updateOptions); /** - * Update all documents in the collection that have been synchronized from the remote - * according to the specified arguments. + * Update all documents in the collection that have been synchronized with the remote + * according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to @@ -266,8 +270,9 @@ SyncUpdateResult updateOne( SyncUpdateResult updateMany(final Bson filter, final Bson update); /** - * Update all documents in the collection that have been synchronized from the remote - * according to the specified arguments. + * Update all documents in the collection that have been synchronized with the remote + * according to the specified arguments. If the update results in an upsert, + * the newly upserted document will automatically become synchronized. * * @param filter a document describing the query filter, which may not be null. * @param update a document describing the update, which may not be null. The update to From c62568c123387d96a9772241de96215e03b84f4a Mon Sep 17 00:00:00 2001 From: Jason Flax Date: Thu, 1 Nov 2018 12:06:14 +0000 Subject: [PATCH 14/14] Appease linter --- .../mongodb/remote/sync/internal/CoreSyncUnitTests.kt | 2 +- .../remote/sync/internal/DataSynchronizerUnitTests.kt | 5 ----- .../mongodb/remote/sync/internal/SyncUnitTestHarness.kt | 8 +++++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt index 210ec9587..4605dba4f 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreSyncUnitTests.kt @@ -38,7 +38,7 @@ class CoreSyncUnitTests { val author: String ) - class CustomCodecConsideredHarmfulCodec: Codec { + class CustomCodecConsideredHarmfulCodec : Codec { override fun getEncoderClass(): Class { return CustomCodecConsideredHarmful::class.java } diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt index 1b7e8925a..5ddcf0bbf 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/DataSynchronizerUnitTests.kt @@ -10,14 +10,9 @@ import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.SyncUnitTes import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory import org.bson.BsonDocument import org.bson.BsonInt32 -import org.bson.BsonReader import org.bson.BsonString -import org.bson.BsonWriter import org.bson.Document import org.bson.codecs.BsonDocumentCodec -import org.bson.codecs.Codec -import org.bson.codecs.DecoderContext -import org.bson.codecs.EncoderContext import org.bson.codecs.configuration.CodecRegistries import org.junit.After diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt index 30303e98a..fc3458839 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/SyncUnitTestHarness.kt @@ -603,9 +603,11 @@ class SyncUnitTestHarness : Closeable { return namespaceChangeStreamListener to nsConfigMock } - internal fun createCoreSyncWithContext(context: DataSynchronizerTestContext, - resultClass: Class, - codec: Codec? = null): + internal fun createCoreSyncWithContext( + context: DataSynchronizerTestContext, + resultClass: Class, + codec: Codec? = null + ): Pair, SyncOperations> { val syncOperations = Mockito.spy(SyncOperations( context.namespace,