Skip to content

Commit d802b30

Browse files
martinbonninBoD
andauthored
Introduce RetryStrategy (#6764)
* RetryOnError: allow to customize what exceptions are considered recoverable * Add RetryDelegate * fix tests * update docs * Update libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/interceptor/RetryOnErrorInterceptor.kt Co-authored-by: Benoit 'BoD' Lubek <BoD@JRAF.org> * code review * RetryDelegate -> RetryStrategy * update apidump * wording --------- Co-authored-by: Benoit 'BoD' Lubek <BoD@JRAF.org>
1 parent 34dc055 commit d802b30

File tree

14 files changed

+227
-129
lines changed

14 files changed

+227
-129
lines changed

docs/source/advanced/network-connectivity.mdx

Lines changed: 12 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22
title: Monitor the network state to reduce latency
33
---
44

5-
<ExperimentalFeature>
6-
7-
**Network Monitor APIs are currently [experimental](https://www.apollographql.com/docs/resources/product-launch-stages/#experimental-features) in Apollo Kotlin.** If you have feedback on them, please let us know via [GitHub issues](https://github.com/apollographql/apollo-kotlin/issues/new?assignees=&labels=Type%3A+Bug&template=bug_report.md) or in the [Kotlin Slack community](https://slack.kotl.in/).
8-
9-
</ExperimentalFeature>
105

116
Android and Apple targets provide APIs to monitor the network state of your device:
127

@@ -40,6 +35,7 @@ When a `NetworkMonitor` is configured, you can use `failFastIfOffline` to avoid
4035
// Opt-in `failFastIfOffline` on all queries
4136
val apolloClient = ApolloClient.Builder()
4237
.serverUrl("https://example.com/graphql")
38+
.retryOnErrorInterceptor(RetryOnErrorInterceptor(networkMonitor))
4339
.failFastIfOffline(true)
4440
.build()
4541

@@ -57,35 +53,20 @@ When a `NetworkMonitor` is configured, `retryOnError` uses `NetworkMonitor.waitF
5753

5854
### Customizing the retry algorithm
5955

60-
You can customize the retry algorithm further by defining your own interceptor:
56+
To customize the retry algorithm, use a `RetryStrategy`. The following example shows a `RetryStrategy` that uses exponential backoff and gives up after four tries:
6157

6258
```kotlin
6359
val apolloClient = ApolloClient.Builder()
64-
.retryOnErrorInterceptor(MyRetryOnErrorInterceptor())
65-
.build()
66-
67-
class MyRetryOnErrorInterceptor : ApolloInterceptor {
68-
object RetryException : Exception()
69-
70-
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
71-
var attempt = 0
72-
return chain.proceed(request).onEach {
73-
if (request.retryOnError == true && it.exception != null && it.exception is ApolloNetworkException) {
74-
throw RetryException
75-
} else {
76-
attempt = 0
77-
}
78-
}.retryWhen { cause, _ ->
79-
if (cause is RetryException) {
80-
attempt++
81-
delay(2.0.pow(attempt).seconds)
82-
true
83-
} else {
84-
// Not a RetryException, probably a programming error, pass it through
85-
false
60+
.retryOnErrorInterceptor(RetryOnErrorInterceptor(networkMonitor) { state, request, response, ->
61+
val exception = response.exception
62+
if (exception == null) {
63+
return@RetryOnErrorInterceptor false
8664
}
87-
}
88-
}
89-
}
65+
66+
delay(2.0.pow(state.attempt).seconds)
67+
state.attempt++
68+
return@RetryOnErrorInterceptor state.attempt < 4
69+
})
70+
.build()
9071
```
9172

libraries/apollo-api/api/apollo-api.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,10 @@ public final class com/apollographql/apollo/exception/ApolloNetworkException : c
13131313
public final fun getPlatformCause ()Ljava/lang/Object;
13141314
}
13151315

1316+
public final class com/apollographql/apollo/exception/ApolloOfflineException : com/apollographql/apollo/exception/ApolloException {
1317+
public fun <init> ()V
1318+
}
1319+
13161320
public final class com/apollographql/apollo/exception/ApolloParseException : com/apollographql/apollo/exception/ApolloException {
13171321
public fun <init> ()V
13181322
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V

libraries/apollo-api/api/apollo-api.klib.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1183,6 +1183,10 @@ final class com.apollographql.apollo.exception/ApolloNetworkException : com.apol
11831183
final fun <get-platformCause>(): kotlin/Any? // com.apollographql.apollo.exception/ApolloNetworkException.platformCause.<get-platformCause>|<get-platformCause>(){}[0]
11841184
}
11851185

1186+
final class com.apollographql.apollo.exception/ApolloOfflineException : com.apollographql.apollo.exception/ApolloException { // com.apollographql.apollo.exception/ApolloOfflineException|null[0]
1187+
constructor <init>() // com.apollographql.apollo.exception/ApolloOfflineException.<init>|<init>(){}[0]
1188+
}
1189+
11861190
final class com.apollographql.apollo.exception/ApolloParseException : com.apollographql.apollo.exception/ApolloException { // com.apollographql.apollo.exception/ApolloParseException|null[0]
11871191
constructor <init>(kotlin/String? = ..., kotlin/Throwable? = ...) // com.apollographql.apollo.exception/ApolloParseException.<init>|<init>(kotlin.String?;kotlin.Throwable?){}[0]
11881192
}

libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/ApolloRequest.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ private constructor(
3737
override val canBeBatched: Boolean?,
3838
override val ignoreUnknownKeys: Boolean?,
3939
val ignoreApolloClientHttpHeaders: Boolean?,
40-
@ApolloExperimental
4140
val retryOnError: Boolean?,
42-
@ApolloExperimental
4341
val failFastIfOffline: Boolean?,
4442
val sendEnhancedClientAwareness: Boolean,
4543
) : ExecutionOptions {
@@ -90,10 +88,8 @@ private constructor(
9088
private set
9189
var ignoreApolloClientHttpHeaders: Boolean? = null
9290
private set
93-
@ApolloExperimental
9491
var retryOnError: Boolean? = null
9592
private set
96-
@ApolloExperimental
9793
var failFastIfOffline: Boolean? = null
9894
private set
9995
var sendEnhancedClientAwareness: Boolean = true
@@ -151,12 +147,10 @@ private constructor(
151147
this.ignoreUnknownKeys = ignoreUnknownKeys
152148
}
153149

154-
@ApolloExperimental
155150
fun retryOnError(retryOnError: Boolean?): Builder<D> = apply {
156151
this.retryOnError = retryOnError
157152
}
158153

159-
@ApolloExperimental
160154
fun failFastIfOffline(failFastIfOffline: Boolean?): Builder<D> = apply {
161155
this.failFastIfOffline = failFastIfOffline
162156
}

libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/exception/Exceptions.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// This is in the `exception` package and not `api.exception` to keep some compatibility with 2.x
22
package com.apollographql.apollo.exception
33

4+
import com.apollographql.apollo.annotations.ApolloDeprecatedSince
45
import com.apollographql.apollo.annotations.ApolloExperimental
56
import com.apollographql.apollo.annotations.ApolloInternal
67
import com.apollographql.apollo.api.Error
@@ -44,6 +45,13 @@ class ApolloNetworkException(
4445
/**
4546
* The device has been detected as offline
4647
*/
48+
class ApolloOfflineException: ApolloException("The device is offline")
49+
50+
/**
51+
* The device has been detected as offline
52+
*/
53+
@Deprecated("This exception is not used anymore, check for `ApolloOfflineException` instead.", level = DeprecationLevel.ERROR)
54+
@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0)
4755
data object OfflineException: IOException("The device is offline")
4856

4957
/**

libraries/apollo-runtime/api/android/apollo-runtime.api

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public final class com/apollographql/apollo/ApolloClient$Builder : com/apollogra
142142
public final fun networkTransport (Lcom/apollographql/apollo/network/NetworkTransport;)Lcom/apollographql/apollo/ApolloClient$Builder;
143143
public final fun removeHttpInterceptor (Lcom/apollographql/apollo/network/http/HttpInterceptor;)Lcom/apollographql/apollo/ApolloClient$Builder;
144144
public final fun removeInterceptor (Lcom/apollographql/apollo/interceptor/ApolloInterceptor;)Lcom/apollographql/apollo/ApolloClient$Builder;
145+
public final fun retryOnError (Ljava/lang/Boolean;)Lcom/apollographql/apollo/ApolloClient$Builder;
145146
public final fun retryOnError (Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo/ApolloClient$Builder;
146147
public final fun retryOnErrorInterceptor (Lcom/apollographql/apollo/interceptor/ApolloInterceptor;)Lcom/apollographql/apollo/ApolloClient$Builder;
147148
public fun sendApqExtensions (Ljava/lang/Boolean;)Lcom/apollographql/apollo/ApolloClient$Builder;
@@ -232,6 +233,18 @@ public final class com/apollographql/apollo/interceptor/AutoPersistedQueryInterc
232233

233234
public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorKt {
234235
public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor;
236+
public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;Lcom/apollographql/apollo/interceptor/RetryStrategy;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor;
237+
}
238+
239+
public final class com/apollographql/apollo/interceptor/RetryState {
240+
public fun <init> (Lcom/apollographql/apollo/network/NetworkMonitor;)V
241+
public final fun getAttempt ()I
242+
public final fun getNetworkMonitor ()Lcom/apollographql/apollo/network/NetworkMonitor;
243+
public final fun setAttempt (I)V
244+
}
245+
246+
public abstract interface class com/apollographql/apollo/interceptor/RetryStrategy {
247+
public abstract fun shouldRetry (Lcom/apollographql/apollo/interceptor/RetryState;Lcom/apollographql/apollo/api/ApolloRequest;Lcom/apollographql/apollo/api/ApolloResponse;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
235248
}
236249

237250
public final class com/apollographql/apollo/network/IncrementalDeliveryProtocol : java/lang/Enum {

libraries/apollo-runtime/api/apollo-runtime.klib.api

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ final enum class com.apollographql.apollo.network/IncrementalDeliveryProtocol :
3232
final fun values(): kotlin/Array<com.apollographql.apollo.network/IncrementalDeliveryProtocol> // com.apollographql.apollo.network/IncrementalDeliveryProtocol.values|values#static(){}[0]
3333
}
3434

35+
abstract fun interface com.apollographql.apollo.interceptor/RetryStrategy { // com.apollographql.apollo.interceptor/RetryStrategy|null[0]
36+
abstract suspend fun shouldRetry(com.apollographql.apollo.interceptor/RetryState, com.apollographql.apollo.api/ApolloRequest<*>, com.apollographql.apollo.api/ApolloResponse<*>): kotlin/Boolean // com.apollographql.apollo.interceptor/RetryStrategy.shouldRetry|shouldRetry(com.apollographql.apollo.interceptor.RetryState;com.apollographql.apollo.api.ApolloRequest<*>;com.apollographql.apollo.api.ApolloResponse<*>){}[0]
37+
}
38+
3539
abstract interface <#A: com.apollographql.apollo.api/Operation.Data> com.apollographql.apollo.network.websocket/SubscriptionParser { // com.apollographql.apollo.network.websocket/SubscriptionParser|null[0]
3640
abstract fun parse(kotlin/Any?): com.apollographql.apollo.api/ApolloResponse<#A>? // com.apollographql.apollo.network.websocket/SubscriptionParser.parse|parse(kotlin.Any?){}[0]
3741
}
@@ -227,6 +231,17 @@ final class com.apollographql.apollo.interceptor/AutoPersistedQueryInterceptor :
227231
}
228232
}
229233

234+
final class com.apollographql.apollo.interceptor/RetryState { // com.apollographql.apollo.interceptor/RetryState|null[0]
235+
constructor <init>(com.apollographql.apollo.network/NetworkMonitor?) // com.apollographql.apollo.interceptor/RetryState.<init>|<init>(com.apollographql.apollo.network.NetworkMonitor?){}[0]
236+
237+
final val networkMonitor // com.apollographql.apollo.interceptor/RetryState.networkMonitor|{}networkMonitor[0]
238+
final fun <get-networkMonitor>(): com.apollographql.apollo.network/NetworkMonitor? // com.apollographql.apollo.interceptor/RetryState.networkMonitor.<get-networkMonitor>|<get-networkMonitor>(){}[0]
239+
240+
final var attempt // com.apollographql.apollo.interceptor/RetryState.attempt|{}attempt[0]
241+
final fun <get-attempt>(): kotlin/Int // com.apollographql.apollo.interceptor/RetryState.attempt.<get-attempt>|<get-attempt>(){}[0]
242+
final fun <set-attempt>(kotlin/Int) // com.apollographql.apollo.interceptor/RetryState.attempt.<set-attempt>|<set-attempt>(kotlin.Int){}[0]
243+
}
244+
230245
final class com.apollographql.apollo.network.http/ApolloClientAwarenessInterceptor : com.apollographql.apollo.network.http/HttpInterceptor { // com.apollographql.apollo.network.http/ApolloClientAwarenessInterceptor|null[0]
231246
constructor <init>(kotlin/String, kotlin/String) // com.apollographql.apollo.network.http/ApolloClientAwarenessInterceptor.<init>|<init>(kotlin.String;kotlin.String){}[0]
232247

@@ -701,6 +716,7 @@ final class com.apollographql.apollo/ApolloClient : com.apollographql.apollo.api
701716
final fun networkTransport(com.apollographql.apollo.network/NetworkTransport?): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.networkTransport|networkTransport(com.apollographql.apollo.network.NetworkTransport?){}[0]
702717
final fun removeHttpInterceptor(com.apollographql.apollo.network.http/HttpInterceptor): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.removeHttpInterceptor|removeHttpInterceptor(com.apollographql.apollo.network.http.HttpInterceptor){}[0]
703718
final fun removeInterceptor(com.apollographql.apollo.interceptor/ApolloInterceptor): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.removeInterceptor|removeInterceptor(com.apollographql.apollo.interceptor.ApolloInterceptor){}[0]
719+
final fun retryOnError(kotlin/Boolean?): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.retryOnError|retryOnError(kotlin.Boolean?){}[0]
704720
final fun retryOnError(kotlin/Function1<com.apollographql.apollo.api/ApolloRequest<*>, kotlin/Boolean>?): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.retryOnError|retryOnError(kotlin.Function1<com.apollographql.apollo.api.ApolloRequest<*>,kotlin.Boolean>?){}[0]
705721
final fun retryOnErrorInterceptor(com.apollographql.apollo.interceptor/ApolloInterceptor?): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.retryOnErrorInterceptor|retryOnErrorInterceptor(com.apollographql.apollo.interceptor.ApolloInterceptor?){}[0]
706722
final fun sendApqExtensions(kotlin/Boolean?): com.apollographql.apollo/ApolloClient.Builder // com.apollographql.apollo/ApolloClient.Builder.sendApqExtensions|sendApqExtensions(kotlin.Boolean?){}[0]
@@ -787,6 +803,7 @@ final fun (com.apollographql.apollo.network/NetworkTransport).com.apollographql.
787803
final fun (com.apollographql.apollo.network/NetworkTransport).com.apollographql.apollo.network.ws/closeConnection(kotlin/Throwable) // com.apollographql.apollo.network.ws/closeConnection|closeConnection@com.apollographql.apollo.network.NetworkTransport(kotlin.Throwable){}[0]
788804
final fun <#A: kotlin/Any?> (com.apollographql.apollo.api/MutableExecutionOptions<#A>).com.apollographql.apollo/conflateFetchPolicyInterceptorResponses(kotlin/Boolean): #A // com.apollographql.apollo/conflateFetchPolicyInterceptorResponses|conflateFetchPolicyInterceptorResponses@com.apollographql.apollo.api.MutableExecutionOptions<0:0>(kotlin.Boolean){0§<kotlin.Any?>}[0]
789805
final fun com.apollographql.apollo.interceptor/RetryOnErrorInterceptor(com.apollographql.apollo.network/NetworkMonitor): com.apollographql.apollo.interceptor/ApolloInterceptor // com.apollographql.apollo.interceptor/RetryOnErrorInterceptor|RetryOnErrorInterceptor(com.apollographql.apollo.network.NetworkMonitor){}[0]
806+
final fun com.apollographql.apollo.interceptor/RetryOnErrorInterceptor(com.apollographql.apollo.network/NetworkMonitor, com.apollographql.apollo.interceptor/RetryStrategy): com.apollographql.apollo.interceptor/ApolloInterceptor // com.apollographql.apollo.interceptor/RetryOnErrorInterceptor|RetryOnErrorInterceptor(com.apollographql.apollo.network.NetworkMonitor;com.apollographql.apollo.interceptor.RetryStrategy){}[0]
790807
final fun com.apollographql.apollo.network.http/DefaultHttpEngine(kotlin/Long = ...): com.apollographql.apollo.network.http/HttpEngine // com.apollographql.apollo.network.http/DefaultHttpEngine|DefaultHttpEngine(kotlin.Long){}[0]
791808
final fun com.apollographql.apollo.network.websocket/WebSocketEngine(): com.apollographql.apollo.network.websocket/WebSocketEngine // com.apollographql.apollo.network.websocket/WebSocketEngine|WebSocketEngine(){}[0]
792809

libraries/apollo-runtime/api/jvm/apollo-runtime.api

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ public final class com/apollographql/apollo/ApolloClient$Builder : com/apollogra
142142
public final fun networkTransport (Lcom/apollographql/apollo/network/NetworkTransport;)Lcom/apollographql/apollo/ApolloClient$Builder;
143143
public final fun removeHttpInterceptor (Lcom/apollographql/apollo/network/http/HttpInterceptor;)Lcom/apollographql/apollo/ApolloClient$Builder;
144144
public final fun removeInterceptor (Lcom/apollographql/apollo/interceptor/ApolloInterceptor;)Lcom/apollographql/apollo/ApolloClient$Builder;
145+
public final fun retryOnError (Ljava/lang/Boolean;)Lcom/apollographql/apollo/ApolloClient$Builder;
145146
public final fun retryOnError (Lkotlin/jvm/functions/Function1;)Lcom/apollographql/apollo/ApolloClient$Builder;
146147
public final fun retryOnErrorInterceptor (Lcom/apollographql/apollo/interceptor/ApolloInterceptor;)Lcom/apollographql/apollo/ApolloClient$Builder;
147148
public fun sendApqExtensions (Ljava/lang/Boolean;)Lcom/apollographql/apollo/ApolloClient$Builder;
@@ -232,6 +233,18 @@ public final class com/apollographql/apollo/interceptor/AutoPersistedQueryInterc
232233

233234
public final class com/apollographql/apollo/interceptor/RetryOnErrorInterceptorKt {
234235
public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor;
236+
public static final fun RetryOnErrorInterceptor (Lcom/apollographql/apollo/network/NetworkMonitor;Lcom/apollographql/apollo/interceptor/RetryStrategy;)Lcom/apollographql/apollo/interceptor/ApolloInterceptor;
237+
}
238+
239+
public final class com/apollographql/apollo/interceptor/RetryState {
240+
public fun <init> (Lcom/apollographql/apollo/network/NetworkMonitor;)V
241+
public final fun getAttempt ()I
242+
public final fun getNetworkMonitor ()Lcom/apollographql/apollo/network/NetworkMonitor;
243+
public final fun setAttempt (I)V
244+
}
245+
246+
public abstract interface class com/apollographql/apollo/interceptor/RetryStrategy {
247+
public abstract fun shouldRetry (Lcom/apollographql/apollo/interceptor/RetryState;Lcom/apollographql/apollo/api/ApolloRequest;Lcom/apollographql/apollo/api/ApolloResponse;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
235248
}
236249

237250
public final class com/apollographql/apollo/network/IncrementalDeliveryProtocol : java/lang/Enum {

libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/ApolloCall.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,8 @@ class ApolloCall<D : Operation.Data> internal constructor(
5656

5757
val ignoreApolloClientHttpHeaders: Boolean? get() = requestBuilder.ignoreApolloClientHttpHeaders
5858

59-
@ApolloExperimental
6059
val retryOnError: Boolean? get() = requestBuilder.retryOnError
61-
@ApolloExperimental
60+
6261
val failFastIfOffline: Boolean? get() = requestBuilder.failFastIfOffline
6362

6463
fun failFastIfOffline(failFastIfOffline: Boolean?) = apply {
@@ -105,7 +104,6 @@ class ApolloCall<D : Operation.Data> internal constructor(
105104
requestBuilder.ignoreUnknownKeys(ignoreUnknownKeys)
106105
}
107106

108-
@ApolloExperimental
109107
fun retryOnError(retryOnError: Boolean?): ApolloCall<D> = apply {
110108
requestBuilder.retryOnError(retryOnError)
111109
}
@@ -144,7 +142,6 @@ class ApolloCall<D : Operation.Data> internal constructor(
144142
*
145143
* The returned [Flow] has [kotlinx.coroutines.channels.Channel.UNLIMITED] buffering so that no response is missed in the case of a slow consumer. Use [kotlinx.coroutines.flow.buffer] to change that behaviour.
146144
*
147-
* @see toFlowV3
148145
* @see ApolloClient.Builder.dispatcher
149146
*/
150147
fun toFlow(): Flow<ApolloResponse<D>> {
@@ -178,7 +175,6 @@ class ApolloCall<D : Operation.Data> internal constructor(
178175
*
179176
* @throws ApolloException if the call returns zero or multiple valid GraphQL responses.
180177
*
181-
* @see executeV3
182178
* @see ApolloClient.Builder.dispatcher
183179
*/
184180
suspend fun execute(): ApolloResponse<D> {

0 commit comments

Comments
 (0)