diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77b13174e..f1dfa804f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,9 +117,10 @@ jobs: - uses: actions/download-artifact@v2 - uses: codecov/codecov-action@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true verbose: false + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} microbenchmarks: name: Microbenchmarks diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt index c678676aa..8381fcea2 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/core/PresenceTest.kt @@ -827,5 +827,51 @@ class PresenceTest { } } + @Test + fun test_whether_presence_event_queue_is_empty_after_consecutive_presence_changes() { + withTwoClientsAndDocuments { c1, _, d1, d2, _ -> + val d1PresenceEvents = mutableListOf() + val d2PresenceEvents = mutableListOf() + val jobs = listOf( + launch(start = CoroutineStart.UNDISPATCHED) { + d1.events.filterIsInstance() + .collect(d1PresenceEvents::add) + }, + launch(start = CoroutineStart.UNDISPATCHED) { + d2.events.filterIsInstance() + .collect(d2PresenceEvents::add) + }, + ) + + d1.updateAsync { _, presence -> + repeat(10) { + presence.put(mapOf("a" to "${it + 1}")) + } + }.await() + + val lastD1PresenceEvent = MyPresence.PresenceChanged( + PresenceInfo(c1.requireClientId(), mapOf("a" to "10")), + ) + val lastD2PresenceEvent = Others.PresenceChanged( + PresenceInfo(c1.requireClientId(), mapOf("a" to "10")), + ) + + withTimeout(GENERAL_TIMEOUT) { + while (lastD1PresenceEvent !in d1PresenceEvents || + lastD2PresenceEvent !in d2PresenceEvents + ) { + delay(50) + } + } + + assertEquals(lastD1PresenceEvent, d1PresenceEvents.last()) + assertTrue(d1.presenceEventQueue.isEmpty()) + assertEquals(lastD2PresenceEvent, d2PresenceEvents.last()) + assertTrue(d2.presenceEventQueue.isEmpty()) + + jobs.forEach(Job::cancel) + } + } + private data class Cursor(val x: Int, val y: Int) } diff --git a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt index 5fdb9c8bb..5b5f6bfe8 100644 --- a/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt +++ b/yorkie/src/androidTest/kotlin/dev/yorkie/document/json/JsonTreeSplitMergeTest.kt @@ -5,6 +5,7 @@ import dev.yorkie.TreeTest import dev.yorkie.core.Client.SyncMode.Manual import dev.yorkie.core.withTwoClientsAndDocuments import dev.yorkie.document.json.JsonTreeTest.Companion.rootTree +import dev.yorkie.document.json.JsonTreeTest.Companion.updateAndSync import kotlin.test.assertEquals import org.junit.Test import org.junit.runner.RunWith @@ -16,7 +17,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_split_at_the_same_position() { withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( "t", @@ -31,7 +32,7 @@ class JsonTreeSplitMergeTest { ) JsonTreeTest.assertTreesXmlEquals("

ab

", d1, d2) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(2, 2, 1) }, @@ -48,7 +49,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_split_at_different_positions_on_the_same_node() { withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( "t", @@ -63,7 +64,7 @@ class JsonTreeSplitMergeTest { ) JsonTreeTest.assertTreesXmlEquals("

abc

", d1, d2) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(2, 2, 1) }, @@ -81,7 +82,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_insert_into_the_split_position() { withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( "t", @@ -96,7 +97,7 @@ class JsonTreeSplitMergeTest { ) JsonTreeTest.assertTreesXmlEquals("

ab

", d1, d2) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(2, 2, 1) }, @@ -114,7 +115,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_insert_into_original_node() { withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( "t", @@ -129,7 +130,7 @@ class JsonTreeSplitMergeTest { ) JsonTreeTest.assertTreesXmlEquals("

ab

", d1, d2) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(2, 2, 1) }, @@ -147,7 +148,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_insert_into_split_node() { withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( "t", @@ -162,7 +163,7 @@ class JsonTreeSplitMergeTest { ) JsonTreeTest.assertTreesXmlEquals("

ab

", d1, d2) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(2, 2, 1) }, @@ -180,7 +181,7 @@ class JsonTreeSplitMergeTest { @Test fun test_contained_split_and_delete_contents_in_split_node() { withTwoClientsAndDocuments(syncMode = Manual) { c1, c2, d1, d2, _ -> - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.setNewTree( "t", @@ -195,7 +196,7 @@ class JsonTreeSplitMergeTest { ) JsonTreeTest.assertTreesXmlEquals("

ab

", d1, d2) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(2, 2, 1) }, @@ -232,7 +233,7 @@ class JsonTreeSplitMergeTest { }.await() JsonTreeTest.assertTreesXmlEquals("

ab

", d1) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(1, 3) }, @@ -268,7 +269,7 @@ class JsonTreeSplitMergeTest { d1.getRoot().rootTree().toXml(), ) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(2, 6) }, @@ -309,7 +310,7 @@ class JsonTreeSplitMergeTest { }.await() assertEquals("

a

c

b

", d1.getRoot().rootTree().toXml()) - JsonTreeTest.updateAndSync( + updateAndSync( JsonTreeTest.Companion.Updater(c1, d1) { root, _ -> root.rootTree().edit(2, 7) }, diff --git a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt index 85eb53ee0..7052159d0 100644 --- a/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt +++ b/yorkie/src/main/kotlin/dev/yorkie/document/Document.kt @@ -100,7 +100,8 @@ public class Document( public val garbageLength: Int get() = root.garbageLength - private val presenceEventQueue = mutableListOf() + @VisibleForTesting + internal val presenceEventQueue = mutableListOf() private val pendingPresenceEvents = mutableListOf() private val onlineClients = MutableStateFlow(setOf()) @@ -360,6 +361,7 @@ public class Document( */ private suspend fun publishPresenceEvent(presences: Presences) { val iterator = presenceEventQueue.listIterator() + var clearPresenceEventQueue = false while (iterator.hasNext()) { val event = iterator.next() if (event is Others && event.changed.actorID == changeID.actor) { @@ -368,10 +370,16 @@ public class Document( } if (presenceEventReadyToBePublished(event, presences)) { + if (presenceEventQueue.first() != event) { + clearPresenceEventQueue = true + } eventStream.emit(event) iterator.remove() } } + if (clearPresenceEventQueue) { + presenceEventQueue.clear() + } } private fun presenceEventReadyToBePublished( @@ -382,14 +390,14 @@ public class Document( is MyPresence.Initialized -> presences.keys.containsAll(event.initialized.keys) is MyPresence.PresenceChanged -> { val actorID = event.changed.actorID - actorID !in presences || event.changed.presence == presences[actorID] + event.changed.presence == presences[actorID] } is Others.Watched -> event.changed.actorID in presences is Others.Unwatched -> event.changed.actorID !in presences is Others.PresenceChanged -> { val actorID = event.changed.actorID - actorID !in presences || event.changed.presence == presences[actorID] + event.changed.presence == presences[actorID] } } }