Skip to content

Conversation

jsflax
Copy link
Contributor

@jsflax jsflax commented Oct 12, 2018

Do not review.

This is for agreement purposes only.

@coveralls
Copy link

coveralls commented Oct 12, 2018

Coverage Status

Coverage increased (+12.0%) to 70.109% when pulling 23e6f64 on jsflax:STITCH-1956 into 94ea1fe on mongodb:master.

Copy link
Contributor

@edaniels edaniels left a comment

Choose a reason for hiding this comment

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

Nice tests so far. Have some various suggestions to improve the strength, readability, and correctness of the tests

null,
BsonDocumentCodec())

dataSynchronizer.start()
Copy link
Contributor

Choose a reason for hiding this comment

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

[style] This takes up a lot of space. I would just group the operation you do on the element with the assertion right after. e.g.

dataSynchronizer.stop()
dataSynchronizer.disableSyncThread()
dataSynchronizer.start()
assertFalse(dataSynchronizer.isRunning)


@Test
fun testNew() {
assertFalse(dataSynchronizer.isRunning)
Copy link
Contributor

Choose a reason for hiding this comment

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

[minor] For unit tests, it reads easier if they don't use a test setup method and instead call a method to get what they need. For example in this test, it's odd that all we check is isRunning but the user reading doesn't know where it comes from.

}

@Test
@Suppress("UNCHECKED_CAST")
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these suppression necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removes unnecessary warnings due to us having to cast certain mocks.


dataSynchronizer.configure(
namespace,
mock(ConflictHandler::class.java) as ConflictHandler<BsonDocument>,
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] Instead of mocking these, just provide real ones. There's no value gained in mocking them as they are never manipulated.

authMonitor
))

private val dataSynchronizer = spy(DataSynchronizer(
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] Move this into a method where new versions of these can be fetched. That means so should the service, local client, and so on. It makes the tests less brittle.


dataSynchronizer.deleteOneById(namespace, document1["_id"])

`when`(coreRemoteMongoCollectionMock.deleteOne(any())).thenAnswer {
Copy link
Contributor

Choose a reason for hiding this comment

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

[test] For all tests that end up emitting errors, we should verify what the error is after each acquire.

}

@Test
fun testFailedDelete() {
Copy link
Contributor

Choose a reason for hiding this comment

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

[test] This test should verify we have frozen sync for this. It would also be good to test after doing insertOneAndSync again that it's not longer frozen and we insert.

dataSynchronizer.insertOneAndSync(namespace, document1)

// verify triggerListeningToNamespace has been called
verify(dataSynchronizer, times(1)).triggerListeningToNamespace(namespace)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is looking too deep into the class. I would remove this and rely on stream function which is the true signal

dataSynchronizer.insertOneAndSync(namespace, document2)

// verify trigger is called again
verify(dataSynchronizer, times(3)).triggerListeningToNamespace(namespace)
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, can delete.

eventSemaphore.acquire()

// verify trigger is called, once for the insert, once for the configure
verify(dataSynchronizer, times(2)).triggerListeningToNamespace(namespace)
Copy link
Contributor

Choose a reason for hiding this comment

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

can delete

@edaniels edaniels requested a review from adamchel October 12, 2018 16:00
Copy link
Contributor

@adamchel adamchel left a comment

Choose a reason for hiding this comment

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

This is looking pretty good and I definitely think it's going in the right direction. Had a few questions/minor comments, and will probably take a closer look once it's actually ready for review.

*/
public boolean doSyncPass() {
if (!syncLock.tryLock()) {
if (!this.isConfigured || !syncLock.tryLock()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

[q] is this what you think might solve the flakiness we're seeing in the configure test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, this is actually how the method should function. I don't believe this will solve any races.

verify(conflictHandler, times(1)).resolveConflict(eq(document1["_id"]), any(), any())
verify(errorListener, times(0)).onError(eq(document1["_id"]), any())

// verify we are inserting!
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] verify we are not inserting? or that we desync-ed/froze?

Copy link
Contributor

Choose a reason for hiding this comment

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

or is this verifying that the conflict resolver deleted the document?

verify(conflictHandler, times(0)).resolveConflict(eq(document2["_id"]), any(), any())
verify(errorListener, times(1)).onError(eq(document2["_id"]), any())

assertEquals(
Copy link
Contributor

Choose a reason for hiding this comment

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

[minor] I think in general, for all these tests, we should add a comment when checking the document in the local collection to explain why we're checking for the specific expected state. I was confused at first why we were expecting the document to exist when the insert failed, but I realized it's because we expect the document to be there locally, just not remotely.

verify(conflictHandler, times(1)).resolveConflict(eq(document1["_id"]), any(), any())
verify(errorListener, times(0)).onError(eq(document1["_id"]), any())

// verify we are inserting!
Copy link
Contributor

Choose a reason for hiding this comment

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

inserting the resolution to the conflict?

return BsonDocument("_id", BsonObjectId()).append(key, value)
}

private fun withoutVersionId(document: BsonDocument?): BsonDocument? {
Copy link
Contributor

Choose a reason for hiding this comment

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

[heads up] this should be renamed to withoutSyncVersion to be consistent with the change I made in my PR

@jsflax
Copy link
Contributor Author

jsflax commented Oct 22, 2018

This is a large PR.

  • Add several tests for each of our core models (configs, ChangeEvents, etc.). These are mostly sanity checks and round trip checks.
  • Add a suite of unit tests to test the ins and outs of the DataSynchronizer, NamespaceChangeStreamListener, and CoreSync. These tests are managed by the SyncTestHarness, which contains a series of helper methods in DataSynchronizerTestContext. The test context should make testing easier to read and more manageable.
  • Consolidate android/server test logic. Proxy relevant methods through a core proxy and run the tests in core. See ProxyRemoteMethods, ProxySyncMethods, SyncIntTestProxy, SyncIntTestRunner.
  • Add comments to integration tests, making them more legible.
  • Fix testConfigure race in Int tests.
  • Various bug fixes.

@edaniels @adamchel PTAL.

Note: Seems like GitHub is having some issues. This is the patch for the latest commit on evg: https://evergreen.mongodb.com/version/5bcdf2dd2a60ed479842434d

Copy link
Contributor

@edaniels edaniels left a comment

Choose a reason for hiding this comment

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

LGTM. Got a question or comment or two. Nice job.


@Test
fun testStaleFetchMultiple()
} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

EOF

// insert a document remotely, and wait for the event to store
val sem = watchForEvents(syncTestRunner.namespace)
remoteColl.insertOne(Document("_id", insertedId).append("fly", "away"))
sem.tryAcquire(3, TimeUnit.SECONDS)
Copy link
Contributor

Choose a reason for hiding this comment

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

[mahor] none of these tests that previously had just a acquire should have try acquire now. That encourages races to occur. If 3 seconds pass and nothing happens, this proceeds.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point. It seemed to me that tryAcquire would at least let the test fail, as opposed to hang. I had only noticed it was an option as it was added to some of the more recent tests. However, I'll remove it from the tests I added it to. In this test, we actually do not need to acquire that semaphore at all– it can be refactored to only play with a changeEvent semaphore, an errorListener semaphore, and a conflict handling semaphore.

}

override fun waitForEvent() {
eventSemaphore?.tryAcquire(2, TimeUnit.SECONDS)
Copy link
Contributor

Choose a reason for hiding this comment

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

These should all change too. We shouldn't base any of these on time

@@ -0,0 +1,567 @@
package com.mongodb.stitch.core.services.mongodb.remote.sync.internal
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this have the word Unit in it to denote that it's only for unit tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was under the impression that "harness" was a term generally for unit tests. Though Int tests have setup methods, you wouldn't want a harness per se.

On the contrary, adding the word Unit does make it undoubtably clear. So I will add it.

Copy link
Contributor

@adamchel adamchel left a comment

Choose a reason for hiding this comment

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

Excellent work! This should make future testing a lot less painful. I have mostly nits and q's, some of which may be out of scope for this ticket anyway and are more just general questions I have about the overall implementation.

assertTrue(namespaceChangeStreamListener.openStream())
ctx.nextStreamEvent = Event.Builder().withEventName("message").build()
namespaceChangeStreamListener.storeNextEvent()
assertTrue(namespaceChangeStreamListener.isOpen)
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] add a check for assertEquals(0, namespaceChangeStreamListener.events.size)?

coreSync.insertOneAndSync(ctx.testDocument)
fail("should have received duplicate key error index")
} catch (e: MongoWriteException) {
assertNotNull(e)
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] you can verify that this is duplicate key error by checking assertTrue(e.message.contains("E11000")) (or something similar)

BsonObjectId().value.toHexString())
}

