Skip to content

Commit

Permalink
Implement tests for backoff strategy
Browse files Browse the repository at this point in the history
To minimize the overall time of the tests task, a variable is introduced
in the build.gradle file to set the back-off interval time to 45 seconds
in case of a timeout, exclusively for the tests.

Signed-off-by: Saeed Rezaee <[email protected]>
  • Loading branch information
SaeedRe committed Mar 4, 2024
1 parent c253085 commit b201ccd
Show file tree
Hide file tree
Showing 5 changed files with 226 additions and 5 deletions.
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ allprojects {
dependencies {
implementation deps.kotlin_stdlib
implementation deps.kotlin_x
implementation deps.joda
}

publishing {
Expand Down Expand Up @@ -110,7 +111,6 @@ project(':ddi-consumer:ddi-api') {
version app_version

dependencies {
implementation deps.joda
}
}

Expand All @@ -132,7 +132,6 @@ project(':virtual-device'){
implementation project(':hara-ddiclient-api')
implementation project(':ddi-consumer')
implementation project(':ddi-consumer:ddi-api')
implementation deps.joda
implementation deps.slf4j_simple
implementation deps.okhttp
}
Expand Down Expand Up @@ -256,6 +255,8 @@ version app_version
test {
systemProperty("LOG_HTTP", project.findProperty("logHttp") ?: "false")
systemProperty("LOG_INTERNAL", project.findProperty("logInternal") ?: "false")
systemProperty("BACKOFF_INTERVAL_SECONDS", 45)

useTestNG()

afterTest { desc, result ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private constructor(scope: ActorScope) : AbstractActor(scope) {
this.send(ErrMsg(errorDetails), state)
LOG.warn(t.message, t)
val newBackoffState = if (t is InterruptedIOException) {
s.copy(backoffPingInterval = Duration.standardMinutes(5))
s.nextBackoffInCaseOfTimeout()
} else {
s.nextBackoff()
}.clearEtags()
Expand Down Expand Up @@ -292,6 +292,17 @@ private constructor(scope: ActorScope) : AbstractActor(scope) {
else -> serverPingInterval
}

fun nextBackoffInCaseOfTimeout(): State {
val testOnlyInterval = System.getProperty(
"BACKOFF_INTERVAL_SECONDS", null)?.toLong()
return if (testOnlyInterval != null) {
val testOnlyDuration = Duration.standardSeconds(testOnlyInterval)
this.copy(backoffPingInterval = testOnlyDuration)
} else {
this.copy(backoffPingInterval = Duration.standardMinutes(5))
}
}

fun nextBackoff(): State {
val nextBackoff = if (backoffPingInterval == null) {
Duration.standardSeconds(30)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
*
* * Copyright © 2017-2024 Kynetics LLC
* *
* * This program and the accompanying materials are made
* * available under the terms of the Eclipse Public License 2.0
* * which is available at https://www.eclipse.org/legal/epl-2.0/
* *
* * SPDX-License-Identifier: EPL-2.0
*
*/

package org.eclipse.hara.ddiclient.integrationtest

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import org.eclipse.hara.ddiclient.api.MessageListener
import org.eclipse.hara.ddiclient.api.MessageListener.Message.Event.NoNewState
import org.eclipse.hara.ddiclient.api.MessageListener.Message.Event.Polling
import org.eclipse.hara.ddiclient.api.MessageListener.Message.State.Idle
import org.eclipse.hara.ddiclient.api.MessageListener.Message.Event.Error
import org.eclipse.hara.ddiclient.integrationtest.abstractions.AbstractHaraMessageTest
import org.eclipse.hara.ddiclient.integrationtest.utils.addOkhttpLogger
import org.eclipse.hara.ddiclient.integrationtest.utils.internalLog
import org.eclipse.hara.ddiclient.integrationtest.utils.logCurrentFunctionName
import org.testng.Assert
import org.testng.annotations.BeforeClass
import org.testng.annotations.Test
import java.io.InterruptedIOException
import java.net.UnknownHostException
import kotlin.coroutines.cancellation.CancellationException

class PingBackoffStrategyTest : AbstractHaraMessageTest() {

override val targetId: String = "PingTimeOutTest"
private val testScope = CoroutineScope(Dispatchers.Default)

private val expectedTestDuration =
Channel<Long>(5, BufferOverflow.DROP_OLDEST)
private var durationCheckJob: Deferred<Unit>? = null

private val currentTime: Long
get() = System.currentTimeMillis()

override fun filterHaraMessages(message: MessageListener.Message): Boolean {
return when (message) {
is Polling,
is Idle,
is Error,
is NoNewState -> true

else -> false
}
}

@BeforeClass
override fun beforeTest() {
super.beforeTest()
setPollingTime("00:00:05")
}

@Test(timeOut = 400_000, priority = 20)
fun `test Pinging Backoff Strategy Should Retry After 5 Minutes In Case Of Server Timeout`() {
logCurrentFunctionName()

runBlocking {
val okHttpBuilder = OkHttpClient.Builder()
.addInterceptor(TimeoutInterceptor())
.addOkhttpLogger()

client = clientFromTargetId(okHttpClientBuilder = okHttpBuilder).invoke(targetId)

expectMessages(
Polling,
Idle,
Polling,
Error(details = listOf(
"exception: class java.io.InterruptedIOException message: Timeout exception")),
Polling,
Idle,
)

//Given that the polling time is 5 seconds, and the backoff strategy should retry
//after 45 seconds (For testing only, the default values is 5 minutes) in case of timeout exception.
//Therefore, the test should be finished in 50 seconds with 1 retry
runTheTestAndExpectToFinishInSeconds(48..52)
}
}

@Test(timeOut = 150_000, priority = 21)
fun `test Pinging Backoff Strategy Should Retry every 30 seconds In Case Of failure other than timeout`() {
logCurrentFunctionName()

runBlocking {

val okHttpBuilder = OkHttpClient.Builder()
.addInterceptor(ConnectionLostInterceptor())
.addOkhttpLogger()

client = clientFromTargetId(okHttpClientBuilder = okHttpBuilder).invoke(targetId)

expectMessages(
Polling,
Idle,
Polling,
Error(details = listOf(
"exception: class java.net.UnknownHostException message: Unable to resolve host Unable to resolve host")),
Polling,
Error(details = listOf(
"exception: class java.net.UnknownHostException message: Unable to resolve host Unable to resolve host")),
Polling,
Idle,
)


//Given that the polling time is 5 seconds, and the backoff strategy should retry after 30, 60, 120 ... seconds
//in case of exceptions other than timeout.
//Therefore, the test should be finished in 95 seconds with 2 retries
val range = 93..98
runTheTestAndExpectToFinishInSeconds(range)
}
}

private suspend fun runTheTestAndExpectToFinishInSeconds(durationRange: IntRange) {
val startTime = currentTime
val testJob = testScope.launch {
startAsyncAndWatchMessages()
}

testJob.invokeOnCompletion {
val endTime = currentTime
val duration = (endTime - startTime) / 1000

runBlocking {
expectedTestDuration.send(duration)
}
}

durationCheckJob = testScope.async {
for (testDuration in expectedTestDuration) {
"Test duration: $testDuration".internalLog()
assert {
Assert.assertTrue(testDuration in durationRange,
"""The test did not finish in the expected time.
Expected time range: $durationRange,
Test duration: $testDuration
"""
)
}
durationCheckJob?.cancel()
}
}

try {
durationCheckJob?.await()
} catch (ignored: CancellationException) {
}

testJob.join()
}

class TimeoutInterceptor : Interceptor {
private var attempt = 0

override fun intercept(chain: Interceptor.Chain): Response {
attempt++
if (attempt == 2) {
"Throwing timeout Exception".internalLog()
throw InterruptedIOException("Timeout exception")
}

return chain.proceed(chain.request())
}
}

class ConnectionLostInterceptor : Interceptor {
private var attempt = 0

override fun intercept(chain: Interceptor.Chain): Response {
attempt++
if (attempt in 2..3) {
"Throwing unknown host Exception".internalLog()
throw UnknownHostException("Unable to resolve host")
}

return chain.proceed(chain.request())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ abstract class AbstractHaraMessageTest : AbstractTest() {
}
}

protected suspend fun startAsyncAndWatchMessages(lastTest: Boolean = false) {
client?.startAsync()
startWatchingExpectedMessages(lastTest)
}

protected suspend fun startWatchingExpectedMessages(lastTest: Boolean = false) {
checkExpectedMessagesJob = getExpectedMessagesCheckingJob(lastTest)
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ abstract class AbstractTest {
protected lateinit var managementApi: ManagementApi
abstract val targetId: String

private val throwableScope = CoroutineScope(Dispatchers.Default)
protected val throwableScope = CoroutineScope(Dispatchers.Default)

private var throwableJob: Deferred<Unit>? = null

Expand Down Expand Up @@ -99,9 +99,15 @@ abstract class AbstractTest {
}

protected suspend fun assertEquals(actual: Any?, expected: Any?) {
throwableJob = throwableScope.async {
assert {
Assert.assertEquals(actual, expected)
}
}

protected suspend fun assert(assertionBlock: () -> Unit) {
throwableJob = throwableScope.async {
assertionBlock()
}
try {
throwableJob?.await()
} catch (ignored: CancellationException) {
Expand Down

0 comments on commit b201ccd

Please sign in to comment.