Skip to content

Commit

Permalink
Merge pull request #86 from Parsely/extract_flush_manager
Browse files Browse the repository at this point in the history
  • Loading branch information
wzieba authored Nov 3, 2023
2 parents fcb6d7c + aad4629 commit 86bac35
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 53 deletions.
3 changes: 3 additions & 0 deletions parsely/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {

ext {
assertJVersion = '3.24.2'
coroutinesVersion = '1.7.3'
mockWebServerVersion = '4.12.0'
}

Expand Down Expand Up @@ -63,12 +64,14 @@ dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3'
implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"

testImplementation 'org.robolectric:robolectric:4.10.3'
testImplementation 'androidx.test:core:1.5.0'
testImplementation "org.assertj:assertj-core:$assertJVersion"
testImplementation 'junit:junit:4.13.2'
testImplementation "com.squareup.okhttp3:mockwebserver:$mockWebServerVersion"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test:rules:1.5.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import java.io.FileInputStream
import java.io.ObjectInputStream
import java.lang.reflect.Field
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import kotlin.io.path.Path
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
Expand Down Expand Up @@ -70,6 +71,47 @@ class FunctionalTests {
}
}

/**
* In this scenario, the consumer app tracks 2 events during the first flush interval.
* Then, we validate, that after flush interval passed the SDK sends the events
* to Parse.ly servers.
*
* Then, the consumer app tracks another event and we validate that the SDK sends the event
* to Parse.ly servers as well.
*/
@Test
fun appFlushesEventsAfterFlushInterval() {
ActivityScenario.launch(SampleActivity::class.java).use { scenario ->
scenario.onActivity { activity: Activity ->
beforeEach(activity)
server.enqueue(MockResponse().setResponseCode(200))
parselyTracker = initializeTracker(activity)

parselyTracker.trackPageview("url", null, null, null)
}

Thread.sleep((flushInterval / 2).inWholeMilliseconds)

scenario.onActivity {
parselyTracker.trackPageview("url", null, null, null)
}

Thread.sleep((flushInterval / 2).inWholeMilliseconds)

val firstRequestPayload = server.takeRequest(2000, TimeUnit.MILLISECONDS)?.toMap()
assertThat(firstRequestPayload!!["events"]).hasSize(2)

scenario.onActivity {
parselyTracker.trackPageview("url", null, null, null)
}

Thread.sleep(flushInterval.inWholeMilliseconds)

val secondRequestPayload = server.takeRequest(2000, TimeUnit.MILLISECONDS)?.toMap()
assertThat(secondRequestPayload!!["events"]).hasSize(1)
}
}

