1
1
package com.featurevisor.sdk
2
2
3
3
import com.featurevisor.types.DatafileContent
4
+ import com.featurevisor.types.DatafileFetchResult
5
+ import kotlinx.coroutines.CoroutineScope
6
+ import kotlinx.coroutines.Job
7
+ import kotlinx.coroutines.delay
8
+ import kotlinx.coroutines.launch
4
9
import kotlinx.serialization.decodeFromString
5
- import java.io.IOException
6
- import okhttp3.*
7
10
import kotlinx.serialization.json.Json
11
+ import okhttp3.*
8
12
import okhttp3.HttpUrl.Companion.toHttpUrl
9
- import java.lang.IllegalArgumentException
13
+ import java.io.IOException
14
+ import java.net.ConnectException
15
+ import java.net.UnknownHostException
10
16
11
17
// MARK: - Fetch datafile content
12
18
@Throws(IOException ::class )
13
- internal fun FeaturevisorInstance.fetchDatafileContent (
19
+ internal fun fetchDatafileContentJob (
20
+ url : String ,
21
+ logger : Logger ? ,
22
+ coroutineScope : CoroutineScope ,
23
+ retryCount : Int = 3, // Retry count
24
+ retryInterval : Long = 300L, // Retry interval in milliseconds
25
+ handleDatafileFetch : DatafileFetchHandler ? = null,
26
+ completion : (Result <DatafileFetchResult >) -> Unit ,
27
+ ): Job {
28
+ val job = Job ()
29
+ coroutineScope.launch(job) {
30
+ fetchDatafileContent(
31
+ url = url,
32
+ handleDatafileFetch = handleDatafileFetch,
33
+ completion = completion,
34
+ retryCount = retryCount,
35
+ retryInterval = retryInterval,
36
+ job = job,
37
+ logger = logger,
38
+ )
39
+ }
40
+ return job
41
+ }
42
+
43
+ internal suspend fun fetchDatafileContent (
14
44
url : String ,
45
+ logger : Logger ? = null,
46
+ retryCount : Int = 1,
47
+ retryInterval : Long = 0L,
48
+ job : Job ? = null,
15
49
handleDatafileFetch : DatafileFetchHandler ? = null,
16
- completion : (Result <DatafileContent >) -> Unit ,
50
+ completion : (Result <DatafileFetchResult >) -> Unit ,
17
51
) {
18
52
handleDatafileFetch?.let { handleFetch ->
19
- val result = handleFetch(url)
20
- completion(result)
53
+ for (attempt in 0 until retryCount) {
54
+ if (job != null && (job.isCancelled || job.isActive.not ())) {
55
+ completion(Result .failure(FeaturevisorError .FetchingDataFileCancelled ))
56
+ break
57
+ }
58
+
59
+ val result = handleFetch(url)
60
+ result.fold(
61
+ onSuccess = {
62
+ completion(Result .success(DatafileFetchResult (it, " " )))
63
+ return
64
+ },
65
+ onFailure = { exception ->
66
+ if (attempt < retryCount - 1 ) {
67
+ logger?.error(exception.localizedMessage)
68
+ delay(retryInterval)
69
+ } else {
70
+ completion(Result .failure(exception))
71
+ }
72
+ }
73
+ )
74
+ }
21
75
} ? : run {
22
- fetchDatafileContentFromUrl(url, completion)
76
+ fetchDatafileContentFromUrl(
77
+ url = url,
78
+ completion = completion,
79
+ retryCount = retryCount,
80
+ retryInterval = retryInterval,
81
+ job = job,
82
+ logger = logger,
83
+ )
23
84
}
24
85
}
25
86
26
- private fun fetchDatafileContentFromUrl (
87
+ const val BODY_BYTE_COUNT = 1000000L
88
+ private val client = OkHttpClient ()
89
+
90
+ private suspend fun fetchDatafileContentFromUrl (
27
91
url : String ,
28
- completion : (Result <DatafileContent >) -> Unit ,
92
+ logger : Logger ? ,
93
+ retryCount : Int ,
94
+ retryInterval : Long ,
95
+ job : Job ? ,
96
+ completion : (Result <DatafileFetchResult >) -> Unit ,
29
97
) {
30
98
try {
31
99
val httpUrl = url.toHttpUrl()
@@ -34,55 +102,66 @@ private fun fetchDatafileContentFromUrl(
34
102
.addHeader(" Content-Type" , " application/json" )
35
103
.build()
36
104
37
- fetch(request, completion)
105
+ fetchWithRetry(
106
+ request = request,
107
+ completion = completion,
108
+ retryCount = retryCount,
109
+ retryInterval = retryInterval,
110
+ job = job,
111
+ logger = logger,
112
+ )
38
113
} catch (throwable: IllegalArgumentException ) {
39
114
completion(Result .failure(FeaturevisorError .InvalidUrl (url)))
115
+ } catch (e: Exception ) {
116
+ logger?.error(" Exception occurred during datafile fetch: ${e.message} " )
117
+ completion(Result .failure(e))
40
118
}
41
119
}
42
120
43
- const val BODY_BYTE_COUNT = 1000000L
44
- private inline fun fetch (
121
+ private suspend fun fetchWithRetry (
45
122
request : Request ,
46
- crossinline completion : (Result <DatafileContent >) -> Unit ,
123
+ logger : Logger ? ,
124
+ completion : (Result <DatafileFetchResult >) -> Unit ,
125
+ retryCount : Int ,
126
+ retryInterval : Long ,
127
+ job : Job ?
47
128
) {
48
- val client = OkHttpClient ()
49
- val call = client.newCall(request)
50
- call.enqueue(object : Callback {
51
- override fun onResponse (call : Call , response : Response ) {
129
+ for (attempt in 0 until retryCount) {
130
+ if (job != null && (job.isCancelled || job.isActive.not ())) {
131
+ completion(Result .failure(FeaturevisorError .FetchingDataFileCancelled ))
132
+ return
133
+ }
134
+
135
+ val call = client.newCall(request)
136
+ try {
137
+ val response = call.execute()
52
138
val responseBody = response.peekBody(BODY_BYTE_COUNT )
139
+ val responseBodyString = responseBody.string()
53
140
if (response.isSuccessful) {
54
- val json = Json {
55
- ignoreUnknownKeys = true
56
- }
57
- val responseBodyString = responseBody.string()
141
+ val json = Json { ignoreUnknownKeys = true }
58
142
FeaturevisorInstance .companionLogger?.debug(responseBodyString)
59
- try {
60
- val content = json.decodeFromString<DatafileContent >(responseBodyString)
61
- completion(Result .success(content))
62
- } catch (throwable: Throwable ) {
63
- completion(
64
- Result .failure(
65
- FeaturevisorError .UnparsableJson (
66
- responseBody.string(),
67
- response.message
68
- )
69
- )
70
- )
143
+ val content = json.decodeFromString<DatafileContent >(responseBodyString)
144
+ completion(Result .success(DatafileFetchResult (content, responseBodyString)))
145
+ return
146
+ } else {
147
+ if (attempt < retryCount - 1 ) {
148
+ logger?.error(" Request failed with message: ${response.message} " )
149
+ delay(retryInterval)
150
+ } else {
151
+ completion(Result .failure(FeaturevisorError .UnparsableJson (responseBodyString, response.message)))
71
152
}
153
+ }
154
+ } catch (e: IOException ) {
155
+ val isInternetException = e is ConnectException || e is UnknownHostException
156
+ if (attempt >= retryCount - 1 || isInternetException) {
157
+ completion(Result .failure(e))
72
158
} else {
73
- completion(
74
- Result .failure(
75
- FeaturevisorError .UnparsableJson (
76
- responseBody.string(),
77
- response.message
78
- )
79
- )
80
- )
159
+ logger?.error(" IOException occurred during request: ${e.message} " )
160
+ delay(retryInterval)
81
161
}
82
- }
83
-
84
- override fun onFailure (call : Call , e : IOException ) {
162
+ } catch (e: Exception ) {
163
+ logger?.error(" Exception occurred during request: ${e.message} " )
85
164
completion(Result .failure(e))
86
165
}
87
- })
166
+ }
88
167
}
0 commit comments