From 9d6307dd8c0bdf642c3300db1b0529a46a35cc86 Mon Sep 17 00:00:00 2001 From: Douglas Kaminsky Date: Tue, 26 Feb 2019 14:45:03 -0500 Subject: [PATCH] STITCH-1986 - Add watch to remote mongo collection (#98) --- .evg.yml | 3 +- .../examples/todosync/TodoListActivity.java | 5 +- .../remote/RemoteMongoClientIntTests.kt | 95 ++- .../internal/SyncMongoClientIntTests.kt | 6 +- .../mongodb/remote/AsyncChangeStream.java | 80 +++ .../mongodb/remote/RemoteMongoCollection.java | 22 + .../android/services/mongodb/remote/Sync.java | 6 +- .../internal/RemoteMongoCollectionImpl.java | 31 + .../mongodb/remote/internal/SyncImpl.java | 6 +- .../services/mongodb/remote/ChangeEvent.java | 258 ++++++++ .../services/mongodb/remote/ChangeStream.java | 97 +++ .../mongodb/remote/ExceptionListener.java | 33 + .../mongodb/remote/OperationType.java | 66 ++ .../mongodb/remote/UpdateDescription.java | 217 +++++++ .../internal/CoreRemoteMongoCollection.java | 23 + .../CoreRemoteMongoCollectionImpl.java | 40 ++ .../mongodb/remote/internal/Operations.java | 10 + .../remote/internal/ResultDecoders.java | 68 ++- .../remote/internal/WatchOperation.java | 59 ++ .../remote/sync/ChangeEventListener.java | 2 +- .../mongodb/remote/sync/ConflictHandler.java | 2 +- .../mongodb/remote/sync/CoreSync.java | 6 +- .../sync/DefaultSyncConflictResolvers.java | 2 +- .../mongodb/remote/sync/ErrorListener.java | 16 +- .../remote/sync/internal/ChangeEvent.java | 565 ------------------ .../remote/sync/internal/ChangeEvents.java | 154 +++++ .../CoreDocumentSynchronizationConfig.java | 51 +- .../remote/sync/internal/CoreSyncImpl.java | 6 +- .../sync/internal/DataSynchronizer.java | 112 ++-- .../InstanceChangeStreamListener.java | 1 + .../InstanceChangeStreamListenerImpl.java | 1 + .../InstanceSynchronizationConfig.java | 17 +- .../NamespaceChangeStreamListener.java | 14 +- .../NamespaceSynchronizationConfig.java | 34 +- .../CoreRemoteMongoCollectionUnitTests.java | 95 +++ .../sync/internal/ChangeEventUnitTests.kt | 76 +-- ...eDocumentSynchronizationConfigUnitTests.kt | 2 +- .../internal/DataSynchronizerTestContext.kt | 6 + .../internal/DataSynchronizerUnitTests.kt | 143 +++-- .../NamespaceChangeStreamListenerUnitTests.kt | 4 +- ...NamespaceSynchronizationConfigUnitTests.kt | 1 + .../sync/internal/SyncUnitTestHarness.kt | 46 +- .../core/testutils/sync/ProxySyncMethods.kt | 4 +- .../core/testutils/sync/SyncIntTestProxy.kt | 19 +- .../remote/PassthroughChangeStream.java | 56 ++ .../mongodb/remote/RemoteMongoCollection.java | 28 + .../internal/RemoteMongoCollectionImpl.java | 29 + .../remote/RemoteMongoClientIntTests.kt | 118 +++- 48 files changed, 1885 insertions(+), 850 deletions(-) create mode 100644 android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/AsyncChangeStream.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ChangeEvent.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ChangeStream.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ExceptionListener.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/OperationType.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/UpdateDescription.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/WatchOperation.java delete mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java create mode 100644 core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvents.java create mode 100644 server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/PassthroughChangeStream.java diff --git a/.evg.yml b/.evg.yml index 31234627b..4e5e0b3c2 100644 --- a/.evg.yml +++ b/.evg.yml @@ -327,7 +327,7 @@ tasks: cd stitch-java-sdk echo "running android tests" echo "test.stitch.baseURL=http://10.0.2.2:9090" >> local.properties - ./gradlew connectedAndroidTest --info --continue --warning-mode=all --stacktrace < /dev/null + ./gradlew connectedAndroidTest jacocoTestReport --info --continue --warning-mode=all --stacktrace < /dev/null - func: "publish_coveralls" - name: run_android_tests_with_proguard @@ -371,7 +371,6 @@ tasks: echo "running android tests" echo "test.stitch.baseURL=http://10.0.2.2:9090" >> local.properties ./gradlew connectedAndroidTest -PwithProguardMinification --info --continue --warning-mode=all --stacktrace < /dev/null - - func: "publish_coveralls" - name: finalize_coverage depends_on: 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 9c37a22b9..bed2b0c80 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,10 +39,11 @@ 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.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.OperationType; 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; import java.util.Collections; @@ -236,7 +237,7 @@ public void onEvent(final BsonValue documentId, final ChangeEvent even private class ItemUpdateListener implements ChangeEventListener { @Override public void onEvent(final BsonValue documentId, final ChangeEvent event) { - if (event.getOperationType() == ChangeEvent.OperationType.DELETE) { + if (event.getOperationType() == OperationType.DELETE) { todoAdapter.removeItemById(event.getDocumentKey().getObjectId("_id").getValue()); return; } diff --git a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/RemoteMongoClientIntTests.kt b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/RemoteMongoClientIntTests.kt index 0f2eac4cf..5ff1cbfed 100644 --- a/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/RemoteMongoClientIntTests.kt +++ b/android/services/mongodb-remote/src/androidTest/java/com/mongodb/stitch/android/services/mongodb/remote/RemoteMongoClientIntTests.kt @@ -13,9 +13,13 @@ import com.mongodb.stitch.core.admin.services.ServiceConfigs import com.mongodb.stitch.core.admin.services.rules.RuleCreator import com.mongodb.stitch.core.auth.providers.anonymous.AnonymousCredential import com.mongodb.stitch.core.internal.common.BsonUtils +import com.mongodb.stitch.core.services.mongodb.remote.OperationType import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions import com.mongodb.stitch.core.testutils.CustomType +import org.bson.BsonDocument +import org.bson.BsonInt32 +import org.bson.BsonString import org.bson.Document import org.bson.codecs.configuration.CodecConfigurationException import org.bson.codecs.configuration.CodecRegistries @@ -69,7 +73,7 @@ class RemoteMongoClientIntTests : BaseStitchAndroidIntTest() { roles = listOf(RuleCreator.MongoDb.Role( read = true, write = true )), - schema = RuleCreator.MongoDb.Schema()) + schema = RuleCreator.MongoDb.Schema().copy(properties = Document())) addRule(svc.second, rule) @@ -431,6 +435,95 @@ class RemoteMongoClientIntTests : BaseStitchAndroidIntTest() { assertEquals(expected, Tasks.await(Tasks.await(iter.iterator()).next())) } + @Test + fun testWatchBsonValueIDs() { + val coll = getTestColl() + assertEquals(0, Tasks.await(coll.count())) + + val rawDoc1 = Document() + rawDoc1["_id"] = 1 + rawDoc1["hello"] = "world" + + val rawDoc2 = Document() + rawDoc2["_id"] = "foo" + rawDoc2["happy"] = "day" + + Tasks.await(coll.insertOne(rawDoc1)) + assertEquals(1, Tasks.await(coll.count())) + + val streamTask = coll.watch(BsonInt32(1), BsonString("foo")) + val stream = Tasks.await(streamTask) + + try { + Tasks.await(coll.insertOne(rawDoc2)) + assertEquals(2, Tasks.await(coll.count())) + Tasks.await(coll.updateMany(BsonDocument(), Document().append("\$set", + Document().append("new", "field")))) + + val insertEvent = Tasks.await(stream.nextEvent()) + assertEquals(OperationType.INSERT, insertEvent.operationType) + assertEquals(rawDoc2, insertEvent.fullDocument) + val updateEvent1 = Tasks.await(stream.nextEvent()) + val updateEvent2 = Tasks.await(stream.nextEvent()) + + assertNotNull(updateEvent1) + assertNotNull(updateEvent2) + + assertEquals(OperationType.UPDATE, updateEvent1.operationType) + assertEquals(rawDoc1.append("new", "field"), updateEvent1.fullDocument) + assertEquals(OperationType.UPDATE, updateEvent2.operationType) + assertEquals(rawDoc2.append("new", "field"), updateEvent2.fullDocument) + } finally { + stream.close() + } + } + + @Test + fun testWatchObjectIdIDs() { + val coll = getTestColl() + assertEquals(0, Tasks.await(coll.count())) + + val objectId1 = ObjectId() + val objectId2 = ObjectId() + + val rawDoc1 = Document() + rawDoc1["_id"] = objectId1 + rawDoc1["hello"] = "world" + + val rawDoc2 = Document() + rawDoc2["_id"] = objectId2 + rawDoc2["happy"] = "day" + + Tasks.await(coll.insertOne(rawDoc1)) + assertEquals(1, Tasks.await(coll.count())) + + val streamTask = coll.watch(objectId1, objectId2) + val stream = Tasks.await(streamTask) + + try { + Tasks.await(coll.insertOne(rawDoc2)) + assertEquals(2, Tasks.await(coll.count())) + Tasks.await(coll.updateMany(BsonDocument(), Document().append("\$set", + Document().append("new", "field")))) + + val insertEvent = Tasks.await(stream.nextEvent()) + assertEquals(OperationType.INSERT, insertEvent.operationType) + assertEquals(rawDoc2, insertEvent.fullDocument) + val updateEvent1 = Tasks.await(stream.nextEvent()) + val updateEvent2 = Tasks.await(stream.nextEvent()) + + assertNotNull(updateEvent1) + assertNotNull(updateEvent2) + + assertEquals(OperationType.UPDATE, updateEvent1.operationType) + assertEquals(rawDoc1.append("new", "field"), updateEvent1.fullDocument) + assertEquals(OperationType.UPDATE, updateEvent2.operationType) + assertEquals(rawDoc2.append("new", "field"), updateEvent2.fullDocument) + } finally { + stream.close() + } + } + private fun withoutIds(documents: Collection): Collection { val list = ArrayList(documents.size) documents.forEach { list.add(withoutId(it)) } 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 d9e43a4d8..eb21a15d6 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 @@ -13,13 +13,13 @@ import com.mongodb.stitch.core.admin.services.ServiceConfigs import com.mongodb.stitch.core.admin.services.rules.RuleCreator import com.mongodb.stitch.core.admin.services.rules.RuleResponse import com.mongodb.stitch.core.auth.providers.anonymous.AnonymousCredential +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult 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 @@ -71,9 +71,9 @@ class SyncMongoClientIntTests : BaseStitchAndroidIntTest(), SyncIntTestRunner { override fun configure( conflictResolver: ConflictHandler, changeEventListener: ChangeEventListener?, - errorListener: ErrorListener? + exceptionListener: ExceptionListener? ) { - sync.configure(conflictResolver, changeEventListener, errorListener) + sync.configure(conflictResolver, changeEventListener, exceptionListener) } override fun syncOne(id: BsonValue) { diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/AsyncChangeStream.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/AsyncChangeStream.java new file mode 100644 index 000000000..519e3bef2 --- /dev/null +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/AsyncChangeStream.java @@ -0,0 +1,80 @@ +/* + * 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; + +import com.google.android.gms.tasks.Task; + +import com.mongodb.stitch.android.core.internal.common.TaskDispatcher; +import com.mongodb.stitch.core.internal.net.StitchEvent; +import com.mongodb.stitch.core.internal.net.Stream; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeStream; + +import java.io.IOException; +import java.util.concurrent.Callable; + +/** + * An implementation of {@link com.mongodb.stitch.core.services.mongodb.remote.ChangeStream} that + * returns each event as a {@link Task}. + * + * @param The type of the full document on the underlying change event to be returned + * asynchronously. + */ +public class AsyncChangeStream extends + ChangeStream>, DocumentT> { + private final TaskDispatcher dispatcher; + + /** + * Initializes a passthrough change stream with the provided underlying event stream. + * + * @param stream The event stream. + * @param dispatcher The event dispatcher. + */ + public AsyncChangeStream(final Stream> stream, + final TaskDispatcher dispatcher) { + super(stream); + this.dispatcher = dispatcher; + } + + /** + * Returns a {@link Task} whose resolution gives the next event from the underlying stream. + * @return task providing the next event + * @throws IOException if the underlying stream throws an {@link IOException} + */ + @Override + public Task> nextEvent() throws IOException { + return dispatcher.dispatchTask(new Callable>() { + @Override + public ChangeEvent call() throws Exception { + final StitchEvent> nextEvent = getStream().nextEvent(); + + if (nextEvent == null) { + return null; + } + if (nextEvent.getError() != null) { + dispatchError(nextEvent); + return null; + } + if (nextEvent.getData() == null) { + return null; + } + + return nextEvent.getData(); + } + }); + } +} diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/RemoteMongoCollection.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/RemoteMongoCollection.java index 0c273762d..3a1ea05f1 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/RemoteMongoCollection.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/RemoteMongoCollection.java @@ -18,15 +18,21 @@ import com.google.android.gms.tasks.Task; import com.mongodb.MongoNamespace; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeStream; import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; + import java.util.List; + +import org.bson.BsonValue; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; +import org.bson.types.ObjectId; /** * The RemoteMongoCollection interface. @@ -241,6 +247,22 @@ Task updateMany( final Bson update, final RemoteUpdateOptions updateOptions); + + /** + * Watches specified IDs in a collection. This convenience overload supports the use case + * of non-{@link BsonValue} instances of {@link ObjectId}. + * @param ids unique object identifiers of the IDs to watch. + * @return the stream of change events. + */ + Task>, DocumentT>> watch(final ObjectId... ids); + + /** + * Watches specified IDs in a collection. + * @param ids the ids to watch. + * @return the stream of change events. + */ + Task>, DocumentT>> watch(final BsonValue... ids); + /** * A set of synchronization related operations on this collection. * 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 ac78e0773..c36049726 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,9 +20,9 @@ import android.support.annotation.Nullable; import com.google.android.gms.tasks.Task; +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener; 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.SyncCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.SyncInsertManyResult; @@ -48,11 +48,11 @@ public interface Sync { * 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 + * @param exceptionListener the error listener to invoke when an irrecoverable error occurs */ void configure(@NonNull final ConflictHandler conflictHandler, @Nullable final ChangeEventListener changeEventListener, - @Nullable final ErrorListener errorListener); + @Nullable final ExceptionListener exceptionListener); /** * Requests that the given document _id be synchronized. diff --git a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/RemoteMongoCollectionImpl.java b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/RemoteMongoCollectionImpl.java index a2147f880..dd5fcf595 100644 --- a/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/RemoteMongoCollectionImpl.java +++ b/android/services/mongodb-remote/src/main/java/com/mongodb/stitch/android/services/mongodb/remote/internal/RemoteMongoCollectionImpl.java @@ -19,10 +19,13 @@ import com.google.android.gms.tasks.Task; import com.mongodb.MongoNamespace; import com.mongodb.stitch.android.core.internal.common.TaskDispatcher; +import com.mongodb.stitch.android.services.mongodb.remote.AsyncChangeStream; import com.mongodb.stitch.android.services.mongodb.remote.RemoteAggregateIterable; import com.mongodb.stitch.android.services.mongodb.remote.RemoteFindIterable; import com.mongodb.stitch.android.services.mongodb.remote.RemoteMongoCollection; import com.mongodb.stitch.android.services.mongodb.remote.Sync; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeStream; import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; @@ -30,10 +33,14 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollection; + import java.util.List; import java.util.concurrent.Callable; + +import org.bson.BsonValue; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; +import org.bson.types.ObjectId; public final class RemoteMongoCollectionImpl implements RemoteMongoCollection { @@ -344,6 +351,30 @@ public RemoteUpdateResult call() { }); } + @Override + public Task>, DocumentT>> watch(final ObjectId... ids) { + return dispatcher.dispatchTask( + new Callable>, DocumentT>>() { + @Override + public ChangeStream>, DocumentT> call() throws Exception { + return new AsyncChangeStream(proxy.watch(ids), dispatcher); + } + } + ); + } + + @Override + public Task>, DocumentT>> watch(final BsonValue... ids) { + return dispatcher.dispatchTask( + new Callable>, DocumentT>>() { + @Override + public ChangeStream>, DocumentT> call() throws Exception { + return new AsyncChangeStream(proxy.watch(ids), dispatcher); + } + } + ); + } + @Override public Sync sync() { return this.sync; 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 8e4812888..0e362de61 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 @@ -24,10 +24,10 @@ 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.ExceptionListener; 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; @@ -56,8 +56,8 @@ public class SyncImpl implements Sync { @Override public void configure(@NonNull final ConflictHandler conflictHandler, @Nullable final ChangeEventListener changeEventListener, - @Nullable final ErrorListener errorListener) { - this.proxy.configure(conflictHandler, changeEventListener, errorListener); + @Nullable final ExceptionListener exceptionListener) { + this.proxy.configure(conflictHandler, changeEventListener, exceptionListener); } @Override diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ChangeEvent.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ChangeEvent.java new file mode 100644 index 000000000..261487095 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ChangeEvent.java @@ -0,0 +1,258 @@ +/* + * 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; + +import static com.mongodb.stitch.core.internal.common.Assertions.keyPresent; + +import com.mongodb.MongoNamespace; + +import java.util.ArrayList; +import java.util.Collection; + +import org.bson.BsonArray; +import org.bson.BsonBoolean; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.BsonValue; + +/** + * Represents a change event communicated via a MongoDB change stream. + * + * @param The underlying type of document for which this change event was produced. + */ +// TODO: Should there be a local and remote type for the pending part? +public final class ChangeEvent { + private final BsonDocument id; // Metadata related to the operation (the resumeToken). + private final OperationType operationType; + private final DocumentT fullDocument; + private final MongoNamespace ns; + private final BsonDocument documentKey; + private final UpdateDescription updateDescription; + private final boolean hasUncommittedWrites; + + /** + * Constructs a change event. + * + * @param id The id of the change event. + * @param operationType The operation type represented by the change event. + * @param fullDocument The full document at some point after the change is applied. + * @param ns The namespace (database and collection) of the document. + * @param documentKey The id if the underlying document that changed. + * @param updateDescription The description of what has changed (for updates only). + * @param hasUncommittedWrites Whether this represents a local uncommitted write. + */ + public ChangeEvent( + final BsonDocument id, + final OperationType operationType, + final DocumentT fullDocument, + final MongoNamespace ns, + final BsonDocument documentKey, + final UpdateDescription updateDescription, + final boolean hasUncommittedWrites + ) { + this.id = id; + this.operationType = operationType; + this.fullDocument = fullDocument; + this.ns = ns; + this.documentKey = documentKey; + this.updateDescription = updateDescription == null + ? new UpdateDescription(null, null) : updateDescription; + this.hasUncommittedWrites = hasUncommittedWrites; + } + + /** + * Returns the ID of the change event itself. + * + * @return the id of this change event. + */ + public BsonDocument getId() { + return id; + } + + /** + * Returns the operation type of the change that triggered the change event. + * + * @return the operation type of this change event. + */ + public OperationType getOperationType() { + return operationType; + } + + /** + * The full document at some point after the change has been applied. + * + * @return the full document. + */ + public DocumentT getFullDocument() { + return fullDocument; + } + + /** + * The namespace the change relates to. + * + * @return the namespace. + */ + public MongoNamespace getNamespace() { + return ns; + } + + /** + * The unique identifier for the document that was actually changed. + * + * @return the document key. + */ + public BsonDocument getDocumentKey() { + return documentKey; + } + + /** + * In the case of an update, the description of which fields have been added, removed or updated. + * + * @return the update description. + */ + public UpdateDescription getUpdateDescription() { + return updateDescription; + } + + /** + * Indicates a local change event that has not yet been synchronized with a remote data store. + * Used only for the sync use case. + * + * @return whether or not this change event represents uncommitted writes. + */ + public boolean hasUncommittedWrites() { + return hasUncommittedWrites; + } + + /** + * Serializes this change event into a {@link BsonDocument}. + * @return the serialized document. + */ + public BsonDocument toBsonDocument() { + final BsonDocument asDoc = new BsonDocument(); + asDoc.put(Fields.ID_FIELD, id); + asDoc.put(Fields.OPERATION_TYPE_FIELD, + new BsonString(operationType.toRemote())); + final BsonDocument nsDoc = new BsonDocument(); + nsDoc.put(Fields.NS_DB_FIELD, + new BsonString(ns.getDatabaseName())); + nsDoc.put(Fields.NS_COLL_FIELD, + new BsonString(getNamespace().getCollectionName())); + asDoc.put(Fields.NS_FIELD, nsDoc); + asDoc.put(Fields.DOCUMENT_KEY_FIELD, documentKey); + if (fullDocument != null && (fullDocument instanceof BsonValue) + && ((BsonValue)fullDocument).isDocument()) { + asDoc.put(Fields.FULL_DOCUMENT_FIELD, (BsonValue)fullDocument); + } + if (updateDescription != null) { + final BsonDocument updateDescDoc = new BsonDocument(); + updateDescDoc.put( + Fields.UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD, + updateDescription.getUpdatedFields()); + + final BsonArray removedFields = new BsonArray(); + for (final String field : updateDescription.getRemovedFields()) { + removedFields.add(new BsonString(field)); + } + updateDescDoc.put( + Fields.UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD, + removedFields); + asDoc.put(Fields.UPDATE_DESCRIPTION_FIELD, updateDescDoc); + } + asDoc.put(Fields.WRITE_PENDING_FIELD, + new BsonBoolean(hasUncommittedWrites)); + return asDoc; + } + + /** + * Deserializes a {@link BsonDocument} into an instance of change event. + * @param document the serialized document + * @return the deserialized change event + */ + public static ChangeEvent fromBsonDocument(final BsonDocument document) { + keyPresent(Fields.ID_FIELD, document); + keyPresent(Fields.OPERATION_TYPE_FIELD, document); + keyPresent(Fields.NS_FIELD, document); + keyPresent(Fields.DOCUMENT_KEY_FIELD, document); + + final BsonDocument nsDoc = document.getDocument(Fields.NS_FIELD); + final UpdateDescription updateDescription; + if (document.containsKey(Fields.UPDATE_DESCRIPTION_FIELD)) { + final BsonDocument updateDescDoc = + document.getDocument(Fields.UPDATE_DESCRIPTION_FIELD); + keyPresent(Fields.UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD, updateDescDoc); + keyPresent(Fields.UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD, updateDescDoc); + + final BsonArray removedFieldsArr = + updateDescDoc.getArray(Fields.UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD); + final Collection removedFields = new ArrayList<>(removedFieldsArr.size()); + for (final BsonValue field : removedFieldsArr) { + removedFields.add(field.asString().getValue()); + } + updateDescription = new UpdateDescription(updateDescDoc.getDocument( + Fields.UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD), + removedFields); + } else { + updateDescription = null; + } + + final BsonDocument fullDocument; + if (document.containsKey(Fields.FULL_DOCUMENT_FIELD)) { + final BsonValue fdVal = document.get(Fields.FULL_DOCUMENT_FIELD); + if (fdVal.isDocument()) { + fullDocument = fdVal.asDocument(); + } else { + fullDocument = null; + } + } else { + fullDocument = null; + } + + return new ChangeEvent<>( + document.getDocument(Fields.ID_FIELD), + OperationType.fromRemote( + document.getString(Fields.OPERATION_TYPE_FIELD).getValue()), + fullDocument, + new MongoNamespace( + nsDoc.getString(Fields.NS_DB_FIELD).getValue(), + nsDoc.getString(Fields.NS_COLL_FIELD).getValue()), + document.getDocument(Fields.DOCUMENT_KEY_FIELD), + updateDescription, + document.getBoolean( + Fields.WRITE_PENDING_FIELD, + BsonBoolean.FALSE).getValue()); + } + + // remove me + private static final class Fields { + static final String ID_FIELD = "_id"; + static final String OPERATION_TYPE_FIELD = "operationType"; + static final String FULL_DOCUMENT_FIELD = "fullDocument"; + static final String DOCUMENT_KEY_FIELD = "documentKey"; + + static final String NS_FIELD = "ns"; + static final String NS_DB_FIELD = "db"; + static final String NS_COLL_FIELD = "coll"; + + static final String UPDATE_DESCRIPTION_FIELD = "updateDescription"; + static final String UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD = "updatedFields"; + static final String UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD = "removedFields"; + + static final String WRITE_PENDING_FIELD = "writePending"; + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ChangeStream.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ChangeStream.java new file mode 100644 index 000000000..8c75ea9f6 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ChangeStream.java @@ -0,0 +1,97 @@ +/* + * 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; + +import com.mongodb.stitch.core.internal.net.StitchEvent; +import com.mongodb.stitch.core.internal.net.Stream; + +import java.io.Closeable; +import java.io.IOException; + +import org.bson.BsonValue; + +/** + * User-level abstraction for a stream returning {@link ChangeEvent}s from the server. + * + * @param The type returned to users of this API when the next event is requested. + * @param The type of the full document on the underlying change event. + */ +public abstract class ChangeStream implements Closeable { + private final Stream> stream; + + private ExceptionListener exceptionListener = null; + + protected ChangeStream(final Stream> stream) { + if (stream == null) { + throw new IllegalArgumentException("null stream passed to change stream"); + } + this.stream = stream; + } + + /** + * Optionally adds a listener that is notified when an attempt to retrieve the next event + * fails. + * + * @param exceptionListener The exception listener. + */ + public void setExceptionListener(final ExceptionListener exceptionListener) { + this.exceptionListener = exceptionListener; + } + + /** + * Returns the next event available from the stream. + * + * @return The next event. + * @throws IOException If the underlying stream throws an {@link IOException} + */ + public abstract EventT nextEvent() throws IOException; + + /** + * Indicates whether or not the change stream is currently open. + * @return True if the underlying change stream is open. + */ + public boolean isOpen() { + return stream.isOpen(); + } + + /** + * Closes the underlying stream. + * @throws IOException If the underlying stream throws an {@link IOException} when it is closed. + */ + @Override + public void close() throws IOException { + stream.close(); + } + + protected Stream> getStream() { + return this.stream; + } + + protected ExceptionListener getExceptionListener() { + return this.exceptionListener; + } + + protected void dispatchError(final StitchEvent> event) { + if (exceptionListener != null) { + BsonValue documentId = null; + if (event.getData() != null) { + documentId = event.getData().getDocumentKey(); + } + exceptionListener.onError(documentId, event.getError()); + } + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ExceptionListener.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ExceptionListener.java new file mode 100644 index 000000000..ae4aac2c4 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/ExceptionListener.java @@ -0,0 +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; + +import org.bson.BsonValue; + +/** + * ExceptionListener receives non-network related errors that occur. + */ +public interface ExceptionListener { + + /** + * Called when an error happens for the given document id. + * + * @param documentId the _id of the document related to the error. + * @param error the error. + */ + void onError(final BsonValue documentId, final Exception error); +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/OperationType.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/OperationType.java new file mode 100644 index 000000000..ec3fb15e8 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/OperationType.java @@ -0,0 +1,66 @@ +/* + * 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; + +/** + * Represents the different remote MongoDB operations that can occur. + */ +public enum OperationType { + INSERT, DELETE, REPLACE, UPDATE, UNKNOWN; + + /** + * Returns the appropriate local operation type enum value vased on the remote operation type + * string from a change stream event. + * @param type the string description of the operation type. + * @return the operation type. + */ + public static OperationType fromRemote(final String type) { + switch (type) { + case "insert": + return INSERT; + case "delete": + return DELETE; + case "replace": + return REPLACE; + case "update": + return UPDATE; + default: + return UNKNOWN; + } + } + + /** + * Converts this operation to the remote string representation of the operation as + * represented in a {@link ChangeEvent} from a remote cluster. + * + * @return the remote representation of the update operation. + */ + public String toRemote() { + switch (this) { + case INSERT: + return "insert"; + case DELETE: + return "delete"; + case REPLACE: + return "replace"; + case UPDATE: + return "update"; + default: + return "unknown"; + } + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/UpdateDescription.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/UpdateDescription.java new file mode 100644 index 000000000..4063e7766 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/UpdateDescription.java @@ -0,0 +1,217 @@ +/* + * 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; + +import static com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer.DOCUMENT_VERSION_FIELD; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bson.BsonArray; +import org.bson.BsonBoolean; +import org.bson.BsonDocument; +import org.bson.BsonElement; +import org.bson.BsonValue; + +/** + * Indicates which fields have been modified in a given update operation. + */ +public final class UpdateDescription { + private final BsonDocument updatedFields; + private final Collection removedFields; + + /** + * Creates an update descirption with the specified updated fields and removed field names. + * @param updatedFields Nested key-value pair representation of updated fields. + * @param removedFields Collection of removed field names. + */ + public UpdateDescription( + final BsonDocument updatedFields, + final Collection removedFields + ) { + this.updatedFields = updatedFields == null ? new BsonDocument() : updatedFields; + this.removedFields = removedFields == null ? Collections.emptyList() : removedFields; + } + + /** + * Returns a {@link BsonDocument} containing keys and values representing (respectively) the + * fields that have changed in the corresponding update and their new values. + * + * @return the updated field names and their new values. + */ + public BsonDocument getUpdatedFields() { + return updatedFields; + } + + /** + * Returns a {@link List} containing the field names that have been removed in the corresponding + * update. + * + * @return the removed fields names. + */ + public Collection getRemovedFields() { + return removedFields; + } + + /** + * Convert this update description to an update document. + * + * @return an update document with the appropriate $set and $unset documents. + */ + public BsonDocument toUpdateDocument() { + final List unsets = new ArrayList<>(); + for (final String removedField : this.removedFields) { + unsets.add(new BsonElement(removedField, new BsonBoolean(true))); + } + final BsonDocument updateDocument = new BsonDocument(); + + if (this.updatedFields.size() > 0) { + updateDocument.append("$set", this.updatedFields); + } + + if (unsets.size() > 0) { + updateDocument.append("$unset", new BsonDocument(unsets)); + } + + return updateDocument; + } + + /** + * Find the diff between two documents. + * + *

NOTE: This does not do a full diff on {@link BsonArray}. If there is + * an inequality between the old and new array, the old array will + * simply be replaced by the new one. + * + * @param beforeDocument original document + * @param afterDocument document to diff on + * @param onKey the key for our depth level + * @param updatedFields contiguous document of updated fields, + * nested or otherwise + * @param removedFields contiguous list of removedFields, + * nested or otherwise + * @return a description of the updated fields and removed keys between the documents + */ + private static UpdateDescription diff( + final @Nonnull BsonDocument beforeDocument, + final @Nonnull BsonDocument afterDocument, + final @Nullable String onKey, + final BsonDocument updatedFields, + final List removedFields) { + // for each key in this document... + for (final Map.Entry entry : beforeDocument.entrySet()) { + final String key = entry.getKey(); + // don't worry about the _id or version field for now + if (key.equals("_id") || key.equals(DOCUMENT_VERSION_FIELD)) { + continue; + } + final BsonValue oldValue = entry.getValue(); + + final String actualKey = onKey == null ? key : String.format("%s.%s", onKey, key); + // if the key exists in the other document AND both are BsonDocuments + // diff the documents recursively, carrying over the keys to keep + // updatedFields and removedFields flat. + // this will allow us to reference whole objects as well as nested + // properties. + // else if the key does not exist, the key has been removed. + if (afterDocument.containsKey(key)) { + final BsonValue newValue = afterDocument.get(key); + if (oldValue instanceof BsonDocument && newValue instanceof BsonDocument) { + diff((BsonDocument) oldValue, + (BsonDocument) newValue, + actualKey, + updatedFields, + removedFields); + } else if (!oldValue.equals(newValue)) { + updatedFields.put(actualKey, newValue); + } + } else { + removedFields.add(actualKey); + } + } + + // for each key in the other document... + for (final Map.Entry entry : afterDocument.entrySet()) { + final String key = entry.getKey(); + // don't worry about the _id or version field for now + if (key.equals("_id") || key.equals(DOCUMENT_VERSION_FIELD)) { + continue; + } + + final BsonValue newValue = entry.getValue(); + // if the key is not in the this document, + // 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); + if (!beforeDocument.containsKey(key)) { + updatedFields.put(actualKey, newValue); + } + } + + return new UpdateDescription(updatedFields, removedFields); + } + + /** + * Find the diff between two documents. + * + *

NOTE: This does not do a full diff on [BsonArray]. If there is + * an inequality between the old and new array, the old array will + * simply be replaced by the new one. + * + * @param beforeDocument original document + * @param afterDocument document to diff on + * @return a description of the updated fields and removed keys between the documents. + */ + public 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, + null, + new BsonDocument(), + new ArrayList<>() + ); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null || !obj.getClass().equals(UpdateDescription.class)) { + return false; + } + final UpdateDescription other = (UpdateDescription) obj; + + return other.getRemovedFields().equals(this.removedFields) + && other.getUpdatedFields().equals(this.updatedFields); + } + + @Override + public int hashCode() { + return removedFields.hashCode() + 31 * updatedFields.hashCode(); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollection.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollection.java index b0303ec69..937d02099 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollection.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollection.java @@ -17,6 +17,8 @@ package com.mongodb.stitch.core.services.mongodb.remote.internal; import com.mongodb.MongoNamespace; +import com.mongodb.stitch.core.internal.net.Stream; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; @@ -25,9 +27,13 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.sync.CoreSync; +import java.io.IOException; import java.util.List; + +import org.bson.BsonValue; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; +import org.bson.types.ObjectId; public interface CoreRemoteMongoCollection { @@ -238,6 +244,23 @@ RemoteUpdateResult updateMany( final Bson update, final RemoteUpdateOptions updateOptions); + /** + * Watches specified IDs in a collection. This convenience overload supports the use case + * of non-{@link BsonValue} instances of {@link ObjectId}. + * @param ids unique object identifiers of the IDs to watch. + * @return the stream of change events. + */ + Stream> watch(final ObjectId... ids) + throws InterruptedException, IOException; + + /** + * Watches specified IDs in a collection. + * @param ids the ids to watch. + * @return the stream of change events. + */ + Stream> watch(final BsonValue... ids) + throws InterruptedException, IOException; + /** * A set of synchronization related operations at the collection level. * diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionImpl.java index c15495f6a..02117c5e0 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/CoreRemoteMongoCollectionImpl.java @@ -20,7 +20,9 @@ import com.mongodb.MongoNamespace; import com.mongodb.stitch.core.internal.net.NetworkMonitor; +import com.mongodb.stitch.core.internal.net.Stream; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; @@ -32,10 +34,17 @@ import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer; import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.SyncOperations; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; + import org.bson.BsonDocument; +import org.bson.BsonObjectId; +import org.bson.BsonValue; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; +import org.bson.types.ObjectId; public class CoreRemoteMongoCollectionImpl implements CoreRemoteMongoCollection { @@ -365,6 +374,37 @@ private RemoteUpdateResult executeUpdate( : operations.updateOne(filter, update, updateOptions).execute(service); } + /** + * Watches specified IDs in a collection. This convenience overload supports the use case + * of non-{@link BsonValue} instances of {@link ObjectId} by wrapping them in + * {@link BsonObjectId} instances for the user. + * @param ids unique object identifiers of the IDs to watch. + * @return the stream of change events. + */ + @Override + public Stream> watch(final ObjectId... ids) + throws InterruptedException, IOException { + final BsonValue[] transformedIds = new BsonValue[ids.length]; + + for (int i = 0; i < ids.length; i++) { + transformedIds[i] = new BsonObjectId(ids[i]); + } + + return watch(transformedIds); + } + + /** + * Watches specified IDs in a collection. + * @param ids the ids to watch. + * @return the stream of change events. + */ + @Override + @SuppressWarnings("unchecked") + public Stream> watch(final BsonValue... ids) + throws InterruptedException, IOException { + return operations.watch(new HashSet<>(Arrays.asList(ids)), documentClass).execute(service); + } + @Override public CoreSync sync() { return this.sync; diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/Operations.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/Operations.java index f75431b62..2be6c4a34 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/Operations.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/Operations.java @@ -26,9 +26,13 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteFindOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions; + import java.util.ArrayList; import java.util.List; +import java.util.Set; + import org.bson.BsonDocument; +import org.bson.BsonValue; import org.bson.codecs.CollectibleCodec; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; @@ -163,6 +167,12 @@ UpdateManyOperation updateMany( .upsert(updateOptions.isUpsert()); } + WatchOperation watch( + final Set ids, + final Class resultClass) { + return new WatchOperation<>(namespace, ids, codecRegistry.get(resultClass)); + } + private List toBsonDocumentList(final List bsonList) { if (bsonList == null) { return null; diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/ResultDecoders.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/ResultDecoders.java index 0897a33cb..1b4769fc8 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/ResultDecoders.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/ResultDecoders.java @@ -18,17 +18,21 @@ import static com.mongodb.stitch.core.internal.common.Assertions.keyPresent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; + import java.util.HashMap; import java.util.Map; + import org.bson.BsonArray; import org.bson.BsonDocument; import org.bson.BsonReader; import org.bson.BsonValue; import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.Codec; import org.bson.codecs.Decoder; import org.bson.codecs.DecoderContext; @@ -37,7 +41,9 @@ public class ResultDecoders { public static final Decoder updateResultDecoder = new UpdateResultDecoder(); private static final class UpdateResultDecoder implements Decoder { - public RemoteUpdateResult decode(final BsonReader reader, final DecoderContext decoderContext) { + public RemoteUpdateResult decode( + final BsonReader reader, + final DecoderContext decoderContext) { final BsonDocument document = (new BsonDocumentCodec()).decode(reader, decoderContext); keyPresent(Fields.MATCHED_COUNT_FIELD, document); keyPresent(Fields.MODIFIED_COUNT_FIELD, document); @@ -63,7 +69,9 @@ private static final class Fields { public static final Decoder deleteResultDecoder = new DeleteResultDecoder(); private static final class DeleteResultDecoder implements Decoder { - public RemoteDeleteResult decode(final BsonReader reader, final DecoderContext decoderContext) { + public RemoteDeleteResult decode( + final BsonReader reader, + final DecoderContext decoderContext) { final BsonDocument document = (new BsonDocumentCodec()).decode(reader, decoderContext); keyPresent(Fields.DELETED_COUNT_FIELD, document); return new RemoteDeleteResult(document.getNumber(Fields.DELETED_COUNT_FIELD).longValue()); @@ -115,4 +123,60 @@ private static final class Fields { static final String INSERTED_IDS_FIELD = "insertedIds"; } } + + @SuppressWarnings("unused") + public static Decoder> + changeEventDecoder(final Codec codec) { + return new ChangeEventDecoder<>(codec); + } + + private static final class ChangeEventDecoder implements + Decoder> { + private final Codec codec; + + ChangeEventDecoder(final Codec codec) { + this.codec = codec; + } + + @Override + @SuppressWarnings("unchecked") + public ChangeEvent decode( + final BsonReader reader, + final DecoderContext decoderContext + ) { + final BsonDocument document = (new BsonDocumentCodec()).decode(reader, decoderContext); + final ChangeEvent rawChangeEvent = ChangeEvent.fromBsonDocument(document); + + if (codec == null || codec.getClass().equals(BsonDocumentCodec.class)) { + return (ChangeEvent)rawChangeEvent; + } + return new ChangeEvent<>( + rawChangeEvent.getId(), + rawChangeEvent.getOperationType(), + rawChangeEvent.getFullDocument() == null ? null : codec.decode( + rawChangeEvent.getFullDocument().asBsonReader(), + DecoderContext.builder().build()), + rawChangeEvent.getNamespace(), + rawChangeEvent.getDocumentKey(), + rawChangeEvent.getUpdateDescription(), + rawChangeEvent.hasUncommittedWrites()); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null || obj.getClass() != ChangeEventDecoder.class) { + return false; + } + final ChangeEventDecoder other = (ChangeEventDecoder) obj; + + // caveat: if someone writes a stateful codec then this logic won't hold up, but we + // can't use .equals without opening a can of worms + return other.codec.getClass() == this.codec.getClass(); + } + + @Override + public int hashCode() { + return this.codec.hashCode(); + } + } } diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/WatchOperation.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/WatchOperation.java new file mode 100644 index 000000000..4a1afc6bb --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/internal/WatchOperation.java @@ -0,0 +1,59 @@ +/* + * 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.internal; + +import com.mongodb.MongoNamespace; +import com.mongodb.stitch.core.internal.net.Stream; +import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +import org.bson.BsonValue; +import org.bson.Document; +import org.bson.codecs.Codec; + +public class WatchOperation { + private final MongoNamespace namespace; + private final Set ids; + private final Codec fullDocumentCodec; + + WatchOperation( + final MongoNamespace namespace, + final Set ids, + final Codec fullDocumentCodec + ) { + this.namespace = namespace; + this.ids = ids; + this.fullDocumentCodec = fullDocumentCodec; + } + + public Stream> execute(final CoreStitchServiceClient service) + throws InterruptedException, IOException { + final Document args = new Document(); + args.put("database", namespace.getDatabaseName()); + args.put("collection", namespace.getCollectionName()); + args.put("ids", ids); + + return service.streamFunction( + "watch", + Collections.singletonList(args), + ResultDecoders.changeEventDecoder(fullDocumentCodec)); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ChangeEventListener.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ChangeEventListener.java index b269d591a..37c21d98b 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ChangeEventListener.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ChangeEventListener.java @@ -16,7 +16,7 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync; -import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import org.bson.BsonValue; diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ConflictHandler.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ConflictHandler.java index 53a3f3390..7a7fc57e4 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ConflictHandler.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ConflictHandler.java @@ -16,7 +16,7 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync; -import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import org.bson.BsonValue; 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 e328e5c63..b72b89354 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,6 +16,8 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync; +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener; + import java.util.List; import java.util.Set; @@ -37,11 +39,11 @@ public interface CoreSync { * 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 + * @param exceptionListener the error listener to invoke when an irrecoverable error occurs */ void configure(@Nonnull final ConflictHandler conflictHandler, @Nullable final ChangeEventListener changeEventListener, - @Nullable final ErrorListener errorListener); + @Nullable final ExceptionListener exceptionListener); /** * Requests that the given document _id be synchronized. diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/DefaultSyncConflictResolvers.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/DefaultSyncConflictResolvers.java index ed8db4ffa..3dc11c98c 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/DefaultSyncConflictResolvers.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/DefaultSyncConflictResolvers.java @@ -16,7 +16,7 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync; -import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import org.bson.BsonValue; diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ErrorListener.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ErrorListener.java index 423c33013..20d282bfa 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ErrorListener.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/ErrorListener.java @@ -16,18 +16,12 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync; -import org.bson.BsonValue; +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener; /** - * ErrorListener receives non-network related errors that occur. + * ExceptionListener receives non-network related errors that occur. */ -public interface ErrorListener { - - /** - * Called when an error happens for the given document id. - * - * @param documentId the _id of the document related to the error. - * @param error the error. - */ - void onError(final BsonValue documentId, final Exception error); +@Deprecated +public interface ErrorListener extends ExceptionListener { + // Empty extension for backward compatibility until removal } 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 deleted file mode 100644 index 8111d29fa..000000000 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvent.java +++ /dev/null @@ -1,565 +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 static com.mongodb.stitch.core.internal.common.Assertions.keyPresent; -import static com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer.DOCUMENT_VERSION_FIELD; -import static com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer.sanitizeDocument; - -import com.mongodb.MongoNamespace; -import com.mongodb.stitch.core.internal.common.BsonUtils; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -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; -import org.bson.BsonDocument; -import org.bson.BsonElement; -import org.bson.BsonReader; -import org.bson.BsonString; -import org.bson.BsonType; -import org.bson.BsonValue; -import org.bson.BsonWriter; -import org.bson.codecs.BsonDocumentCodec; -import org.bson.codecs.Codec; -import org.bson.codecs.Decoder; -import org.bson.codecs.DecoderContext; -import org.bson.codecs.EncoderContext; - -// TODO: Should there be a local and remote type for the pending part? -public final class ChangeEvent { - private final BsonDocument id; // Metadata related to the operation (the resumeToken). - private final OperationType operationType; - private final DocumentT fullDocument; - private final MongoNamespace ns; - private final BsonDocument documentKey; - private final UpdateDescription updateDescription; - private final boolean hasUncommittedWrites; - - ChangeEvent( - final BsonDocument id, - final OperationType operationType, - final DocumentT fullDocument, - final MongoNamespace ns, - final BsonDocument documentKey, - final UpdateDescription updateDescription, - final boolean hasUncommittedWrites - ) { - this.id = id; - this.operationType = operationType; - this.fullDocument = fullDocument; - this.ns = ns; - this.documentKey = documentKey; - this.updateDescription = updateDescription == null - ? new UpdateDescription(null, null) : updateDescription; - this.hasUncommittedWrites = hasUncommittedWrites; - } - - public BsonDocument getId() { - return id; - } - - public OperationType getOperationType() { - return operationType; - } - - public DocumentT getFullDocument() { - return fullDocument; - } - - public MongoNamespace getNamespace() { - return ns; - } - - public BsonDocument getDocumentKey() { - return documentKey; - } - - public UpdateDescription getUpdateDescription() { - return updateDescription; - } - - public boolean hasUncommittedWrites() { - return hasUncommittedWrites; - } - - public enum OperationType { - INSERT, DELETE, REPLACE, UPDATE, UNKNOWN; - - static OperationType fromRemote(final String type) { - switch (type) { - case "insert": - return INSERT; - case "delete": - return DELETE; - case "replace": - return REPLACE; - case "update": - return UPDATE; - default: - return UNKNOWN; - } - } - - String toRemote() { - switch (this) { - case INSERT: - return "insert"; - case DELETE: - return "delete"; - case REPLACE: - return "replace"; - case UPDATE: - return "update"; - default: - return "unknown"; - } - } - } - - public static final class UpdateDescription { - private final BsonDocument updatedFields; - private final Collection removedFields; - - UpdateDescription( - final BsonDocument updatedFields, - final Collection removedFields - ) { - this.updatedFields = updatedFields == null ? new BsonDocument() : updatedFields; - this.removedFields = removedFields == null ? Collections.emptyList() : removedFields; - } - - public BsonDocument getUpdatedFields() { - return updatedFields; - } - - public Collection getRemovedFields() { - return removedFields; - } - - /** - * Convert this update description to an update document. - * @return an update document with the appropriate $set and $unset - * documents - */ - BsonDocument toUpdateDocument() { - final List unsets = new ArrayList<>(); - for (final String removedField : this.removedFields) { - unsets.add(new BsonElement(removedField, new BsonBoolean(true))); - } - final BsonDocument updateDocument = new BsonDocument(); - - if (this.updatedFields.size() > 0) { - updateDocument.append("$set", this.updatedFields); - } - - if (unsets.size() > 0) { - updateDocument.append("$unset", new BsonDocument(unsets)); - } - - return updateDocument; - } - - /** - * Find the diff between two documents. - * - * NOTE: This does not do a full diff on {@link BsonArray}. If there is - * an inequality between the old and new array, the old array will - * simply be replaced by the new one. - * - * @param beforeDocument original document - * @param afterDocument document to diff on - * @param onKey the key for our depth level - * @param updatedFields contiguous document of updated fields, - * nested or otherwise - * @param removedFields contiguous list of removedFields, - * nested or otherwise - * @return a description of the updated fields and removed keys between - * the documents - */ - private static UpdateDescription diff(final @Nonnull BsonDocument beforeDocument, - final @Nonnull BsonDocument afterDocument, - final @Nullable String onKey, - final BsonDocument updatedFields, - final List removedFields) { - // for each key in this document... - for (final Map.Entry entry: beforeDocument.entrySet()) { - final String key = entry.getKey(); - // don't worry about the _id or version field for now - if (key.equals("_id") || key.equals(DOCUMENT_VERSION_FIELD)) { - continue; - } - final BsonValue oldValue = entry.getValue(); - - final String actualKey = onKey == null ? key : String.format("%s.%s", onKey, key); - // if the key exists in the other document AND both are BsonDocuments - // diff the documents recursively, carrying over the keys to keep - // updatedFields and removedFields flat. - // this will allow us to reference whole objects as well as nested - // properties. - // else if the key does not exist, the key has been removed. - if (afterDocument.containsKey(key)) { - final BsonValue newValue = afterDocument.get(key); - if (oldValue instanceof BsonDocument && newValue instanceof BsonDocument) { - diff((BsonDocument) oldValue, - (BsonDocument) newValue, - actualKey, - updatedFields, - removedFields); - } else if (!oldValue.equals(newValue)) { - updatedFields.put(actualKey, newValue); - } - } else { - removedFields.add(actualKey); - } - } - - // for each key in the other document... - for (final Map.Entry entry: afterDocument.entrySet()) { - final String key = entry.getKey(); - // don't worry about the _id or version field for now - if (key.equals("_id") || key.equals(DOCUMENT_VERSION_FIELD)) { - continue; - } - - final BsonValue newValue = entry.getValue(); - // if the key is not in the this document, - // 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); - if (!beforeDocument.containsKey(key)) { - updatedFields.put(actualKey, newValue); - } - } - - return new UpdateDescription(updatedFields, removedFields); - } - - /** - * Find the diff between two documents. - * - * NOTE: This does not do a full diff on [BsonArray]. If there is - * an inequality between the old and new array, the old array will - * simply be replaced by the new one. - * - * @param beforeDocument original document - * @param afterDocument document to diff on - * @return a description of the updated fields and removed keys between - * the documents - */ - 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, - null, - new BsonDocument(), - new ArrayList<>() - ); - } - } - - static BsonDocument toBsonDocument(final ChangeEvent value) { - final BsonDocument asDoc = new BsonDocument(); - asDoc.put(ChangeEventCoder.Fields.ID_FIELD, value.getId()); - asDoc.put(ChangeEventCoder.Fields.OPERATION_TYPE_FIELD, - new BsonString(value.getOperationType().toRemote())); - final BsonDocument nsDoc = new BsonDocument(); - nsDoc.put(ChangeEventCoder.Fields.NS_DB_FIELD, - new BsonString(value.getNamespace().getDatabaseName())); - nsDoc.put(ChangeEventCoder.Fields.NS_COLL_FIELD, - new BsonString(value.getNamespace().getCollectionName())); - asDoc.put(ChangeEventCoder.Fields.NS_FIELD, nsDoc); - asDoc.put(ChangeEventCoder.Fields.DOCUMENT_KEY_FIELD, value.getDocumentKey()); - if (value.getFullDocument() != null) { - asDoc.put(ChangeEventCoder.Fields.FULL_DOCUMENT_FIELD, value.getFullDocument()); - } - if (value.getUpdateDescription() != null) { - final BsonDocument updateDescDoc = new BsonDocument(); - updateDescDoc.put( - ChangeEventCoder.Fields.UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD, - value.getUpdateDescription().getUpdatedFields()); - - final BsonArray removedFields = new BsonArray(); - for (final String field : value.getUpdateDescription().getRemovedFields()) { - removedFields.add(new BsonString(field)); - } - updateDescDoc.put( - ChangeEventCoder.Fields.UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD, - removedFields); - asDoc.put(ChangeEventCoder.Fields.UPDATE_DESCRIPTION_FIELD, updateDescDoc); - } - asDoc.put(ChangeEventCoder.Fields.WRITE_PENDING_FIELD, - new BsonBoolean(value.hasUncommittedWrites)); - return asDoc; - } - - static ChangeEvent fromBsonDocument(final BsonDocument document) { - keyPresent(ChangeEventCoder.Fields.ID_FIELD, document); - keyPresent(ChangeEventCoder.Fields.OPERATION_TYPE_FIELD, document); - keyPresent(ChangeEventCoder.Fields.NS_FIELD, document); - keyPresent(ChangeEventCoder.Fields.DOCUMENT_KEY_FIELD, document); - - final BsonDocument nsDoc = document.getDocument(ChangeEventCoder.Fields.NS_FIELD); - final ChangeEvent.UpdateDescription updateDescription; - if (document.containsKey(ChangeEventCoder.Fields.UPDATE_DESCRIPTION_FIELD)) { - final BsonDocument updateDescDoc = - document.getDocument(ChangeEventCoder.Fields.UPDATE_DESCRIPTION_FIELD); - keyPresent(ChangeEventCoder.Fields.UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD, updateDescDoc); - keyPresent(ChangeEventCoder.Fields.UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD, updateDescDoc); - - final BsonArray removedFieldsArr = - updateDescDoc.getArray(ChangeEventCoder.Fields.UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD); - final Collection removedFields = new ArrayList<>(removedFieldsArr.size()); - for (final BsonValue field : removedFieldsArr) { - removedFields.add(field.asString().getValue()); - } - updateDescription = new ChangeEvent.UpdateDescription(updateDescDoc.getDocument( - ChangeEventCoder.Fields.UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD), - removedFields); - } else { - updateDescription = null; - } - - final BsonDocument fullDocument; - if (document.containsKey(ChangeEventCoder.Fields.FULL_DOCUMENT_FIELD)) { - final BsonValue fdVal = document.get(ChangeEventCoder.Fields.FULL_DOCUMENT_FIELD); - if (fdVal.isDocument()) { - fullDocument = fdVal.asDocument(); - } else { - fullDocument = null; - } - } else { - fullDocument = null; - } - - return new ChangeEvent<>( - document.getDocument(ChangeEventCoder.Fields.ID_FIELD), - ChangeEvent.OperationType.fromRemote( - document.getString(ChangeEventCoder.Fields.OPERATION_TYPE_FIELD).getValue()), - fullDocument, - new MongoNamespace( - nsDoc.getString(ChangeEventCoder.Fields.NS_DB_FIELD).getValue(), - nsDoc.getString(ChangeEventCoder.Fields.NS_COLL_FIELD).getValue()), - document.getDocument(ChangeEventCoder.Fields.DOCUMENT_KEY_FIELD), - updateDescription, - document.getBoolean( - ChangeEventCoder.Fields.WRITE_PENDING_FIELD, - BsonBoolean.FALSE).getValue()); - } - - static final ChangeEventsDecoder changeEventsDecoder = new ChangeEventsDecoder(); - - private static class ChangeEventsDecoder - implements Decoder>>> { - public List>> decode( - final BsonReader reader, - final DecoderContext decoderContext - ) { - final LinkedHashMap> latestEvents = - new LinkedHashMap<>(); - reader.readStartArray(); - while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { - final ChangeEvent event = changeEventCoder.decode(reader, decoderContext); - final BsonValue docId = event.getDocumentKey().get("_id"); - if (latestEvents.containsKey(docId)) { - latestEvents.remove(docId); - } - latestEvents.put(docId, event); - } - reader.readEndArray(); - return new ArrayList<>(latestEvents.entrySet()); - } - } - - static final ChangeEventCoder changeEventCoder = new ChangeEventCoder(); - - static class ChangeEventCoder implements Codec> { - public ChangeEvent decode( - final BsonReader reader, - final DecoderContext decoderContext - ) { - final BsonDocument document = (new BsonDocumentCodec()).decode(reader, decoderContext); - return fromBsonDocument(document); - } - - @Override - public void encode( - final BsonWriter writer, - final ChangeEvent value, - final EncoderContext encoderContext - ) { - new BsonDocumentCodec().encode(writer, toBsonDocument(value), encoderContext); - } - - @Override - public Class> getEncoderClass() { - return null; - } - - private static final class Fields { - static final String ID_FIELD = "_id"; - static final String OPERATION_TYPE_FIELD = "operationType"; - static final String FULL_DOCUMENT_FIELD = "fullDocument"; - static final String DOCUMENT_KEY_FIELD = "documentKey"; - - static final String NS_FIELD = "ns"; - static final String NS_DB_FIELD = "db"; - static final String NS_COLL_FIELD = "coll"; - - static final String UPDATE_DESCRIPTION_FIELD = "updateDescription"; - static final String UPDATE_DESCRIPTION_UPDATED_FIELDS_FIELD = "updatedFields"; - static final String UPDATE_DESCRIPTION_REMOVED_FIELDS_FIELD = "removedFields"; - - static final String WRITE_PENDING_FIELD = "writePending"; - } - } - - /** - * Generates a change event for a local insert of the given document in the given namespace. - * - * @param namespace the namespace where the document was inserted. - * @param document the document that was inserted. - * @return a change event for a local insert of the given document in the given namespace. - */ - static ChangeEvent changeEventForLocalInsert( - final MongoNamespace namespace, - final BsonDocument document, - final boolean writePending - ) { - final BsonValue docId = BsonUtils.getDocumentId(document); - return new ChangeEvent<>( - new BsonDocument(), - ChangeEvent.OperationType.INSERT, - document, - namespace, - new BsonDocument("_id", docId), - null, - writePending); - } - - /** - * Generates a change event for a local update of a document in the given namespace referring - * to the given document _id. - * - * @param namespace the namespace where the document was inserted. - * @param documentId the _id of the document that was updated. - * @param update the update specifier. - * @return a change event for a local update of a document in the given namespace referring - * to the given document _id. - */ - static ChangeEvent changeEventForLocalUpdate( - final MongoNamespace namespace, - final BsonValue documentId, - final UpdateDescription update, - final BsonDocument fullDocumentAfterUpdate, - final boolean writePending - ) { - return new ChangeEvent<>( - new BsonDocument(), - ChangeEvent.OperationType.UPDATE, - fullDocumentAfterUpdate, - namespace, - new BsonDocument("_id", documentId), - update, - writePending); - } - - /** - * Generates a change event for a local replacement of a document in the given namespace referring - * to the given document _id. - * - * @param namespace the namespace where the document was inserted. - * @param documentId the _id of the document that was updated. - * @param document the replacement document. - * @return a change event for a local replacement of a document in the given namespace referring - * to the given document _id. - */ - static ChangeEvent changeEventForLocalReplace( - final MongoNamespace namespace, - final BsonValue documentId, - final BsonDocument document, - final boolean writePending - ) { - return new ChangeEvent<>( - new BsonDocument(), - ChangeEvent.OperationType.REPLACE, - document, - namespace, - new BsonDocument("_id", documentId), - null, - writePending); - } - - /** - * Generates a change event for a local deletion of a document in the given namespace referring - * to the given document _id. - * - * @param namespace the namespace where the document was inserted. - * @param documentId the _id of the document that was updated. - * @return a change event for a local deletion of a document in the given namespace referring - * to the given document _id. - */ - static ChangeEvent changeEventForLocalDelete( - final MongoNamespace namespace, - final BsonValue documentId, - final boolean writePending - ) { - return new ChangeEvent<>( - new BsonDocument(), - ChangeEvent.OperationType.DELETE, - null, - namespace, - new BsonDocument("_id", documentId), - null, - writePending); - } - - /** - * Transforms a {@link ChangeEvent} into one that can be used by a user defined conflict resolver. - * @param event the event to transform. - * @param codec the codec to use to transform any documents specific to the collection. - * @return the transformed {@link ChangeEvent} - */ - static ChangeEvent transformChangeEventForUser( - final ChangeEvent event, - final Codec codec - ) { - return new ChangeEvent<>( - event.getId(), - event.getOperationType(), - event.getFullDocument() == null ? null : codec.decode( - sanitizeDocument(event.getFullDocument()).asBsonReader(), - DecoderContext.builder().build()), - event.getNamespace(), - event.getDocumentKey(), - event.getUpdateDescription(), - event.hasUncommittedWrites()); - } -} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvents.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvents.java new file mode 100644 index 000000000..bd3443c84 --- /dev/null +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/ChangeEvents.java @@ -0,0 +1,154 @@ +/* + * 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 static com.mongodb.stitch.core.services.mongodb.remote.sync.internal.DataSynchronizer.sanitizeDocument; + +import com.mongodb.MongoNamespace; +import com.mongodb.stitch.core.internal.common.BsonUtils; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.OperationType; +import com.mongodb.stitch.core.services.mongodb.remote.UpdateDescription; + +import org.bson.BsonDocument; +import org.bson.BsonValue; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; + +public final class ChangeEvents { + /** + * Generates a change event for a local insert of the given document in the given namespace. + * + * @param namespace the namespace where the document was inserted. + * @param document the document that was inserted. + * @return a change event for a local insert of the given document in the given namespace. + */ + static ChangeEvent changeEventForLocalInsert( + final MongoNamespace namespace, + final BsonDocument document, + final boolean writePending + ) { + final BsonValue docId = BsonUtils.getDocumentId(document); + return new ChangeEvent<>( + new BsonDocument(), + OperationType.INSERT, + document, + namespace, + new BsonDocument("_id", docId), + null, + writePending); + } + + /** + * Generates a change event for a local update of a document in the given namespace referring + * to the given document _id. + * + * @param namespace the namespace where the document was inserted. + * @param documentId the _id of the document that was updated. + * @param update the update specifier. + * @return a change event for a local update of a document in the given namespace referring + * to the given document _id. + */ + static ChangeEvent changeEventForLocalUpdate( + final MongoNamespace namespace, + final BsonValue documentId, + final UpdateDescription update, + final BsonDocument fullDocumentAfterUpdate, + final boolean writePending + ) { + return new ChangeEvent<>( + new BsonDocument(), + OperationType.UPDATE, + fullDocumentAfterUpdate, + namespace, + new BsonDocument("_id", documentId), + update, + writePending); + } + + /** + * Generates a change event for a local replacement of a document in the given namespace referring + * to the given document _id. + * + * @param namespace the namespace where the document was inserted. + * @param documentId the _id of the document that was updated. + * @param document the replacement document. + * @return a change event for a local replacement of a document in the given namespace referring + * to the given document _id. + */ + static ChangeEvent changeEventForLocalReplace( + final MongoNamespace namespace, + final BsonValue documentId, + final BsonDocument document, + final boolean writePending + ) { + return new ChangeEvent<>( + new BsonDocument(), + OperationType.REPLACE, + document, + namespace, + new BsonDocument("_id", documentId), + null, + writePending); + } + + /** + * Generates a change event for a local deletion of a document in the given namespace referring + * to the given document _id. + * + * @param namespace the namespace where the document was inserted. + * @param documentId the _id of the document that was updated. + * @return a change event for a local deletion of a document in the given namespace referring + * to the given document _id. + */ + static ChangeEvent changeEventForLocalDelete( + final MongoNamespace namespace, + final BsonValue documentId, + final boolean writePending + ) { + return new ChangeEvent<>( + new BsonDocument(), + OperationType.DELETE, + null, + namespace, + new BsonDocument("_id", documentId), + null, + writePending); + } + + /** + * Transforms a {@link ChangeEvent} into one that can be used by a user defined conflict resolver. + * @param event the event to transform. + * @param codec the codec to use to transform any documents specific to the collection. + * @return the transformed {@link ChangeEvent} + */ + static ChangeEvent transformChangeEventForUser( + final ChangeEvent event, + final Codec codec + ) { + return new ChangeEvent<>( + event.getId(), + event.getOperationType(), + event.getFullDocument() == null ? null : codec.decode( + sanitizeDocument(event.getFullDocument()).asBsonReader(), + DecoderContext.builder().build()), + event.getNamespace(), + event.getDocumentKey(), + event.getUpdateDescription(), + event.hasUncommittedWrites()); + } +} diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfig.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfig.java index 8f984712f..34f39029a 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfig.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfig.java @@ -20,6 +20,9 @@ import com.mongodb.MongoNamespace; import com.mongodb.client.MongoCollection; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.OperationType; +import com.mongodb.stitch.core.services.mongodb.remote.internal.ResultDecoders; import java.nio.ByteBuffer; import java.util.concurrent.locks.ReadWriteLock; @@ -46,10 +49,13 @@ class CoreDocumentSynchronizationConfig { + static final Codec BSON_DOCUMENT_CODEC = new BsonDocumentCodec(); + private final MongoCollection docsColl; private final MongoNamespace namespace; private final BsonValue documentId; private final ReadWriteLock docLock; + private final BsonDocumentCodec bsonDocumentCodec = new BsonDocumentCodec(); private ChangeEvent lastUncommittedChangeEvent; private long lastResolution; private BsonDocument lastKnownRemoteVersion; @@ -61,47 +67,37 @@ class CoreDocumentSynchronizationConfig { final MongoNamespace namespace, final BsonValue documentId ) { - this.docsColl = docsColl; - this.namespace = namespace; - this.documentId = documentId; - this.docLock = new ReentrantReadWriteLock(); - this.lastResolution = -1; - this.lastKnownRemoteVersion = null; - this.lastUncommittedChangeEvent = null; - this.isStale = false; + this(docsColl, namespace, documentId, null, -1, null, new ReentrantReadWriteLock(), + false, false); } CoreDocumentSynchronizationConfig( final MongoCollection docsColl, final CoreDocumentSynchronizationConfig config ) { - this.docsColl = docsColl; - this.namespace = config.namespace; - this.documentId = config.documentId; - this.docLock = config.docLock; - this.lastResolution = config.lastResolution; - this.lastKnownRemoteVersion = config.lastKnownRemoteVersion; - this.lastUncommittedChangeEvent = config.lastUncommittedChangeEvent; - this.isStale = config.isStale; - this.isPaused = config.isPaused; + this(docsColl, config.namespace, config.documentId, config.lastUncommittedChangeEvent, + config.lastResolution, config.lastKnownRemoteVersion, config.docLock, config.isStale, + config.isPaused); } private CoreDocumentSynchronizationConfig( + final MongoCollection docsColl, final MongoNamespace namespace, final BsonValue documentId, final ChangeEvent lastUncommittedChangeEvent, final long lastResolution, final BsonDocument lastVersion, + final ReadWriteLock docsLock, final boolean isStale, final boolean isPaused ) { + this.docsColl = docsColl; this.namespace = namespace; this.documentId = documentId; this.lastResolution = lastResolution; this.lastKnownRemoteVersion = lastVersion; this.lastUncommittedChangeEvent = lastUncommittedChangeEvent; - this.docLock = new ReentrantReadWriteLock(); - this.docsColl = null; + this.docLock = docsLock; this.isStale = isStale; this.isPaused = isPaused; } @@ -373,7 +369,7 @@ private static ChangeEvent coalesceChangeEvents( case UPDATE: return new ChangeEvent<>( newestChangeEvent.getId(), - ChangeEvent.OperationType.INSERT, + OperationType.INSERT, newestChangeEvent.getFullDocument(), newestChangeEvent.getNamespace(), newestChangeEvent.getDocumentKey(), @@ -392,7 +388,7 @@ private static ChangeEvent coalesceChangeEvents( case INSERT: return new ChangeEvent<>( newestChangeEvent.getId(), - ChangeEvent.OperationType.REPLACE, + OperationType.REPLACE, newestChangeEvent.getFullDocument(), newestChangeEvent.getNamespace(), newestChangeEvent.getDocumentKey(), @@ -420,11 +416,12 @@ BsonDocument toBsonDocument() { if (getLastKnownRemoteVersion() != null) { asDoc.put(ConfigCodec.Fields.LAST_KNOWN_REMOTE_VERSION_FIELD, getLastKnownRemoteVersion()); } - if (getLastUncommittedChangeEvent() != null) { - final BsonDocument ceDoc = ChangeEvent.toBsonDocument(getLastUncommittedChangeEvent()); + + if (lastUncommittedChangeEvent != null) { + final BsonDocument ceDoc = lastUncommittedChangeEvent.toBsonDocument(); final OutputBuffer outputBuffer = new BasicOutputBuffer(); final BsonWriter innerWriter = new BsonBinaryWriter(outputBuffer); - new BsonDocumentCodec().encode(innerWriter, ceDoc, EncoderContext.builder().build()); + bsonDocumentCodec.encode(innerWriter, ceDoc, EncoderContext.builder().build()); final BsonBinary encoded = new BsonBinary(outputBuffer.toByteArray()); // TODO: This may put the doc above the 16MiB but ignore for now. asDoc.put(ConfigCodec.Fields.LAST_UNCOMMITTED_CHANGE_EVENT, encoded); @@ -470,18 +467,20 @@ static CoreDocumentSynchronizationConfig fromBsonDocument(final BsonDocument doc final BsonBinary eventBin = document.getBinary(ConfigCodec.Fields.LAST_UNCOMMITTED_CHANGE_EVENT); final BsonReader innerReader = new BsonBinaryReader(ByteBuffer.wrap(eventBin.getData())); - lastUncommittedChangeEvent = - ChangeEvent.changeEventCoder.decode(innerReader, DecoderContext.builder().build()); + lastUncommittedChangeEvent = ResultDecoders.changeEventDecoder(BSON_DOCUMENT_CODEC) + .decode(innerReader, DecoderContext.builder().build()); } else { lastUncommittedChangeEvent = null; } return new CoreDocumentSynchronizationConfig( + null, namespace, document.get(ConfigCodec.Fields.DOCUMENT_ID_FIELD), lastUncommittedChangeEvent, document.getNumber(ConfigCodec.Fields.LAST_RESOLUTION_FIELD).longValue(), lastVersion, + new ReentrantReadWriteLock(), document.getBoolean(ConfigCodec.Fields.IS_STALE).getValue(), document.getBoolean(ConfigCodec.Fields.IS_PAUSED, new BsonBoolean(false)).getValue()); } 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 acf33bd42..057d44ac6 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 @@ -18,12 +18,12 @@ import com.mongodb.MongoNamespace; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener; 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; @@ -63,12 +63,12 @@ public CoreSyncImpl(final MongoNamespace namespace, @Override public void configure(@Nonnull final ConflictHandler conflictHandler, @Nullable final ChangeEventListener changeEventListener, - @Nullable final ErrorListener errorListener) { + @Nullable final ExceptionListener exceptionListener) { this.dataSynchronizer.configure( namespace, conflictHandler, changeEventListener, - errorListener, + exceptionListener, this.service.getCodecRegistry().get(documentClass) ); } 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 c887c0990..5815a267a 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 @@ -16,11 +16,6 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal; -import static com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent.changeEventForLocalDelete; -import static com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent.changeEventForLocalInsert; -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; @@ -47,13 +42,16 @@ import com.mongodb.stitch.core.internal.common.Dispatcher; import com.mongodb.stitch.core.internal.net.NetworkMonitor; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener; +import com.mongodb.stitch.core.services.mongodb.remote.OperationType; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; +import com.mongodb.stitch.core.services.mongodb.remote.UpdateDescription; import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoClient; import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollection; 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 java.lang.ref.WeakReference; import java.util.ArrayList; @@ -92,7 +90,7 @@ */ public class DataSynchronizer implements NetworkMonitor.StateListener { - static final String DOCUMENT_VERSION_FIELD = "__stitch_sync_version"; + public static final String DOCUMENT_VERSION_FIELD = "__stitch_sync_version"; private final CoreStitchServiceClient service; private final CoreRemoteMongoClient remoteClient; @@ -118,7 +116,7 @@ public class DataSynchronizer implements NetworkMonitor.StateListener { private final Lock listenersLock; private final Dispatcher eventDispatcher; - private ErrorListener errorListener; + private ExceptionListener exceptionListener; private Thread initThread; private DispatchGroup ongoingOperationsGroup; @@ -348,7 +346,7 @@ public void wipeInMemorySettings() { public void configure(@Nonnull final MongoNamespace namespace, @Nullable final ConflictHandler conflictHandler, @Nullable final ChangeEventListener changeEventListener, - @Nullable final ErrorListener errorListener, + @Nullable final ExceptionListener exceptionListener, @Nonnull final Codec codec) { this.waitUntilInitialized(); @@ -360,7 +358,7 @@ public void configure(@Nonnull final MongoNamespace namespace, return; } - this.errorListener = errorListener; + this.exceptionListener = exceptionListener; this.syncConfig.getNamespaceConfig(namespace).configure( conflictHandler, @@ -593,7 +591,7 @@ private void syncRemoteToLocal() { syncRemoteChangeEventToLocal( nsConfig, docConfig, - changeEventForLocalReplace( + ChangeEvents.changeEventForLocalReplace( nsConfig.getNamespace(), docId, latestDocumentMap.get(docId), @@ -623,7 +621,7 @@ private void syncRemoteToLocal() { syncRemoteChangeEventToLocal( nsConfig, docConfig, - changeEventForLocalDelete( + ChangeEvents.changeEventForLocalDelete( nsConfig.getNamespace(), unseenId, docConfig.hasUncommittedWrites() @@ -855,7 +853,7 @@ private void syncRemoteChangeEventToLocal( resolveConflict( nsConfig.getNamespace(), docConfig, - changeEventForLocalDelete( + ChangeEvents.changeEventForLocalDelete( nsConfig.getNamespace(), docConfig.getDocumentId(), docConfig.hasUncommittedWrites())); @@ -914,7 +912,7 @@ private void syncRemoteChangeEventToLocal( resolveConflict( nsConfig.getNamespace(), docConfig, - changeEventForLocalReplace( + ChangeEvents.changeEventForLocalReplace( nsConfig.getNamespace(), docConfig.getDocumentId(), newestRemoteDocument, @@ -1137,7 +1135,7 @@ private void syncLocalToRemote() { continue; } - final ChangeEvent.UpdateDescription localUpdateDescription = + final UpdateDescription localUpdateDescription = localChangeEvent.getUpdateDescription(); if (localUpdateDescription.getRemovedFields().isEmpty() && localUpdateDescription.getUpdatedFields().isEmpty()) { @@ -1353,7 +1351,7 @@ private void emitError(final CoreDocumentSynchronizationConfig docConfig, private void emitError(final CoreDocumentSynchronizationConfig docConfig, final String msg, final Exception ex) { - if (this.errorListener != null) { + if (this.exceptionListener != null) { final Exception dispatchException; if (ex == null) { dispatchException = new DataSynchronizerException(msg); @@ -1361,7 +1359,7 @@ private void emitError(final CoreDocumentSynchronizationConfig docConfig, dispatchException = ex; } this.eventDispatcher.dispatch(() -> { - errorListener.onError(docConfig.getDocumentId(), dispatchException); + exceptionListener.onError(docConfig.getDocumentId(), dispatchException); return null; }); } @@ -1414,11 +1412,10 @@ private void resolveConflict( final Object resolvedDocument; final ChangeEvent transformedRemoteEvent; try { - final ChangeEvent transformedLocalEvent = ChangeEvent.transformChangeEventForUser( + final ChangeEvent transformedLocalEvent = ChangeEvents.transformChangeEventForUser( docConfig.getLastUncommittedChangeEvent(), syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); - transformedRemoteEvent = - ChangeEvent.transformChangeEventForUser( + transformedRemoteEvent = ChangeEvents.transformChangeEventForUser( remoteEvent, syncConfig.getNamespaceConfig(namespace).getDocumentCodec()); resolvedDocument = resolveConflictWithResolver( @@ -1447,7 +1444,7 @@ private void resolveConflict( } final BsonDocument remoteVersion; - if (remoteEvent.getOperationType() == ChangeEvent.OperationType.DELETE) { + if (remoteEvent.getOperationType() == OperationType.DELETE) { // We expect there will be no version on the document. Note: it's very possible // that the document could be reinserted at this point with no version field and we // would end up deleting it, unless we receive a notification in time. @@ -1593,9 +1590,9 @@ private ChangeEvent getSynthesizedRemoteChangeEventForDocument( // a. When the document is looked up, if it cannot be found the synthesized change event is a // DELETE, otherwise it's a REPLACE. if (document == null) { - return changeEventForLocalDelete(ns, documentId, false); + return ChangeEvents.changeEventForLocalDelete(ns, documentId, false); } - return changeEventForLocalReplace(ns, documentId, document, false); + return ChangeEvents.changeEventForLocalReplace(ns, documentId, document, false); } /** @@ -1705,14 +1702,17 @@ public void syncDocumentsFromRemote( final BsonValue... documentIds ) { this.waitUntilInitialized(); + boolean added = false; try { ongoingOperationsGroup.enter(); for (final BsonValue documentId : documentIds) { - syncConfig.addSynchronizedDocument(namespace, documentId); + added = syncConfig.addSynchronizedDocument(namespace, documentId) || added; } - triggerListeningToNamespace(namespace); + if (added) { + triggerListeningToNamespace(namespace); + } } finally { ongoingOperationsGroup.exit(); } @@ -1732,20 +1732,26 @@ public void desyncDocumentsFromRemote( this.waitUntilInitialized(); final Lock lock = this.syncConfig.getNamespaceConfig(namespace).getLock().writeLock(); lock.lock(); + boolean removed = false; try { ongoingOperationsGroup.enter(); + for (final BsonValue documentId : documentIds) { - syncConfig.removeSynchronizedDocument(namespace, documentId); + removed = syncConfig.removeSynchronizedDocument(namespace, documentId) || removed; } - getLocalCollection(namespace).deleteMany( - new Document("_id", new Document("$in", Arrays.asList(documentIds)))); + if (removed) { + getLocalCollection(namespace).deleteMany( + new Document("_id", new Document("$in", Arrays.asList(documentIds)))); + } } finally { lock.unlock(); ongoingOperationsGroup.exit(); } - triggerListeningToNamespace(namespace); + if (removed) { + triggerListeningToNamespace(namespace); + } } /** @@ -1932,8 +1938,8 @@ void insertOne(final MongoNamespace namespace, final BsonDocument document) { try { getLocalCollection(namespace).insertOne(docForStorage); documentId = BsonUtils.getDocumentId(docForStorage); - event = changeEventForLocalInsert(namespace, docForStorage, true); - final CoreDocumentSynchronizationConfig config = syncConfig.addSynchronizedDocument( + event = ChangeEvents.changeEventForLocalInsert(namespace, docForStorage, true); + final CoreDocumentSynchronizationConfig config = syncConfig.addAndGetSynchronizedDocument( namespace, documentId ); @@ -1975,8 +1981,8 @@ void insertMany(final MongoNamespace namespace, for (final BsonDocument document : docsForStorage) { final BsonValue documentId = BsonUtils.getDocumentId(document); final ChangeEvent event = - changeEventForLocalInsert(namespace, document, true); - final CoreDocumentSynchronizationConfig config = syncConfig.addSynchronizedDocument( + ChangeEvents.changeEventForLocalInsert(namespace, document, true); + final CoreDocumentSynchronizationConfig config = syncConfig.addAndGetSynchronizedDocument( namespace, documentId ); @@ -2083,15 +2089,15 @@ UpdateResult updateOne( // else this is an update if (documentBeforeUpdate == null && updateOptions.isUpsert()) { triggerNamespace = true; - config = syncConfig.addSynchronizedDocument(namespace, documentId); - event = changeEventForLocalInsert(namespace, documentAfterUpdate, true); + config = syncConfig.addAndGetSynchronizedDocument(namespace, documentId); + event = ChangeEvents.changeEventForLocalInsert(namespace, documentAfterUpdate, true); } else { triggerNamespace = false; config = syncConfig.getSynchronizedDocument(namespace, documentId); - event = changeEventForLocalUpdate( + event = ChangeEvents.changeEventForLocalUpdate( namespace, BsonUtils.getDocumentId(documentAfterUpdate), - ChangeEvent.UpdateDescription.diff(documentBeforeUpdate, documentAfterUpdate), + UpdateDescription.diff(documentBeforeUpdate, documentAfterUpdate), documentAfterUpdate, true); } @@ -2218,14 +2224,14 @@ UpdateResult updateMany( // 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); - event = changeEventForLocalInsert(namespace, afterDocument, true); + config = syncConfig.addAndGetSynchronizedDocument(namespace, documentId); + event = ChangeEvents.changeEventForLocalInsert(namespace, afterDocument, true); } else { config = syncConfig.getSynchronizedDocument(namespace, documentId); - event = changeEventForLocalUpdate( + event = ChangeEvents.changeEventForLocalUpdate( namespace, documentId, - ChangeEvent.UpdateDescription.diff(beforeDocument, afterDocument), + UpdateDescription.diff(beforeDocument, afterDocument), afterDocument, true); } @@ -2293,13 +2299,13 @@ private void updateOrUpsertOneFromResolution( docForStorage, new FindOneAndReplaceOptions().upsert(true).returnDocument(ReturnDocument.AFTER)); - if (remoteEvent.getOperationType() == ChangeEvent.OperationType.DELETE) { - event = changeEventForLocalInsert(namespace, documentAfterUpdate, true); + if (remoteEvent.getOperationType() == OperationType.DELETE) { + event = ChangeEvents.changeEventForLocalInsert(namespace, documentAfterUpdate, true); } else { - event = changeEventForLocalUpdate( + event = ChangeEvents.changeEventForLocalUpdate( namespace, documentId, - ChangeEvent.UpdateDescription.diff( + UpdateDescription.diff( sanitizeDocument(remoteEvent.getFullDocument()), documentAfterUpdate), docForStorage, @@ -2366,7 +2372,7 @@ private void replaceOrUpsertOneFromRemote( undoCollection.deleteOne(getDocumentIdFilter(documentId)); } - event = changeEventForLocalReplace(namespace, documentId, docForStorage, false); + event = ChangeEvents.changeEventForLocalReplace(namespace, documentId, docForStorage, false); } finally { lock.unlock(); } @@ -2413,12 +2419,12 @@ DeleteResult deleteOne(final MongoNamespace namespace, final Bson filter) { undoCollection.insertOne(docToDelete); result = localCollection.deleteOne(filter); - event = changeEventForLocalDelete(namespace, documentId, true); + event = ChangeEvents.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) { + == OperationType.INSERT) { desyncDocumentsFromRemote(config.getNamespace(), config.getDocumentId()); undoCollection.deleteOne(getDocumentIdFilter(config.getDocumentId())); return result; @@ -2480,12 +2486,12 @@ public BsonValue apply(@NonNull final BsonDocument bsonDocument) { } final ChangeEvent event = - changeEventForLocalDelete(namespace, documentId, true); + ChangeEvents.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) { + == OperationType.INSERT) { desyncDocumentsFromRemote(config.getNamespace(), config.getDocumentId()); undoCollection.deleteOne(getDocumentIdFilter(documentId)); continue; @@ -2539,7 +2545,7 @@ private void deleteOneFromResolution( } localCollection .deleteOne(getDocumentIdFilter(documentId)); - event = changeEventForLocalDelete(namespace, documentId, true); + event = ChangeEvents.changeEventForLocalDelete(namespace, documentId, true); config.setSomePendingWrites( logicalT, atVersion, event); if (documentToDelete != null) { @@ -2586,7 +2592,7 @@ private void deleteOneFromRemote( } finally { lock.unlock(); } - emitEvent(documentId, changeEventForLocalDelete(namespace, documentId, false)); + emitEvent(documentId, ChangeEvents.changeEventForLocalDelete(namespace, documentId, false)); } private void triggerListeningToNamespace(final MongoNamespace namespace) { @@ -2659,7 +2665,7 @@ public Object call() { if (namespaceListener.getEventListener() != null) { namespaceListener.getEventListener().onEvent( documentId, - ChangeEvent.transformChangeEventForUser( + ChangeEvents.transformChangeEventForUser( event, namespaceListener.getDocumentCodec())); } } catch (final Exception ex) { diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceChangeStreamListener.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceChangeStreamListener.java index 0217b4ab3..b29bb91ed 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceChangeStreamListener.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceChangeStreamListener.java @@ -18,6 +18,7 @@ import com.mongodb.MongoNamespace; import com.mongodb.stitch.core.internal.common.Callback; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import java.util.Map; diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceChangeStreamListenerImpl.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceChangeStreamListenerImpl.java index 69cb2923a..630859560 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceChangeStreamListenerImpl.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceChangeStreamListenerImpl.java @@ -21,6 +21,7 @@ import com.mongodb.stitch.core.internal.common.Callback; import com.mongodb.stitch.core.internal.net.NetworkMonitor; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import java.util.HashMap; import java.util.Map; diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceSynchronizationConfig.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceSynchronizationConfig.java index caf06d691..5ec951b55 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceSynchronizationConfig.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/InstanceSynchronizationConfig.java @@ -130,18 +130,27 @@ public CoreDocumentSynchronizationConfig getSynchronizedDocument( return getNamespaceConfig(namespace).getSynchronizedDocument(documentId); } - public CoreDocumentSynchronizationConfig addSynchronizedDocument( + public boolean addSynchronizedDocument( final MongoNamespace namespace, final BsonValue documentId ) { - return getNamespaceConfig(namespace).addSynchronizedDocument(namespace, documentId); + return getNamespaceConfig(namespace).addSynchronizedDocument(documentId); } - public void removeSynchronizedDocument( + public CoreDocumentSynchronizationConfig addAndGetSynchronizedDocument( final MongoNamespace namespace, final BsonValue documentId ) { - getNamespaceConfig(namespace).removeSynchronizedDocument(documentId); + final NamespaceSynchronizationConfig nsConfig = getNamespaceConfig(namespace); + nsConfig.addSynchronizedDocument(documentId); + return nsConfig.getSynchronizedDocument(documentId); + } + + public boolean removeSynchronizedDocument( + final MongoNamespace namespace, + final BsonValue documentId + ) { + return getNamespaceConfig(namespace).removeSynchronizedDocument(documentId); } /** diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListener.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListener.java index e2ca967f1..ef51886e3 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListener.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceChangeStreamListener.java @@ -25,6 +25,8 @@ import com.mongodb.stitch.core.internal.net.StitchEvent; import com.mongodb.stitch.core.internal.net.Stream; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.internal.ResultDecoders; import java.io.Closeable; import java.io.IOException; @@ -43,10 +45,14 @@ import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.Document; +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.Codec; import org.bson.diagnostics.Logger; import org.bson.diagnostics.Loggers; public class NamespaceChangeStreamListener implements Closeable { + private static final Codec BSON_DOCUMENT_CODEC = new BsonDocumentCodec(); + private final MongoNamespace namespace; private final NamespaceSynchronizationConfig nsConfig; private final CoreStitchServiceClient service; @@ -212,10 +218,10 @@ boolean openStream() throws InterruptedException, IOException { args.put("ids", idsToWatch); currentStream = - service.streamFunction( - "watch", - Collections.singletonList(args), - ChangeEvent.changeEventCoder); + service.streamFunction( + "watch", + Collections.singletonList(args), + ResultDecoders.changeEventDecoder(BSON_DOCUMENT_CODEC)); if (currentStream != null && currentStream.isOpen()) { this.nsConfig.setStale(true); diff --git a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfig.java b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfig.java index be7f56d18..2b570e2b4 100644 --- a/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfig.java +++ b/core/services/mongodb-remote/src/main/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfig.java @@ -23,7 +23,6 @@ import com.mongodb.MongoNamespace; import com.mongodb.client.DistinctIterable; import com.mongodb.client.MongoCollection; -import com.mongodb.client.model.ReplaceOptions; import com.mongodb.stitch.core.services.mongodb.remote.sync.ChangeEventListener; import com.mongodb.stitch.core.services.mongodb.remote.sync.ConflictHandler; @@ -212,46 +211,41 @@ Codec getDocumentCodec() { return documentCodec; } - CoreDocumentSynchronizationConfig addSynchronizedDocument( - final MongoNamespace namespace, + boolean addSynchronizedDocument( final BsonValue documentId ) { final CoreDocumentSynchronizationConfig newConfig; final CoreDocumentSynchronizationConfig existingConfig = getSynchronizedDocument(documentId); - if (existingConfig == null) { - newConfig = new CoreDocumentSynchronizationConfig( - docsColl, - namespace, - documentId); - } else { - newConfig = new CoreDocumentSynchronizationConfig( - docsColl, - existingConfig); + if (existingConfig != null) { + return false; } + newConfig = new CoreDocumentSynchronizationConfig(docsColl, namespace, documentId); + nsLock.writeLock().lock(); try { - docsColl.replaceOne( - getDocFilter(newConfig.getNamespace(), newConfig.getDocumentId()), - newConfig, - new ReplaceOptions().upsert(true)); + docsColl.insertOne(newConfig); syncedDocuments.put(documentId, newConfig); - return newConfig; + return true; } finally { nsLock.writeLock().unlock(); } } - public void removeSynchronizedDocument(final BsonValue documentId) { + public boolean removeSynchronizedDocument(final BsonValue documentId) { nsLock.writeLock().lock(); try { - docsColl.deleteOne(getDocFilter(namespace, documentId)); - syncedDocuments.remove(documentId); + if (syncedDocuments.containsKey(documentId)) { + docsColl.deleteOne(getDocFilter(namespace, documentId)); + syncedDocuments.remove(documentId); + return true; + } } finally { nsLock.writeLock().unlock(); } + return false; } public ConflictHandler getConflictHandler() { 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..444f54d51 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 @@ -34,7 +34,9 @@ import com.mongodb.MongoNamespace; import com.mongodb.stitch.core.internal.common.BsonUtils; import com.mongodb.stitch.core.internal.common.CollectionDecoder; +import com.mongodb.stitch.core.internal.net.Stream; import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; @@ -43,13 +45,17 @@ import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.CoreRemoteClientFactory; import com.mongodb.stitch.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; + import org.bson.BsonDocument; import org.bson.BsonInt32; import org.bson.BsonInt64; @@ -58,8 +64,10 @@ import org.bson.BsonValue; import org.bson.Document; import org.bson.codecs.Decoder; +import org.bson.codecs.DocumentCodec; import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.configuration.CodecRegistry; +import org.bson.types.ObjectId; import org.junit.After; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -604,4 +612,91 @@ public void testUpdateMany() { assertThrows(() -> coll.updateMany(new Document(), new Document()), IllegalArgumentException.class); } + + @Test + @SuppressWarnings("unchecked") + public void testWatchBsonValueIDs() throws IOException, InterruptedException { + final CoreStitchServiceClient service = Mockito.mock(CoreStitchServiceClient.class); + when(service.getCodecRegistry()).thenReturn(BsonUtils.DEFAULT_CODEC_REGISTRY); + final CoreRemoteMongoClient client = + CoreRemoteClientFactory.getClient( + service, + getClientInfo(), + ServerEmbeddedMongoClientFactory.getInstance()); + final CoreRemoteMongoCollection coll = getCollection(client); + + final Stream> mockStream = Mockito.mock(Stream.class); + + doReturn(mockStream).when(service).streamFunction(any(), any(), any(Decoder.class)); + + final BsonValue[] expectedIDs = new BsonValue[] {new BsonString("foobar"), + new BsonObjectId(), + new BsonDocument()}; + coll.watch(expectedIDs); + + final ArgumentCaptor funcNameArg = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor funcArgsArg = ArgumentCaptor.forClass(List.class); + final ArgumentCaptor> decoderArgumentCaptor = + ArgumentCaptor.forClass(Decoder.class); + verify(service) + .streamFunction( + funcNameArg.capture(), + funcArgsArg.capture(), + decoderArgumentCaptor.capture()); + + assertEquals("watch", funcNameArg.getValue()); + assertEquals(1, funcArgsArg.getValue().size()); + final Document expectedArgs = new Document(); + expectedArgs.put("database", "dbName1"); + expectedArgs.put("collection", "collName1"); + expectedArgs.put("ids", new HashSet<>(Arrays.asList(expectedIDs))); + assertEquals(expectedArgs, funcArgsArg.getValue().get(0)); + assertEquals(ResultDecoders.changeEventDecoder(new DocumentCodec()), + decoderArgumentCaptor.getValue()); + } + + @Test + @SuppressWarnings("unchecked") + public void testWatchObjectIdIDs() throws IOException, InterruptedException { + final CoreStitchServiceClient service = Mockito.mock(CoreStitchServiceClient.class); + when(service.getCodecRegistry()).thenReturn(BsonUtils.DEFAULT_CODEC_REGISTRY); + final CoreRemoteMongoClient client = + CoreRemoteClientFactory.getClient( + service, + getClientInfo(), + ServerEmbeddedMongoClientFactory.getInstance()); + final CoreRemoteMongoCollection coll = getCollection(client); + + final Stream> mockStream = Mockito.mock(Stream.class); + + doReturn(mockStream).when(service).streamFunction(any(), any(), any(Decoder.class)); + + final ObjectId[] expectedIDs = new ObjectId[] {new ObjectId(), new ObjectId(), new ObjectId()}; + coll.watch(expectedIDs); + + final ArgumentCaptor funcNameArg = ArgumentCaptor.forClass(String.class); + final ArgumentCaptor funcArgsArg = ArgumentCaptor.forClass(List.class); + final ArgumentCaptor> decoderArgumentCaptor = + ArgumentCaptor.forClass(Decoder.class); + verify(service) + .streamFunction( + funcNameArg.capture(), + funcArgsArg.capture(), + decoderArgumentCaptor.capture()); + + assertEquals("watch", funcNameArg.getValue()); + assertEquals(1, funcArgsArg.getValue().size()); + final Document expectedArgs = new Document(); + expectedArgs.put("database", "dbName1"); + expectedArgs.put("collection", "collName1"); + expectedArgs.put("ids", Arrays.stream(expectedIDs).map(BsonObjectId::new) + .collect(Collectors.toSet())); + + for (final Map.Entry entry : expectedArgs.entrySet()) { + final Object capturedValue = ((Document)funcArgsArg.getValue().get(0)).get(entry.getKey()); + assertEquals(entry.getValue(), capturedValue); + } + assertEquals(ResultDecoders.changeEventDecoder(new DocumentCodec()), + decoderArgumentCaptor.getValue()); + } } 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 5624b625b..bba70793d 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 @@ -2,7 +2,9 @@ package com.mongodb.stitch.core.services.mongodb.remote.sync.internal import com.mongodb.MongoNamespace import com.mongodb.client.MongoCollection -import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.ChangeEvent.UpdateDescription.diff +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent +import com.mongodb.stitch.core.services.mongodb.remote.OperationType +import com.mongodb.stitch.core.services.mongodb.remote.UpdateDescription import org.bson.BsonArray import org.bson.BsonBoolean import org.bson.BsonDocument @@ -24,21 +26,21 @@ class ChangeEventUnitTests { fun testNew() { val expectedFullDocument = BsonDocument("foo", BsonString("bar")).append("_id", BsonObjectId()) val expectedId = BsonDocument("_id", expectedFullDocument["_id"]) - val expectedOperationType = ChangeEvent.OperationType.INSERT + val expectedOperationType = OperationType.INSERT val expectedNamespace = namespace val expectedDocumentKey = BsonDocument("_id", expectedFullDocument["_id"]) - val expectedUpdateDescription = ChangeEvent.UpdateDescription( + val expectedUpdateDescription = UpdateDescription( BsonDocument("foo", BsonString("bar")), listOf("baz")) val changeEvent = ChangeEvent( - expectedId, - expectedOperationType, - expectedFullDocument, - expectedNamespace, - expectedDocumentKey, - expectedUpdateDescription, - true + expectedId, + expectedOperationType, + expectedFullDocument, + expectedNamespace, + expectedDocumentKey, + expectedUpdateDescription, + true ) assertEquals(expectedId, changeEvent.id) @@ -53,56 +55,56 @@ class ChangeEventUnitTests { @Test fun testOperationTypeFromRemote() { assertEquals( - ChangeEvent.OperationType.INSERT, - ChangeEvent.OperationType.fromRemote("insert")) + OperationType.INSERT, + OperationType.fromRemote("insert")) assertEquals( - ChangeEvent.OperationType.UPDATE, - ChangeEvent.OperationType.fromRemote("update")) + OperationType.UPDATE, + OperationType.fromRemote("update")) assertEquals( - ChangeEvent.OperationType.REPLACE, - ChangeEvent.OperationType.fromRemote("replace")) + OperationType.REPLACE, + OperationType.fromRemote("replace")) assertEquals( - ChangeEvent.OperationType.DELETE, - ChangeEvent.OperationType.fromRemote("delete")) + OperationType.DELETE, + OperationType.fromRemote("delete")) assertEquals( - ChangeEvent.OperationType.UNKNOWN, - ChangeEvent.OperationType.fromRemote("bad")) + OperationType.UNKNOWN, + OperationType.fromRemote("bad")) } @Test fun testOperationTypeToRemote() { - assertEquals("insert", ChangeEvent.OperationType.INSERT.toRemote()) - assertEquals("update", ChangeEvent.OperationType.UPDATE.toRemote()) - assertEquals("replace", ChangeEvent.OperationType.REPLACE.toRemote()) - assertEquals("delete", ChangeEvent.OperationType.DELETE.toRemote()) - assertEquals("unknown", ChangeEvent.OperationType.UNKNOWN.toRemote()) + assertEquals("insert", OperationType.INSERT.toRemote()) + assertEquals("update", OperationType.UPDATE.toRemote()) + assertEquals("replace", OperationType.REPLACE.toRemote()) + assertEquals("delete", OperationType.DELETE.toRemote()) + assertEquals("unknown", OperationType.UNKNOWN.toRemote()) } @Test fun testToBsonDocumentRoundTrip() { val expectedFullDocument = BsonDocument("foo", BsonString("bar")).append("_id", BsonObjectId()) val expectedId = BsonDocument("_id", expectedFullDocument["_id"]) - val expectedOperationType = ChangeEvent.OperationType.INSERT + val expectedOperationType = OperationType.INSERT val expectedNamespace = namespace val expectedDocumentKey = BsonDocument("_id", expectedFullDocument["_id"]) - val expectedUpdateDescription = ChangeEvent.UpdateDescription( + val expectedUpdateDescription = UpdateDescription( BsonDocument("foo", BsonString("bar")), listOf("baz")) val changeEvent = ChangeEvent( - expectedId, - expectedOperationType, - expectedFullDocument, - expectedNamespace, - expectedDocumentKey, - expectedUpdateDescription, - true) + expectedId, + expectedOperationType, + expectedFullDocument, + expectedNamespace, + expectedDocumentKey, + expectedUpdateDescription, + true) - val changeEventDocument = ChangeEvent.toBsonDocument(changeEvent) + val changeEventDocument = changeEvent.toBsonDocument() assertEquals(expectedFullDocument, changeEventDocument["fullDocument"]) assertEquals(expectedId, changeEventDocument["_id"]) @@ -333,7 +335,7 @@ class ChangeEventUnitTests { val updatedFields = BsonDocument("hi", BsonString("there")) val removedFields = listOf("meow", "bark") - val updateDoc = ChangeEvent.UpdateDescription( + val updateDoc = UpdateDescription( updatedFields, removedFields ).toUpdateDocument() @@ -349,7 +351,7 @@ class ChangeEventUnitTests { afterDocument: BsonDocument ) { // create an update description via diff'ing the two documents. - val updateDescription = diff(withoutId(beforeDocument), withoutId(afterDocument)) + val updateDescription = UpdateDescription.diff(withoutId(beforeDocument), withoutId(afterDocument)) assertEquals( expectedUpdateDocument, diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfigUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfigUnitTests.kt index 9bee44ff6..bf2b3290e 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfigUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/CoreDocumentSynchronizationConfigUnitTests.kt @@ -94,7 +94,7 @@ class CoreDocumentSynchronizationConfigUnitTests { fun testToBsonDocumentRoundTrip() { var config = CoreDocumentSynchronizationConfig(coll, namespace, id) val expectedTestVersion = BsonDocument("dummy", BsonString("version")) - val expectedEvent = ChangeEvent.changeEventForLocalDelete(namespace, id, false) + val expectedEvent = ChangeEvents.changeEventForLocalDelete(namespace, id, false) config.setSomePendingWrites( 1, expectedTestVersion, 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 0ac364af5..7a0976255 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 @@ -6,6 +6,7 @@ import com.mongodb.client.MongoCollection import com.mongodb.client.result.DeleteResult import com.mongodb.client.result.UpdateResult import com.mongodb.stitch.core.internal.net.Event +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollectionImpl @@ -70,6 +71,11 @@ interface DataSynchronizerTestContext : Closeable { */ fun reconfigure() + /** + * Wait for the data synchronizer to open streams. + */ + fun waitForDataSynchronizerStreams() + /** * Wait for an error to be emitted. */ 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 1a60f9ee6..0dbadaaa9 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 @@ -6,6 +6,7 @@ import com.mongodb.stitch.core.StitchServiceErrorCode import com.mongodb.stitch.core.StitchServiceException import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult +import com.mongodb.stitch.core.services.mongodb.remote.UpdateDescription import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteFindIterable import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteFindIterableImpl import com.mongodb.stitch.core.services.mongodb.remote.sync.internal.SyncUnitTestHarness.Companion.withoutSyncVersion @@ -130,7 +131,7 @@ class DataSynchronizerUnitTests { ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalInsert( + ChangeEvents.changeEventForLocalInsert( ctx.namespace, ctx.testDocument, true)) @@ -138,7 +139,7 @@ class DataSynchronizerUnitTests { ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalInsert( + ChangeEvents.changeEventForLocalInsert( ctx.namespace, ctx.testDocument, false)) @@ -172,11 +173,11 @@ class DataSynchronizerUnitTests { ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) + ChangeEvents.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) ctx.verifyConflictHandlerCalledForActiveDoc( times = 1, - expectedLocalConflictEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true), - expectedRemoteConflictEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) + expectedLocalConflictEvent = ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true), + expectedRemoteConflictEvent = ChangeEvents.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) ctx.verifyErrorListenerCalledForActiveDoc(times = 0) assertNull(ctx.findTestDocumentFromLocalCollection()) @@ -194,11 +195,11 @@ class DataSynchronizerUnitTests { ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) + ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) ctx.verifyConflictHandlerCalledForActiveDoc( times = 1, - expectedLocalConflictEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true), - expectedRemoteConflictEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) + expectedLocalConflictEvent = ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true), + expectedRemoteConflictEvent = ChangeEvents.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false)) ctx.verifyErrorListenerCalledForActiveDoc(times = 0) assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection()) @@ -274,10 +275,10 @@ class DataSynchronizerUnitTests { ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalUpdate( + ChangeEvents.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, - ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(3)), Collections.emptyList()), + UpdateDescription(BsonDocument("count", BsonInt32(3)), Collections.emptyList()), expectedDocument, false ) @@ -365,14 +366,14 @@ class DataSynchronizerUnitTests { ctx.doSyncPass() ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, - ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) ctx.updateTestDocument() ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc(1, - ChangeEvent.changeEventForLocalUpdate( + ChangeEvents.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, - ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), + UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), docAfterUpdate, true )) @@ -382,10 +383,10 @@ class DataSynchronizerUnitTests { ctx.mockUpdateResult(RemoteUpdateResult(1, 1, null)) ctx.doSyncPass() ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalUpdate( + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvents.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, - ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), + UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), docAfterUpdate, false )) @@ -393,7 +394,7 @@ class DataSynchronizerUnitTests { verify(ctx.collectionMock, times(1)).updateOne(any(), docCaptor.capture()) // create what we expect the diff to look like - val expectedDiff = ChangeEvent.UpdateDescription.diff( + val expectedDiff = UpdateDescription.diff( BsonDocument.parse(ctx.testDocument.toJson()), docAfterUpdate).toUpdateDocument() expectedDiff.remove("\$unset") @@ -417,10 +418,10 @@ class DataSynchronizerUnitTests { var ctx = harness.freshTestContext() // setup our expectations var docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) - var expectedLocalEvent = ChangeEvent.changeEventForLocalUpdate( + var expectedLocalEvent = ChangeEvents.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, - ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), + UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), docAfterUpdate, true) @@ -455,13 +456,13 @@ class DataSynchronizerUnitTests { // reset (delete, insert, sync) ctx = harness.freshTestContext() docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) - expectedLocalEvent = ChangeEvent.changeEventForLocalUpdate( + expectedLocalEvent = ChangeEvents.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, - ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), + UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), docAfterUpdate, true) - var expectedRemoteEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false) + var expectedRemoteEvent = ChangeEvents.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false) ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) ctx.insertTestDocument() @@ -470,7 +471,7 @@ class DataSynchronizerUnitTests { ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) // update the document and wait for the local update event ctx.updateTestDocument() @@ -486,7 +487,7 @@ class DataSynchronizerUnitTests { // and no errors were emitted ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalInsert(ctx.namespace, docAfterUpdate, true)) + ChangeEvents.changeEventForLocalInsert(ctx.namespace, docAfterUpdate, true)) ctx.verifyConflictHandlerCalledForActiveDoc(1, expectedLocalEvent, expectedRemoteEvent) ctx.verifyErrorListenerCalledForActiveDoc(0) @@ -500,13 +501,13 @@ class DataSynchronizerUnitTests { // reset (delete, insert, sync) ctx = harness.freshTestContext() docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) - expectedLocalEvent = ChangeEvent.changeEventForLocalUpdate( + expectedLocalEvent = ChangeEvents.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, - ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), + UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), docAfterUpdate, true) - expectedRemoteEvent = ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false) + expectedRemoteEvent = ChangeEvents.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, false) ctx.mockUpdateResult(RemoteUpdateResult(0, 0, null)) @@ -515,7 +516,7 @@ class DataSynchronizerUnitTests { ctx.doSyncPass() ctx.waitForEvents() ctx.verifyChangeEventListenerCalledForActiveDoc( - 1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + 1, ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) ctx.doSyncPass() // update the reset doc @@ -567,10 +568,10 @@ class DataSynchronizerUnitTests { val ctx = harness.freshTestContext() // set up expectations and insert val docAfterUpdate = BsonDocument("count", BsonInt32(2)).append("_id", ctx.testDocumentId) - val expectedEvent = ChangeEvent.changeEventForLocalUpdate( + val expectedEvent = ChangeEvents.changeEventForLocalUpdate( ctx.namespace, ctx.testDocument["_id"], - ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), + UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), docAfterUpdate, true ) @@ -594,7 +595,7 @@ class DataSynchronizerUnitTests { verify(ctx.collectionMock, times(1)).updateOne(any(), docCaptor.capture()) // create what we expect the diff to look like - val expectedDiff = ChangeEvent.UpdateDescription.diff( + val expectedDiff = UpdateDescription.diff( BsonDocument.parse(ctx.testDocument.toJson()), expectedEvent.fullDocument).toUpdateDocument() expectedDiff.remove("\$unset") @@ -626,17 +627,17 @@ class DataSynchronizerUnitTests { // have been reflected w/ and w/o pending writes ctx.insertTestDocument() ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true)) ctx.doSyncPass() ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) // delete the document and wait ctx.deleteTestDocument() ctx.waitForEvents() // verify a delete event with pending writes is called - ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalDelete( + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvents.changeEventForLocalDelete( ctx.namespace, ctx.testDocument["_id"], true)) @@ -649,7 +650,7 @@ class DataSynchronizerUnitTests { val docCaptor = ArgumentCaptor.forClass(BsonDocument::class.java) verify(ctx.collectionMock, times(1)).deleteOne(docCaptor.capture()) assertEquals(ctx.testDocument["_id"], docCaptor.value["_id"]) - ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalDelete( + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvents.changeEventForLocalDelete( ctx.namespace, ctx.testDocument["_id"], false)) @@ -662,7 +663,7 @@ class DataSynchronizerUnitTests { fun testConflictedDelete() { var ctx = harness.freshTestContext() - var expectedLocalEvent = ChangeEvent.changeEventForLocalDelete( + var expectedLocalEvent = ChangeEvents.changeEventForLocalDelete( ctx.namespace, ctx.testDocument["_id"], true @@ -685,14 +686,14 @@ class DataSynchronizerUnitTests { ctx.doSyncPass() ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalReplace( + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvents.changeEventForLocalReplace( ctx.namespace, ctx.testDocumentId, ctx.testDocument, false )) ctx.verifyConflictHandlerCalledForActiveDoc(1, expectedLocalEvent, - ChangeEvent.changeEventForLocalUpdate(ctx.namespace, ctx.testDocumentId, null, ctx.testDocument, false)) + ChangeEvents.changeEventForLocalUpdate(ctx.namespace, ctx.testDocumentId, null, ctx.testDocument, false)) ctx.verifyErrorListenerCalledForActiveDoc(0) assertEquals( @@ -702,7 +703,7 @@ class DataSynchronizerUnitTests { // 2: Local wins ctx = harness.freshTestContext() - expectedLocalEvent = ChangeEvent.changeEventForLocalDelete( + expectedLocalEvent = ChangeEvents.changeEventForLocalDelete( ctx.namespace, ctx.testDocument["_id"], true @@ -724,14 +725,14 @@ class DataSynchronizerUnitTests { ctx.doSyncPass() ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalDelete( + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvents.changeEventForLocalDelete( ctx.namespace, ctx.testDocumentId, true )) ctx.verifyConflictHandlerCalledForActiveDoc(1, - ChangeEvent.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, true), - ChangeEvent.changeEventForLocalUpdate( + ChangeEvents.changeEventForLocalDelete(ctx.namespace, ctx.testDocumentId, true), + ChangeEvents.changeEventForLocalUpdate( ctx.namespace, ctx.testDocumentId, null, ctx.testDocument, false )) ctx.verifyErrorListenerCalledForActiveDoc(0) @@ -743,7 +744,7 @@ class DataSynchronizerUnitTests { fun testFailedDelete() { val ctx = harness.freshTestContext() - val expectedEvent = ChangeEvent.changeEventForLocalDelete( + val expectedEvent = ChangeEvents.changeEventForLocalDelete( ctx.namespace, ctx.testDocument["_id"], true @@ -844,7 +845,7 @@ class DataSynchronizerUnitTests { ctx.insertTestDocument() - val expectedEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true) + val expectedEvent = ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true) ctx.deleteTestDocument() @@ -867,8 +868,8 @@ class DataSynchronizerUnitTests { ctx.dataSynchronizer.insertMany(ctx.namespace, listOf(doc1, doc2)) - val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1, true) - val expectedEvent2 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc2, true) + val expectedEvent1 = ChangeEvents.changeEventForLocalInsert(ctx.namespace, doc1, true) + val expectedEvent2 = ChangeEvents.changeEventForLocalInsert(ctx.namespace, doc2, true) ctx.waitForEvents(amount = 2) @@ -931,8 +932,8 @@ class DataSynchronizerUnitTests { assertTrue(updateResult.wasAcknowledged()) ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalUpdate( - ctx.namespace, ctx.testDocumentId, ChangeEvent.UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), expectedDocumentAfterUpdate, true)) + ChangeEvents.changeEventForLocalUpdate( + ctx.namespace, ctx.testDocumentId, UpdateDescription(BsonDocument("count", BsonInt32(2)), listOf()), expectedDocumentAfterUpdate, true)) // assert that the updated document equals what we've expected assertEquals(ctx.testDocument["_id"], ctx.findTestDocumentFromLocalCollection()?.get("_id")) assertEquals(expectedDocumentAfterUpdate, ctx.findTestDocumentFromLocalCollection()!!) @@ -959,7 +960,7 @@ class DataSynchronizerUnitTests { assertEquals(1, result.modifiedCount) assertNotNull(result.upsertedId) - val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, + val expectedEvent1 = ChangeEvents.changeEventForLocalInsert(ctx.namespace, doc1.append("_id", result.upsertedId), true) ctx.waitForEvents(amount = 1) @@ -1010,9 +1011,9 @@ class DataSynchronizerUnitTests { ctx.dataSynchronizer.insertMany(ctx.namespace, listOf(doc1, doc2, doc3)) - val expectedEvent1 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc1, true) - val expectedEvent2 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc2, true) - val expectedEvent3 = ChangeEvent.changeEventForLocalInsert(ctx.namespace, doc3, true) + val expectedEvent1 = ChangeEvents.changeEventForLocalInsert(ctx.namespace, doc1, true) + val expectedEvent2 = ChangeEvents.changeEventForLocalInsert(ctx.namespace, doc2, true) + val expectedEvent3 = ChangeEvents.changeEventForLocalInsert(ctx.namespace, doc3, true) ctx.waitForEvents(amount = 3) @@ -1046,19 +1047,19 @@ class DataSynchronizerUnitTests { expectedEvent1, expectedEvent2, expectedEvent3, - ChangeEvent.changeEventForLocalUpdate( + ChangeEvents.changeEventForLocalUpdate( ctx.namespace, doc1["_id"], - ChangeEvent.UpdateDescription( + UpdateDescription( BsonDocument("count", BsonInt32(2)), listOf() ), expectedDocAfterUpdate1, true), - ChangeEvent.changeEventForLocalUpdate( + ChangeEvents.changeEventForLocalUpdate( ctx.namespace, doc2["_id"], - ChangeEvent.UpdateDescription( + UpdateDescription( BsonDocument("count", BsonInt32(2)), listOf() ), @@ -1097,7 +1098,7 @@ class DataSynchronizerUnitTests { assertEquals(0, result.modifiedCount) assertNotNull(result.upsertedId) - val expectedEvent1 = ChangeEvent.changeEventForLocalInsert( + val expectedEvent1 = ChangeEvents.changeEventForLocalInsert( ctx.namespace, doc1.append("_id", result.upsertedId), true) @@ -1206,7 +1207,7 @@ class DataSynchronizerUnitTests { assertEquals(1, deleteResult.deletedCount) assertTrue(deleteResult.wasAcknowledged()) ctx.verifyChangeEventListenerCalledForActiveDoc(1, - ChangeEvent.changeEventForLocalDelete( + ChangeEvents.changeEventForLocalDelete( ctx.namespace, ctx.testDocumentId, true )) // assert that the updated document equals what we've expected @@ -1263,7 +1264,7 @@ class DataSynchronizerUnitTests { ctx.insertTestDocument() ctx.waitForEvents() - ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvent.changeEventForLocalInsert( + ctx.verifyChangeEventListenerCalledForActiveDoc(1, ChangeEvents.changeEventForLocalInsert( ctx.namespace, ctx.testDocument, true)) assertTrue(ctx.dataSynchronizer.isRunning) } @@ -1350,7 +1351,7 @@ class DataSynchronizerUnitTests { ctx.waitForEvents(1) ctx.verifyChangeEventListenerCalledForActiveDoc( 1, - ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) + ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, false)) // update the document and wait for the local update event ctx.updateTestDocument() @@ -1593,6 +1594,7 @@ class DataSynchronizerUnitTests { // which doesn't exist to resolve the conflict val findMock = mock(CoreRemoteFindIterableImpl::class.java) `when`(findMock.first()).thenReturn(pseudoUpdatedDocument) + @Suppress("UNCHECKED_CAST") `when`(ctx.collectionMock.find(any())).thenReturn(findMock as CoreRemoteFindIterable) // sync, creating a conflict. because remote has an empty version, // there will be a conflict on the next L2R pass that we will resolve @@ -1661,10 +1663,10 @@ class DataSynchronizerUnitTests { origCtx.setPendingWritesForDocId( testDocumentId, - ChangeEvent.changeEventForLocalUpdate( + ChangeEvents.changeEventForLocalUpdate( origCtx.namespace, testDocumentId, - ChangeEvent.UpdateDescription.diff(originalTestDocument, expectedNewDocument), + UpdateDescription.diff(originalTestDocument, expectedNewDocument), expectedNewDocument, true )) @@ -1738,7 +1740,7 @@ class DataSynchronizerUnitTests { origCtx.setPendingWritesForDocId( testDocumentId, - ChangeEvent.changeEventForLocalDelete( + ChangeEvents.changeEventForLocalDelete( origCtx.namespace, testDocumentId, true @@ -1856,6 +1858,7 @@ class DataSynchronizerUnitTests { assertFalse(ctx.dataSynchronizer.isRunning) } + @Test fun testMissingDocument() { val ctx = harness.freshTestContext() @@ -1865,12 +1868,16 @@ class DataSynchronizerUnitTests { ctx.dataSynchronizer.syncDocumentsFromRemote(ctx.namespace, ctx.testDocumentId) + ctx.waitForDataSynchronizerStreams() + ctx.doSyncPass() val mockEmptyFindResult = mock(CoreRemoteFindIterableImpl::class.java) + @Suppress("UNCHECKED_CAST") `when`(mockEmptyFindResult .into(any(MutableCollection::class.java as Class>))) - .thenReturn(HashSet()) + .thenReturn(HashSet()) + @Suppress("UNCHECKED_CAST") `when`(ctx.collectionMock.find(any())) .thenReturn(mockEmptyFindResult as CoreRemoteFindIterable) @@ -1891,12 +1898,16 @@ class DataSynchronizerUnitTests { ctx.dataSynchronizer.syncDocumentsFromRemote(ctx.namespace, ctx.testDocumentId) + ctx.waitForDataSynchronizerStreams() + ctx.doSyncPass() val mockEmptyFindResult = mock(CoreRemoteFindIterableImpl::class.java) + @Suppress("UNCHECKED_CAST") `when`(mockEmptyFindResult .into(any(MutableCollection::class.java as Class>))) - .thenReturn(HashSet()) + .thenReturn(HashSet()) + @Suppress("UNCHECKED_CAST") `when`(ctx.collectionMock.find(any())) .thenReturn(mockEmptyFindResult as CoreRemoteFindIterable) @@ -1922,9 +1933,11 @@ class DataSynchronizerUnitTests { ctx.doSyncPass() val mockEmptyFindResult = mock(CoreRemoteFindIterableImpl::class.java) + @Suppress("UNCHECKED_CAST") `when`(mockEmptyFindResult .into(any(MutableCollection::class.java as Class>))) - .thenReturn(HashSet()) + .thenReturn(HashSet()) + @Suppress("UNCHECKED_CAST") `when`(ctx.collectionMock.find(any())) .thenReturn(mockEmptyFindResult as CoreRemoteFindIterable) 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 1b6d5c227..b38807b8b 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 @@ -83,9 +83,9 @@ class NamespaceChangeStreamListenerUnitTests { // assert that, with an expected ChangeEvent, the event is stored // and the stream remains open - val expectedChangeEvent = ChangeEvent.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true) + val expectedChangeEvent = ChangeEvents.changeEventForLocalInsert(ctx.namespace, ctx.testDocument, true) ctx.nextStreamEvent = Event.Builder().withEventName("message").withData( - ChangeEvent.toBsonDocument(expectedChangeEvent).toJson() + expectedChangeEvent.toBsonDocument().toJson() ).build() namespaceChangeStreamListener.storeNextEvent() assertTrue(namespaceChangeStreamListener.isOpen) diff --git a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfigUnitTests.kt b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfigUnitTests.kt index 92c668bcb..3c084b1e7 100644 --- a/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfigUnitTests.kt +++ b/core/services/mongodb-remote/src/test/java/com/mongodb/stitch/core/services/mongodb/remote/sync/internal/NamespaceSynchronizationConfigUnitTests.kt @@ -14,6 +14,7 @@ import org.mockito.Mockito.mock class NamespaceSynchronizationConfigUnitTests { @Test + @Suppress("UNCHECKED_CAST") fun testToBsonDocumentRoundTrip() { val namespace = newNamespace() 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 2d1a17e4f..a36e0d6af 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 @@ -14,16 +14,19 @@ import com.mongodb.stitch.core.internal.net.NetworkMonitor import com.mongodb.stitch.core.internal.net.Stream import com.mongodb.stitch.core.services.internal.CoreStitchServiceClient import com.mongodb.stitch.core.services.internal.CoreStitchServiceClientImpl +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener +import com.mongodb.stitch.core.services.mongodb.remote.OperationType import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteFindIterable import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoClientImpl import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollectionImpl import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoDatabaseImpl +import com.mongodb.stitch.core.services.mongodb.remote.internal.ResultDecoders 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.server.services.mongodb.local.internal.ServerEmbeddedMongoClientFactory import org.bson.BsonDocument import org.bson.BsonInt32 @@ -200,8 +203,8 @@ class SyncUnitTestHarness : Closeable { private fun newErrorListener( emitErrorSemaphore: Semaphore? = null, expectedDocumentId: BsonValue? = null - ): ErrorListener { - open class TestErrorListener : ErrorListener { + ): ExceptionListener { + open class TestExceptionListener : ExceptionListener { override fun onError(actualDocumentId: BsonValue?, error: Exception?) { if (expectedDocumentId != null) { Assert.assertEquals(expectedDocumentId, actualDocumentId) @@ -210,7 +213,7 @@ class SyncUnitTestHarness : Closeable { emitErrorSemaphore?.release() } } - return Mockito.spy(TestErrorListener()) + return Mockito.spy(TestExceptionListener()) } private fun newConflictHandler( @@ -267,7 +270,7 @@ class SyncUnitTestHarness : Closeable { Mockito.mock(CoreRemoteMongoCollectionImpl::class.java) as CoreRemoteMongoCollectionImpl override var nextStreamEvent: Event = Event.Builder().withEventName("MOCK").build() - private val streamMock = Stream(TestEventStream(this), ChangeEvent.changeEventCoder) + private val streamMock = Stream(TestEventStream(this), ResultDecoders.changeEventDecoder(BsonDocumentCodec())) 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) } @@ -382,7 +385,7 @@ class SyncUnitTestHarness : Closeable { Mockito.`when`(service.streamFunction( ArgumentMatchers.anyString(), ArgumentMatchers.anyList(), - ArgumentMatchers.eq(ChangeEvent.changeEventCoder)) + ArgumentMatchers.eq(ResultDecoders.changeEventDecoder(BsonDocumentCodec()))) ).thenReturn(streamMock) val databaseSpy = Mockito.mock(CoreRemoteMongoDatabaseImpl::class.java) @@ -426,6 +429,15 @@ class SyncUnitTestHarness : Closeable { assertTrue(changeEventListener.emitEventSemaphore?.tryAcquire(10, TimeUnit.SECONDS) ?: true) } + override fun waitForDataSynchronizerStreams() { + waitLock.lock() + while (!dataSynchronizer.areAllStreamsOpen()) { + Thread.sleep(500) + } + waitLock.unlock() + assertTrue(dataSynchronizer.areAllStreamsOpen()) + } + override fun waitForError() { assertTrue(errorSemaphore?.tryAcquire(10, TimeUnit.SECONDS) ?: true) } @@ -486,7 +498,7 @@ class SyncUnitTestHarness : Closeable { override fun queueConsumableRemoteInsertEvent() { `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( - mapOf(testDocument to ChangeEvent.changeEventForLocalInsert(namespace, testDocument, true)), + mapOf(testDocument to ChangeEvents.changeEventForLocalInsert(namespace, testDocument, true)), mapOf()) } @@ -539,27 +551,27 @@ class SyncUnitTestHarness : Closeable { } } `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( - mapOf(document to ChangeEvent.changeEventForLocalUpdate( + mapOf(document to ChangeEvents.changeEventForLocalUpdate( namespace, id, null, fakeUpdateDoc, false)), mapOf()) } override fun queueConsumableRemoteDeleteEvent() { `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( - mapOf(testDocument to ChangeEvent.changeEventForLocalDelete(namespace, testDocumentId, true)), + mapOf(testDocument to ChangeEvents.changeEventForLocalDelete(namespace, testDocumentId, true)), mapOf()) } override fun queueConsumableRemoteUnknownEvent() { `when`(dataSynchronizer.getEventsForNamespace(any())).thenReturn( mapOf(testDocument to ChangeEvent( - BsonDocument("_id", testDocumentId), - ChangeEvent.OperationType.UNKNOWN, - testDocument, - namespace, - BsonDocument("_id", testDocumentId), - null, - true)), mapOf()) + BsonDocument("_id", testDocumentId), + OperationType.UNKNOWN, + testDocument, + namespace, + BsonDocument("_id", testDocumentId), + null, + true)), mapOf()) } override fun findTestDocumentFromLocalCollection(): BsonDocument? { @@ -630,7 +642,7 @@ class SyncUnitTestHarness : Closeable { override fun verifyWatchFunctionCalled(times: Int, expectedArgs: Document) { Mockito.verify(service, times(times)).streamFunction( - eq("watch"), eq(Collections.singletonList(expectedArgs)), eq(ChangeEvent.changeEventCoder)) + eq("watch"), eq(Collections.singletonList(expectedArgs)), eq(ResultDecoders.changeEventDecoder(BsonDocumentCodec()))) } override fun verifyStartCalled(times: Int) { 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 bfa419975..40e90aea7 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,8 +1,8 @@ package com.mongodb.stitch.core.testutils.sync +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener 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 @@ -21,7 +21,7 @@ interface ProxySyncMethods { fun configure( conflictResolver: ConflictHandler, changeEventListener: ChangeEventListener?, - errorListener: ErrorListener? + exceptionListener: ExceptionListener? ) fun syncOne(id: BsonValue) 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 4980735f6..64389f59a 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 @@ -7,12 +7,13 @@ import com.mongodb.stitch.core.admin.services.rules.RuleCreator import com.mongodb.stitch.core.admin.services.rules.rule import com.mongodb.stitch.core.internal.common.Callback import com.mongodb.stitch.core.internal.common.OperationResult +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent +import com.mongodb.stitch.core.services.mongodb.remote.ExceptionListener +import com.mongodb.stitch.core.services.mongodb.remote.OperationType 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 import org.bson.BsonElement @@ -901,7 +902,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { hasChangeEventListenerBeenInvoked = true changeEventListenerSemaphore.release() }, - ErrorListener { _, _ -> } + ExceptionListener { _, _ -> } ) waitForAllStreamsOpen() @@ -1020,7 +1021,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { coll.configure( failingConflictHandler, null, - ErrorListener { _, _ -> errorEmittedSem.release() }) + ExceptionListener { _, _ -> errorEmittedSem.release() }) remoteColl.insertOne(docToInsert) @@ -1206,7 +1207,7 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { // ensure that there is no version information in the event document. assertNoVersionFieldsInDoc(event.fullDocument) - if (event.operationType == ChangeEvent.OperationType.UPDATE && + if (event.operationType == OperationType.UPDATE && !event.hasUncommittedWrites()) { assertEquals( updateDoc["\$set"], @@ -1278,8 +1279,8 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { }, ChangeEventListener { _: BsonValue, _: ChangeEvent -> }, - ErrorListener { _, _ -> - }) + ExceptionListener { _, _ -> + }) // insert an initial doc val testDoc = Document("hello", "world") @@ -1913,12 +1914,12 @@ class SyncIntTestProxy(private val syncTestRunner: SyncIntTestRunner) { BsonDocument("_id", documentId) private val failingConflictHandler = ConflictHandler { _: BsonValue, event1: ChangeEvent, event2: ChangeEvent -> - val localEventDescription = when (event1.operationType == ChangeEvent.OperationType.DELETE) { + val localEventDescription = when (event1.operationType == OperationType.DELETE) { true -> "delete" false -> event1.fullDocument.toJson() } - val remoteEventDescription = when (event2.operationType == ChangeEvent.OperationType.DELETE) { + val remoteEventDescription = when (event2.operationType == OperationType.DELETE) { true -> "delete" false -> event2.fullDocument.toJson() } diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/PassthroughChangeStream.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/PassthroughChangeStream.java new file mode 100644 index 000000000..4a7b937e9 --- /dev/null +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/PassthroughChangeStream.java @@ -0,0 +1,56 @@ +/* + * 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; + +import com.mongodb.stitch.core.internal.net.StitchEvent; +import com.mongodb.stitch.core.internal.net.Stream; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeStream; + +import java.io.IOException; + +/** + * Simple {@link ChangeStream} implementation that unwraps and returns the same change event + * provided on the internal {@link StitchEvent}. + * @param The type of full document on the change event. + */ +public class PassthroughChangeStream extends + ChangeStream, DocumentT> { + /** + * Initializes a passthrough change stream with the provided underlying event stream. + * @param stream The event stream. + */ + public PassthroughChangeStream(final Stream> stream) { + super(stream); + } + + @Override + public ChangeEvent nextEvent() throws IOException { + final StitchEvent> nextEvent = getStream().nextEvent(); + + if (nextEvent == null) { + return null; + } + + if (nextEvent.getError() != null) { + dispatchError(nextEvent); + return null; + } + + return nextEvent.getData(); + } +} diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/RemoteMongoCollection.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/RemoteMongoCollection.java index 9ac548f93..faf5050db 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/RemoteMongoCollection.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/RemoteMongoCollection.java @@ -17,15 +17,22 @@ package com.mongodb.stitch.server.services.mongodb.remote; import com.mongodb.MongoNamespace; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeStream; import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertOneResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; + +import java.io.IOException; import java.util.List; + +import org.bson.BsonValue; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; +import org.bson.types.ObjectId; /** * The RemoteMongoCollection interface. @@ -239,4 +246,25 @@ RemoteUpdateResult updateMany( final Bson filter, final Bson update, final RemoteUpdateOptions updateOptions); + + /** + * Watches specified IDs in a collection. This convenience overload supports the use case + * of non-{@link BsonValue} instances of {@link ObjectId}. + * @param ids unique object identifiers of the IDs to watch. + * @return the stream of change events. + * @throws InterruptedException if the operation is interrupted. + * @throws IOException if the operation fails. + */ + ChangeStream, DocumentT> watch(final ObjectId... ids) + throws InterruptedException, IOException; + + /** + * Watches specified IDs in a collection. + * @param ids the ids to watch. + * @return the stream of change events. + * @throws InterruptedException if the operation is interrupted. + * @throws IOException if the operation fails. + */ + ChangeStream, DocumentT> watch(final BsonValue... ids) + throws InterruptedException, IOException; } diff --git a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/RemoteMongoCollectionImpl.java b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/RemoteMongoCollectionImpl.java index 310fb2064..a8dea5f79 100644 --- a/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/RemoteMongoCollectionImpl.java +++ b/server/services/mongodb-remote/src/main/java/com/mongodb/stitch/server/services/mongodb/remote/internal/RemoteMongoCollectionImpl.java @@ -17,6 +17,8 @@ package com.mongodb.stitch.server.services.mongodb.remote.internal; import com.mongodb.MongoNamespace; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeEvent; +import com.mongodb.stitch.core.services.mongodb.remote.ChangeStream; import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteDeleteResult; import com.mongodb.stitch.core.services.mongodb.remote.RemoteInsertManyResult; @@ -24,13 +26,18 @@ import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions; import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateResult; import com.mongodb.stitch.core.services.mongodb.remote.internal.CoreRemoteMongoCollection; +import com.mongodb.stitch.server.services.mongodb.remote.PassthroughChangeStream; import com.mongodb.stitch.server.services.mongodb.remote.RemoteAggregateIterable; import com.mongodb.stitch.server.services.mongodb.remote.RemoteFindIterable; import com.mongodb.stitch.server.services.mongodb.remote.RemoteMongoCollection; +import java.io.IOException; import java.util.List; + +import org.bson.BsonValue; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; +import org.bson.types.ObjectId; public final class RemoteMongoCollectionImpl implements RemoteMongoCollection { @@ -278,4 +285,26 @@ public RemoteUpdateResult updateMany( ) { return proxy.updateMany(filter, update, updateOptions); } + + /** + * Watches specified IDs in a collection. + * @param ids unique object identifiers of the IDs to watch. + * @return the stream of change events. + */ + @Override + public ChangeStream, DocumentT> watch(final ObjectId... ids) + throws InterruptedException, IOException { + return new PassthroughChangeStream<>(proxy.watch(ids)); + } + + /** + * Watches specified IDs in a collection. + * @param ids the ids to watch. + * @return the stream of change events. + */ + @Override + public ChangeStream, DocumentT> watch(final BsonValue... ids) + throws InterruptedException, IOException { + return new PassthroughChangeStream<>(proxy.watch(ids)); + } } diff --git a/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/RemoteMongoClientIntTests.kt b/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/RemoteMongoClientIntTests.kt index 848d07b4b..1d7792776 100644 --- a/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/RemoteMongoClientIntTests.kt +++ b/server/services/mongodb-remote/src/test/java/com/mongodb/stitch/server/services/mongodb/remote/RemoteMongoClientIntTests.kt @@ -2,7 +2,6 @@ package com.mongodb.stitch.server.services.mongodb.remote import com.mongodb.Block import com.mongodb.MongoNamespace -import com.mongodb.stitch.server.testutils.BaseStitchServerIntTest import com.mongodb.stitch.core.StitchServiceErrorCode import com.mongodb.stitch.core.StitchServiceException import com.mongodb.stitch.core.admin.authProviders.ProviderConfigs @@ -10,10 +9,16 @@ import com.mongodb.stitch.core.admin.services.ServiceConfigs import com.mongodb.stitch.core.admin.services.rules.RuleCreator import com.mongodb.stitch.core.auth.providers.anonymous.AnonymousCredential import com.mongodb.stitch.core.internal.common.BsonUtils +import com.mongodb.stitch.core.services.mongodb.remote.OperationType import com.mongodb.stitch.core.services.mongodb.remote.RemoteCountOptions import com.mongodb.stitch.core.services.mongodb.remote.RemoteUpdateOptions import com.mongodb.stitch.core.testutils.CustomType +import com.mongodb.stitch.server.core.StitchAppClient import com.mongodb.stitch.server.services.mongodb.remote.internal.RemoteMongoClientImpl +import com.mongodb.stitch.server.testutils.BaseStitchServerIntTest +import org.bson.BsonDocument +import org.bson.BsonInt32 +import org.bson.BsonString import org.bson.Document import org.bson.codecs.configuration.CodecConfigurationException import org.bson.codecs.configuration.CodecRegistries @@ -50,6 +55,19 @@ class RemoteMongoClientIntTests : BaseStitchServerIntTest() { Assume.assumeTrue("no MongoDB URI in properties; skipping test", getMongoDbUri().isNotEmpty()) super.setup() + val rule = RuleCreator.MongoDb( + database = dbName, + collection = collName, + roles = listOf(RuleCreator.MongoDb.Role( + read = true, write = true + )), + schema = RuleCreator.MongoDb.Schema().copy(properties = Document())) + val client = createStitchClientForAppWithRule(rule) + + mongoClientOpt = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") + } + + private fun createStitchClientForAppWithRule(rule: RuleCreator): StitchAppClient { val app = createApp() addProvider(app.second, ProviderConfigs.Anon) val svc = addService( @@ -57,19 +75,12 @@ class RemoteMongoClientIntTests : BaseStitchServerIntTest() { "mongodb", "mongodb1", ServiceConfigs.Mongo(getMongoDbUri())) - - val rule = RuleCreator.MongoDb( - database = dbName, - collection = collName, - roles = listOf(RuleCreator.MongoDb.Role( - read = true, write = true - )), - schema = RuleCreator.MongoDb.Schema()) addRule(svc.second, rule) val client = getAppClient(app.first) client.auth.loginWithCredential(AnonymousCredential()) - mongoClientOpt = client.getServiceClient(RemoteMongoClient.factory, "mongodb1") + + return client } @After @@ -403,6 +414,93 @@ class RemoteMongoClientIntTests : BaseStitchServerIntTest() { assertEquals(expected, iter.iterator().next()) } + @Test + fun testWatchBsonValueIDs() { + val coll = getTestColl() + assertEquals(0, coll.count()) + + val rawDoc1 = Document() + rawDoc1["_id"] = 1 + rawDoc1["hello"] = "world" + + val rawDoc2 = Document() + rawDoc2["_id"] = "foo" + rawDoc2["happy"] = "day" + + coll.insertOne(rawDoc1) + assertEquals(1, coll.count()) + + val stream = coll.watch(BsonInt32(1), BsonString("foo")) + + try { + coll.insertOne(rawDoc2) + assertEquals(2, coll.count()) + coll.updateMany(BsonDocument(), Document().append("\$set", + BsonDocument().append("new", BsonString("field")))) + + val insertEvent = stream.nextEvent() + assertEquals(OperationType.INSERT, insertEvent.operationType) + assertEquals(rawDoc2, insertEvent.fullDocument) + val updateEvent1 = stream.nextEvent() + val updateEvent2 = stream.nextEvent() + + assertNotNull(updateEvent1) + assertNotNull(updateEvent2) + + assertEquals(OperationType.UPDATE, updateEvent1.operationType) + assertEquals(rawDoc1.append("new", "field"), updateEvent1.fullDocument) + assertEquals(OperationType.UPDATE, updateEvent2.operationType) + assertEquals(rawDoc2.append("new", "field"), updateEvent2.fullDocument) + } finally { + stream.close() + } + } + + @Test + fun testWatchObjectIdIDs() { + val coll = getTestColl() + assertEquals(0, coll.count()) + + val objectId1 = ObjectId() + val objectId2 = ObjectId() + + val rawDoc1 = Document() + rawDoc1["_id"] = objectId1 + rawDoc1["hello"] = "world" + + val rawDoc2 = Document() + rawDoc2["_id"] = objectId2 + rawDoc2["happy"] = "day" + + coll.insertOne(rawDoc1) + assertEquals(1, coll.count()) + + val stream = coll.watch(objectId1, objectId2) + + try { + coll.insertOne(rawDoc2) + assertEquals(2, coll.count()) + coll.updateMany(BsonDocument(), Document().append("\$set", + BsonDocument().append("new", BsonString("field")))) + + val insertEvent = stream.nextEvent() + assertEquals(OperationType.INSERT, insertEvent.operationType) + assertEquals(rawDoc2, insertEvent.fullDocument) + val updateEvent1 = stream.nextEvent() + val updateEvent2 = stream.nextEvent() + + assertNotNull(updateEvent1) + assertNotNull(updateEvent2) + + assertEquals(OperationType.UPDATE, updateEvent1.operationType) + assertEquals(rawDoc1.append("new", "field"), updateEvent1.fullDocument) + assertEquals(OperationType.UPDATE, updateEvent2.operationType) + assertEquals(rawDoc2.append("new", "field"), updateEvent2.fullDocument) + } finally { + stream.close() + } + } + private fun withoutIds(documents: Collection): Collection { val list = ArrayList(documents.size) documents.forEach { list.add(withoutId(it)) }