private fun RecordedRequest.toMap(): Map<String, List<Event>> {
val listType: TypeReference<Map<String, List<Event>>> =
object : TypeReference<Map<String, List<Event>>>() {}
Expand Down Expand Up @@ -103,7 +145,7 @@ class FunctionalTests {
private companion object {
const val siteId = "123"
const val localStorageFileName = "parsely-events.ser"
val flushInterval = 10.seconds
val flushInterval = 5.seconds
}

class SampleActivity : Activity()
Expand Down
38 changes: 38 additions & 0 deletions parsely/src/main/java/com/parsely/parselyandroid/FlushManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.parsely.parselyandroid

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

/**
* Manager for the event flush timer.
*
*
* Handles stopping and starting the flush timer. The flush timer
* controls how often we send events to Parse.ly servers.
*/
internal class FlushManager(
private val parselyTracker: ParselyTracker,
val intervalMillis: Long,
private val coroutineScope: CoroutineScope
) {
private var job: Job? = null

fun start() {
if (job?.isActive == true) return

job = coroutineScope.launch {
while (isActive) {
delay(intervalMillis)
parselyTracker.flushEvents()
}
}
}

fun stop() = job?.cancel()

val isRunning: Boolean
get() = job?.isActive ?: false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.parsely.parselyandroid

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob

val sdkScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

/**
Expand Down Expand Up @@ -87,7 +86,8 @@ protected ParselyTracker(String siteId, int flushInterval, Context c) {

eventQueue = new ArrayList<>();

flushManager = new FlushManager(timer, flushInterval * 1000L);
flushManager = new FlushManager(this, flushInterval * 1000L,
ParselyCoroutineScopeKt.getSdkScope());

if (getStoredQueue().size() > 0) {
startFlushTimer();
Expand Down Expand Up @@ -668,56 +668,7 @@ protected synchronized Void doInBackground(Void... params) {
}
}

/**
* Manager for the event flush timer.
* <p>
* Handles stopping and starting the flush timer. The flush timer
* controls how often we send events to Parse.ly servers.
*/
private class FlushManager {

private final Timer parentTimer;
private final long intervalMillis;
private TimerTask runningTask;

public FlushManager(Timer parentTimer, long intervalMillis) {
this.parentTimer = parentTimer;
this.intervalMillis = intervalMillis;
}

public void start() {
if (runningTask != null) {
return;
}

runningTask = new TimerTask() {
public void run() {
flushEvents();
}
};
parentTimer.scheduleAtFixedRate(runningTask, intervalMillis, intervalMillis);
}

public boolean stop() {
if (runningTask == null) {
return false;
} else {
boolean output = runningTask.cancel();
runningTask = null;
return output;
}
}

public boolean isRunning() {
return runningTask != null;
}

public long getIntervalMillis() {
return intervalMillis;
}
}

private void flushEvents() {
void flushEvents() {
new FlushQueue().execute();
}

Expand Down
106 changes: 106 additions & 0 deletions parsely/src/test/java/com/parsely/parselyandroid/FlushManagerTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.parsely.parselyandroid

import androidx.test.core.app.ApplicationProvider
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(RobolectricTestRunner::class)
class FlushManagerTest {

private lateinit var sut: FlushManager
private val tracker = FakeTracker()

@Test
fun `when timer starts and interval time passes, then flush queue`() = runTest {
sut = FlushManager(tracker, DEFAULT_INTERVAL_MILLIS, backgroundScope)

sut.start()
advanceTimeBy(DEFAULT_INTERVAL_MILLIS)
runCurrent()

assertThat(tracker.flushEventsCounter).isEqualTo(1)
}

@Test
fun `when timer starts and three interval time passes, then flush queue 3 times`() = runTest {
sut = FlushManager(tracker, DEFAULT_INTERVAL_MILLIS, backgroundScope)

sut.start()
advanceTimeBy(3 * DEFAULT_INTERVAL_MILLIS)
runCurrent()

assertThat(tracker.flushEventsCounter).isEqualTo(3)
}

@Test
fun `when timer starts and is stopped after 2 intervals passes, then flush queue 2 times`() =
runTest {
sut = FlushManager(tracker, DEFAULT_INTERVAL_MILLIS, backgroundScope)

sut.start()
advanceTimeBy(2 * DEFAULT_INTERVAL_MILLIS)
runCurrent()
sut.stop()
advanceTimeBy(DEFAULT_INTERVAL_MILLIS)
runCurrent()

assertThat(tracker.flushEventsCounter).isEqualTo(2)
}

@Test
fun `when timer starts, is stopped before end of interval and then time of interval passes, then do not flush queue`() =
runTest {
sut = FlushManager(tracker, DEFAULT_INTERVAL_MILLIS, backgroundScope)

sut.start()
advanceTimeBy(DEFAULT_INTERVAL_MILLIS / 2)
runCurrent()
sut.stop()
advanceTimeBy(DEFAULT_INTERVAL_MILLIS / 2)
runCurrent()

assertThat(tracker.flushEventsCounter).isEqualTo(0)
}

@Test
fun `when timer starts, and another timer starts after some time, then flush queue according to the first start`() =
runTest {
sut = FlushManager(tracker, DEFAULT_INTERVAL_MILLIS, backgroundScope)

sut.start()
advanceTimeBy(DEFAULT_INTERVAL_MILLIS / 2)
runCurrent()
sut.start()
advanceTimeBy(DEFAULT_INTERVAL_MILLIS / 2)
runCurrent()

assertThat(tracker.flushEventsCounter).isEqualTo(1)

advanceTimeBy(DEFAULT_INTERVAL_MILLIS / 2)
runCurrent()

assertThat(tracker.flushEventsCounter).isEqualTo(1)
}

private companion object {
val DEFAULT_INTERVAL_MILLIS: Long = 30.seconds.inWholeMilliseconds
}

class FakeTracker : ParselyTracker(
"", 0, ApplicationProvider.getApplicationContext()
) {
var flushEventsCounter = 0

override fun flushEvents() {
flushEventsCounter++
}
}
}

0 comments on commit 86bac35

Please sign in to comment.