Skip to content

Commit 8d294e0

Browse files
authored
fix: on unknown exceptions, dont retry requests (#150)
* fix: on unknown exceptions, dont retry requests * chore: added interceptor to shortcircuit requests when no network is available
1 parent 50ccdb8 commit 8d294e0

File tree

9 files changed

+82
-19
lines changed

9 files changed

+82
-19
lines changed

android-client-sdk/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
33
package="com.devcycle.sdk.android">
44

5+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
56
<uses-permission android:name="android.permission.INTERNET" />
67

78
</manifest>

android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DVCApiClient.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.devcycle.sdk.android.api
22

3+
import android.content.Context
4+
import com.devcycle.sdk.android.interceptor.NetworkConnectionInterceptor
35
import com.devcycle.sdk.android.util.JSONMapper
46
import okhttp3.OkHttpClient
57
import retrofit2.Retrofit
@@ -10,7 +12,8 @@ internal class DVCApiClient {
1012
private val adapterBuilder: Retrofit.Builder = Retrofit.Builder()
1113
.addConverterFactory(JacksonConverterFactory.create(JSONMapper.mapper))
1214

13-
fun initialize(baseUrl: String): DVCApi {
15+
fun initialize(baseUrl: String, context: Context): DVCApi {
16+
okBuilder.addInterceptor(NetworkConnectionInterceptor(context))
1417
return adapterBuilder
1518
.baseUrl(baseUrl)
1619
.client(okBuilder.build())

android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DVCClient.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class DVCClient private constructor(
4848
private val defaultIntervalInMs: Long = 10000
4949
private val flushInMs: Long = options?.flushEventsIntervalMs ?: defaultIntervalInMs
5050
private val dvcSharedPrefs: DVCSharedPrefs = DVCSharedPrefs(context)
51-
private val request: Request = Request(sdkKey, apiUrl, eventsUrl)
51+
private val request: Request = Request(sdkKey, apiUrl, eventsUrl, context)
5252
private val observable: BucketedUserConfigListener = BucketedUserConfigListener()
5353
private val eventQueue: EventQueue = EventQueue(request, ::user, CoroutineScope(coroutineContext), flushInMs)
5454
private val enableEdgeDB: Boolean = options?.enableEdgeDB ?: false

android-client-sdk/src/main/java/com/devcycle/sdk/android/api/DVCEdgeDBApiClient.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.devcycle.sdk.android.api
22

3+
import android.content.Context
34
import com.devcycle.sdk.android.interceptor.AuthorizationHeaderInterceptor
5+
import com.devcycle.sdk.android.interceptor.NetworkConnectionInterceptor
46
import com.devcycle.sdk.android.util.JSONMapper
57
import okhttp3.OkHttpClient
68
import retrofit2.Retrofit
@@ -11,8 +13,9 @@ internal class DVCEdgeDBApiClient {
1113
private val adapterBuilder: Retrofit.Builder = Retrofit.Builder()
1214
.addConverterFactory(JacksonConverterFactory.create(JSONMapper.mapper))
1315

14-
fun initialize(sdkKey: String, baseUrl: String): DVCEdgeDBApi {
16+
fun initialize(sdkKey: String, baseUrl: String, context: Context): DVCEdgeDBApi {
1517
okBuilder.addInterceptor(AuthorizationHeaderInterceptor(sdkKey))
18+
okBuilder.addNetworkInterceptor(NetworkConnectionInterceptor(context))
1619
return adapterBuilder
1720
.baseUrl(baseUrl)
1821
.client(okBuilder.build())

android-client-sdk/src/main/java/com/devcycle/sdk/android/api/Request.kt

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.devcycle.sdk.android.api
22

3+
import android.content.Context
34
import com.devcycle.sdk.android.exception.DVCRequestException
45

56
import com.devcycle.sdk.android.model.*
@@ -15,10 +16,10 @@ import retrofit2.Response
1516
import com.devcycle.sdk.android.util.DVCLogger
1617
import java.io.IOException
1718

18-
internal class Request constructor(sdkKey: String, apiBaseUrl: String, eventsBaseUrl: String) {
19-
private val api: DVCApi = DVCApiClient().initialize(apiBaseUrl)
19+
internal class Request constructor(sdkKey: String, apiBaseUrl: String, eventsBaseUrl: String, context: Context) {
20+
private val api: DVCApi = DVCApiClient().initialize(apiBaseUrl, context)
2021
private val eventApi: DVCEventsApi = DVCEventsApiClient().initialize(sdkKey, eventsBaseUrl)
21-
private val edgeDBApi: DVCEdgeDBApi = DVCEdgeDBApiClient().initialize(sdkKey, apiBaseUrl)
22+
private val edgeDBApi: DVCEdgeDBApi = DVCEdgeDBApiClient().initialize(sdkKey, apiBaseUrl, context)
2223
private val configMutex = Mutex()
2324

2425
private fun <T> getResponseHandler(response: Response<T>): T {
@@ -88,16 +89,21 @@ internal class Request constructor(sdkKey: String, apiBaseUrl: String, eventsBas
8889
}
8990
.flowOn(Dispatchers.Default)
9091
.retryWhen { cause, attempt ->
91-
if ((cause is DVCRequestException && !cause.isRetryable) || attempt > 4) {
92-
return@retryWhen false
92+
if (cause is DVCRequestException) {
93+
if (!cause.isRetryable || attempt > 4) {
94+
return@retryWhen false
95+
} else {
96+
delay(currentDelay)
97+
currentDelay = (currentDelay * delayFactor).coerceAtMost(maxDelay)
98+
DVCLogger.w(
99+
cause,
100+
"Request Config Failed. Retrying in %s seconds.", currentDelay / 1000
101+
)
102+
return@retryWhen true
103+
}
93104
} else {
94-
delay(currentDelay)
95-
currentDelay = (currentDelay * delayFactor).coerceAtMost(maxDelay)
96-
DVCLogger.w(
97-
cause,
98-
"Request Config Failed. Retrying in %s seconds.", currentDelay / 1000
99-
)
100-
return@retryWhen true
105+
DVCLogger.e(cause, cause.message)
106+
return@retryWhen false
101107
}
102108
}
103109
.collect{
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.devcycle.sdk.android.exception
2+
3+
import java.io.IOException
4+
5+
class NoNetworkException(message: String?) : IOException(message) { }
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.devcycle.sdk.android.interceptor
2+
3+
import android.content.Context
4+
import android.net.ConnectivityManager
5+
import android.net.NetworkCapabilities
6+
import com.devcycle.sdk.android.exception.NoNetworkException
7+
import okhttp3.Interceptor
8+
import okhttp3.Response
9+
import java.io.IOException
10+
11+
class NetworkConnectionInterceptor(context: Context): Interceptor {
12+
private val applicationContext: Context
13+
init {
14+
applicationContext = context
15+
}
16+
17+
@Throws(IOException::class)
18+
override fun intercept(chain: Interceptor.Chain): Response {
19+
var request = chain.request()
20+
if (isNetworkAvailable()) {
21+
throw NoNetworkException("No network connection is available")
22+
}
23+
return chain.proceed(request)
24+
}
25+
26+
private fun isNetworkAvailable(): Boolean {
27+
val connectivityManager =
28+
applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE)
29+
if (connectivityManager is ConnectivityManager) {
30+
val capabilities =
31+
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) ?: return false
32+
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
33+
return true
34+
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
35+
return true
36+
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
37+
return true
38+
}
39+
}
40+
return false
41+
}
42+
}

android-client-sdk/src/test/java/com/devcycle/sdk/android/api/DVCClientTests.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ class DVCClientTests {
271271
client.close()
272272
}
273273

274+
@ExperimentalCoroutinesApi
274275
@Test
275276
fun `ensure config requests are queued and executed later`() {
276277
val config = generateConfig("activate-flag", "Flag activated!", Variable.TypeEnum.STRING)

android-client-sdk/src/test/java/com/devcycle/sdk/android/api/EventQueueTests.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
package com.devcycle.sdk.android.api
22

3-
import com.devcycle.sdk.android.helpers.TestDVCLogger
4-
import com.devcycle.sdk.android.model.DVCUser
3+
import android.content.Context
54
import com.devcycle.sdk.android.model.Event
65
import com.devcycle.sdk.android.model.PopulatedUser
7-
import com.devcycle.sdk.android.util.DVCLogger
86
import kotlinx.coroutines.*
97
import kotlinx.coroutines.test.resetMain
108
import kotlinx.coroutines.test.setMain
119
import org.junit.Assert
1210
import org.junit.Test
1311
import org.junit.jupiter.api.AfterEach
1412
import org.junit.jupiter.api.BeforeEach
13+
import org.mockito.Mock
1514
import org.mockito.Mockito
1615
import java.math.BigDecimal
1716

17+
1818
class EventQueueTests {
1919

20+
private val mockContext: Context = Mockito.mock(Context::class.java)
21+
2022
@DelicateCoroutinesApi
2123
private val mainThreadSurrogate = newSingleThreadContext("UI thread")
2224

@@ -37,7 +39,7 @@ class EventQueueTests {
3739

3840
@Test
3941
fun `events are aggregated correctly`() {
40-
val request = Request("some-key", "http://fake.com", "http://fake.com")
42+
val request = Request("some-key", "http://fake.com", "http://fake.com", mockContext)
4143
val user = PopulatedUser("test")
4244
val eventQueue = EventQueue(request, { user }, CoroutineScope(Dispatchers.Default), 10000)
4345

0 commit comments

Comments
 (0)