fun withoutVersionId(document: BsonDocument?): BsonDocument? {
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] I renamed this to withoutSyncVersion in the integration tests since it's more than just an ID now, so you could change it here as well.

emitErrorSemaphore: Semaphore? = null,
expectedDocumentId: BsonValue? = null
): ErrorListener {
open class TestErrorListener : ErrorListener {
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit q] why is this class embedded in the function whereas the errors are at the level of the SyncTestHarness class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment, we don't need to interact with it. Might as well not muddle up what developers can access.

}

override fun waitForEvent() {
eventSemaphore?.tryAcquire(2, TimeUnit.SECONDS)
Copy link
Contributor

Choose a reason for hiding this comment

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

[minor] assertTrue(eventSemaphore?.tryAcquire(10, TimeUnit.SECONDS))
(or however you need to do it so the case where the semaphore is null works properly too).

// should be frozen since the operation type was unknown
ctx.queueConsumableRemoteUpdateEvent()
ctx.doSyncPass()
assertEquals(ctx.testDocument, ctx.findTestDocumentFromLocalCollection())
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit q] same q as before, how do we know this is frozen? don't we need to try deleting first?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need to try deleting– making sure the document doesn't update works as well.

it.isFrozen = true
}
dataSynchronizer.reloadConfig()
// 2: Local wins
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] where is 1.?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding now.


ctx.doSyncPass()
ctx.waitForError()
// verify we are inserting!
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] copy-pasted comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes

// assert this doc does not exist
assertNull(ctx.findTestDocumentFromLocalCollection())

// update the non-existent document...
Copy link
Contributor

Choose a reason for hiding this comment

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

[nit] update -> delete

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

// change event listener have been called
streamAndSync()

changeEventListenerSemaphore.tryAcquire(3, TimeUnit.SECONDS)
Copy link
Contributor

Choose a reason for hiding this comment

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

[major] another tryAcquire to assertTrue and change to 10 seconds

@jsflax
Copy link
Contributor Author

jsflax commented Oct 24, 2018

@adamchel PTAL

Copy link
Contributor

@adamchel adamchel left a comment

Choose a reason for hiding this comment

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

LGTM!

@jsflax jsflax merged commit 9da14c8 into mongodb:master Oct 24, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants