Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Coroutines: add event flow #91

Merged
merged 19 commits into from
Nov 9, 2023
Merged

Coroutines: add event flow #91

merged 19 commits into from
Nov 9, 2023

Conversation

wzieba
Copy link
Collaborator

@wzieba wzieba commented Nov 8, 2023

Description

This PR introduces Kotlin Coroutines for "add an event" flow. It covers

  • adding an event to the in-memory buffer
  • storing event in local storage (file)

This PR also:

  • removes logic of QueueManager as discussed internally p1699018266041689-slack-C0533SEJ82H
  • makes save to file operation thread-safe of the main thread

How to test

  1. Run the test app
  2. Tap on "Track URL" multiple times. Count how many times you tapped.
  3. Observe logs: you'll notice a new log Persisting n events, where n is the number of events added in 1-second time frame
  4. Move app to the background
  5. Assert that number m of events in Sending request with m events log equals the number you counted in step 2

to `LocalStorageRepository`
The logic of validating queue is no longer relevant as discussed internally. This commits removes `QueueManager` completely, and replaces it with a thread-safe `InMemoryBuffer`, which holds newly recorded events and, as soon as possible, adds them to local persistence.

The new event not added to persistence right away, because the local storage file might be occupied/locked by, e.g. process/coroutine of sending events. We do not want to make consumer of the library to wait for adding a new event ever. Hence, the `InMemoryRepository#buffer`.
By using mutual exclusion (`Mutex`). Also, remove the `persistEvent` method, as it's no longer needed actually.
Also, add a name for easier identification while debugging
To reduce overhead of constant loop, the `InMemoryBuffer` will check `buffer` list every second.
As `inMemoryBuffer` triggers asynchronous operation, this PR adds a listener that asserts starting flush queue after adding an event.
50 events batches are no longer a thing since we removed `QueueManager`
50 events batches are no longer a thing since we removed `QueueManager`
private val onEventAddedListener: () -> Unit,
) {

private val mutex = Mutex()
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

To test if this mutual exclusion works correctly, comment this, usages in init and add and then run stressTest. It should fail with unexpected result - sometimes recorded events are smaller, sometimes bigger.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the instructions! 🥇

FYI: I run that stressTest 3 times after removing mutex and everything was still working as expected on my side (testing on actual device). I then also triggered the whole ./gradlew connectedDebugAndroidTest from CLI and it still worked as expected, everything was successful. 🤷

Copy link
Collaborator

Choose a reason for hiding this comment

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

UPDATE: I then also removed the mutex on the repository level and run the stressTest 3 times, along with running the whole ./gradlew connectedDebugAndroidTest from CLI. Running the stressTest succeeded every time, but running the whole ./gradlew connectedDebugAndroidTest from CLI indeed failed (both times I tried):

com.parsely.parselyandroid.FunctionalTests > stressTest[Pixel 4 - 13] FAILED 
        java.lang.AssertionError:
        Expected size: 500 but was: 1000 in:
Tests on Pixel 4 - 13 failed: There was 1 failure(s).

PS:

  1. I then uncommented the mutex on the repository level and run whole ./gradlew connectedDebugAndroidTest from CLI again and it started failing as well. 👍
  2. I then, uncommented the mutex on the buffer level and run whole ./gradlew connectedDebugAndroidTest from CLI again and it now succeeded. ✅

I guess there exist some flakiness but it seems that everything is working as expected after all. Just wanted to document my testing here, just in case it somehow helps you.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for sharing! I think for some reason, they're more likely to fail without mutex on emulator. In any case, one can also remove the delay to the InMemoryBuffer#init - this would make the test fail more certain.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think for some reason, they're more likely to fail without mutex on emulator.

Yea, maybe! 🤷

In any case, one can also remove the delay to the InMemoryBuffer#init - this would make the test fail more certain.

True, good tip! 💯

@wzieba wzieba requested a review from ParaskP7 November 8, 2023 15:37
@wzieba wzieba marked this pull request as ready for review November 8, 2023 15:37
Copy link

codecov bot commented Nov 8, 2023

Codecov Report

Attention: 9 lines in your changes are missing coverage. Please review.

Comparison is base (5e590c2) 53.00% compared to head (8c4ce70) 55.85%.

Additional details and impacted files
@@              Coverage Diff               @@
##           coroutines      #91      +/-   ##
==============================================
+ Coverage       53.00%   55.85%   +2.84%     
==============================================
  Files              12       12              
  Lines             366      376      +10     
  Branches           57       57              
==============================================
+ Hits              194      210      +16     
+ Misses            155      148       -7     
- Partials           17       18       +1     
Files Coverage Δ
...m/parsely/parselyandroid/LocalStorageRepository.kt 76.47% <100.00%> (+0.60%) ⬆️
...om/parsely/parselyandroid/ParselyCoroutineScope.kt 100.00% <100.00%> (ø)
.../java/com/parsely/parselyandroid/InMemoryBuffer.kt 95.65% <95.65%> (ø)
...ava/com/parsely/parselyandroid/ParselyTracker.java 11.81% <27.27%> (+0.69%) ⬆️

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

All events are now add to `InMemoryBuffer` and then from there to local storage which is our SSOT for events that are about to be sent.
Copy link
Collaborator

@ParaskP7 ParaskP7 left a comment

Choose a reason for hiding this comment

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

👋 @wzieba !

I have reviewed and tested this PR as per the instructions, everything works as expected, awesome job with InMemoryBuffer and removing the eventQueue, lots of complexity is now actually gone! 🌟 🎉 🚀


I have left a questions (❓) and a couple of suggestions (💡) for you to consider. I am going to approve this PR anyway, since none is blocking (ish). I am NOT going to merge this PR yet to give you some time to apply any of my suggestions. However, feel free to ignore them and merge the PR yourself.

.github/workflows/readme.yml Show resolved Hide resolved
ParselyTracker.PLog("Persisting event queue")
persistObject((inMemoryQueue + getStoredQueue()).distinct())
open suspend fun insertEvents(toInsert: List<Map<String, Any?>?>) = mutex.withLock {
ParselyTracker.PLog("Persisting ${toInsert.size} events")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion (💡): Just like it is done on InMemoryBuffer.add(...), maybe this log can be moved to InMemoryBuffer.init { ... } as well and make the logging closer to source and more consistent, at least with each other. Wdyt? 🤔

private val repository = FakeLocalStorageRepository()

@Test
fun `when adding a new event, then save it to local storage and run onEventAdded listener`() = runTest {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggestion (💡): Consider splitting this test into two:

  1. when adding a new event, then save it to local storage
  2. given a listener, when adding a new event, then run listener

private val onEventAddedListener: () -> Unit,
) {

private val mutex = Mutex()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the instructions! 🥇

FYI: I run that stressTest 3 times after removing mutex and everything was still working as expected on my side (testing on actual device). I then also triggered the whole ./gradlew connectedDebugAndroidTest from CLI and it still worked as expected, everything was successful. 🤷

private val onEventAddedListener: () -> Unit,
) {

private val mutex = Mutex()
Copy link
Collaborator

Choose a reason for hiding this comment

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

UPDATE: I then also removed the mutex on the repository level and run the stressTest 3 times, along with running the whole ./gradlew connectedDebugAndroidTest from CLI. Running the stressTest succeeded every time, but running the whole ./gradlew connectedDebugAndroidTest from CLI indeed failed (both times I tried):

com.parsely.parselyandroid.FunctionalTests > stressTest[Pixel 4 - 13] FAILED 
        java.lang.AssertionError:
        Expected size: 500 but was: 1000 in:
Tests on Pixel 4 - 13 failed: There was 1 failure(s).

PS:

  1. I then uncommented the mutex on the repository level and run whole ./gradlew connectedDebugAndroidTest from CLI again and it started failing as well. 👍
  2. I then, uncommented the mutex on the buffer level and run whole ./gradlew connectedDebugAndroidTest from CLI again and it now succeeded. ✅

I guess there exist some flakiness but it seems that everything is working as expected after all. Just wanted to document my testing here, just in case it somehow helps you.

@wzieba wzieba merged commit 845947b into coroutines Nov 9, 2023
2 checks passed
@wzieba wzieba deleted the coroutines-queue-manager branch November 9, 2023 19:48
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.

None yet

2 participants