Skip to content

Commit 4bb9250

Browse files
committed
Run suspending calls within CoroutineDispatcher
1 parent 4860743 commit 4bb9250

File tree

5 files changed

+132
-106
lines changed

5 files changed

+132
-106
lines changed

retrofit-mock/src/main/java/retrofit2/mock/BehaviorDelegate.java

+3-7
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,9 @@ public <R> T returning(Call<R> call) {
7777

7878
Call<Object> adaptedCall = (Call<Object>) adapted;
7979
Continuation<Object> continuation = (Continuation<Object>) args[args.length - 1];
80-
try {
81-
return adapterInfo.wantsResponse
82-
? KotlinExtensions.awaitResponse(adaptedCall, continuation)
83-
: KotlinExtensions.await(adaptedCall, continuation);
84-
} catch (Exception e) {
85-
return KotlinExtensions.suspendAndThrow(e, continuation);
86-
}
80+
return adapterInfo.wantsResponse
81+
? KotlinExtensions.awaitResponse(adaptedCall, continuation)
82+
: KotlinExtensions.await(adaptedCall, continuation);
8783
});
8884
}
8985

retrofit/src/main/java/retrofit2/HttpServiceMethod.java

+17-25
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.lang.reflect.Type;
2525
import javax.annotation.Nullable;
2626
import kotlin.coroutines.Continuation;
27+
import kotlinx.coroutines.CoroutineDispatcher;
2728
import okhttp3.ResponseBody;
2829

2930
/** Adapts an invocation of an interface method into an HTTP call. */
@@ -94,7 +95,8 @@ static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotatio
9495
requestFactory,
9596
callFactory,
9697
responseConverter,
97-
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
98+
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
99+
retrofit.coroutineDispatcher);
98100
} else {
99101
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
100102
return (HttpServiceMethod<ResponseT, ReturnT>)
@@ -103,7 +105,8 @@ static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotatio
103105
callFactory,
104106
responseConverter,
105107
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
106-
continuationBodyNullable);
108+
continuationBodyNullable,
109+
retrofit.coroutineDispatcher);
107110
}
108111
}
109112

@@ -168,14 +171,17 @@ protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
168171

169172
static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
170173
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
174+
private final @Nullable CoroutineDispatcher coroutineDispatcher;
171175

172176
SuspendForResponse(
173177
RequestFactory requestFactory,
174178
okhttp3.Call.Factory callFactory,
175179
Converter<ResponseBody, ResponseT> responseConverter,
176-
CallAdapter<ResponseT, Call<ResponseT>> callAdapter) {
180+
CallAdapter<ResponseT, Call<ResponseT>> callAdapter,
181+
@Nullable CoroutineDispatcher coroutineDispatcher) {
177182
super(requestFactory, callFactory, responseConverter);
178183
this.callAdapter = callAdapter;
184+
this.coroutineDispatcher = coroutineDispatcher;
179185
}
180186

181187
@Override
@@ -186,28 +192,26 @@ protected Object adapt(Call<ResponseT> call, Object[] args) {
186192
Continuation<Response<ResponseT>> continuation =
187193
(Continuation<Response<ResponseT>>) args[args.length - 1];
188194

189-
// See SuspendForBody for explanation about this try/catch.
190-
try {
191-
return KotlinExtensions.awaitResponse(call, continuation);
192-
} catch (Exception e) {
193-
return KotlinExtensions.suspendAndThrow(e, continuation);
194-
}
195+
return KotlinExtensions.awaitResponse(call, this.coroutineDispatcher, continuation);
195196
}
196197
}
197198

198199
static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
199200
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
200201
private final boolean isNullable;
202+
private final @Nullable CoroutineDispatcher coroutineDispatcher;
201203

202204
SuspendForBody(
203205
RequestFactory requestFactory,
204206
okhttp3.Call.Factory callFactory,
205207
Converter<ResponseBody, ResponseT> responseConverter,
206208
CallAdapter<ResponseT, Call<ResponseT>> callAdapter,
207-
boolean isNullable) {
209+
boolean isNullable,
210+
@Nullable CoroutineDispatcher coroutineDispatcher) {
208211
super(requestFactory, callFactory, responseConverter);
209212
this.callAdapter = callAdapter;
210213
this.isNullable = isNullable;
214+
this.coroutineDispatcher = coroutineDispatcher;
211215
}
212216

213217
@Override
@@ -217,21 +221,9 @@ protected Object adapt(Call<ResponseT> call, Object[] args) {
217221
//noinspection unchecked Checked by reflection inside RequestFactory.
218222
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
219223

220-
// Calls to OkHttp Call.enqueue() like those inside await and awaitNullable can sometimes
221-
// invoke the supplied callback with an exception before the invoking stack frame can return.
222-
// Coroutines will intercept the subsequent invocation of the Continuation and throw the
223-
// exception synchronously. A Java Proxy cannot throw checked exceptions without them being
224-
// declared on the interface method. To avoid the synchronous checked exception being wrapped
225-
// in an UndeclaredThrowableException, it is intercepted and supplied to a helper which will
226-
// force suspension to occur so that it can be instead delivered to the continuation to
227-
// bypass this restriction.
228-
try {
229-
return isNullable
230-
? KotlinExtensions.awaitNullable(call, continuation)
231-
: KotlinExtensions.await(call, continuation);
232-
} catch (Exception e) {
233-
return KotlinExtensions.suspendAndThrow(e, continuation);
234-
}
224+
return isNullable
225+
? KotlinExtensions.awaitNullable(call, coroutineDispatcher, continuation)
226+
: KotlinExtensions.await(call, coroutineDispatcher, continuation);
235227
}
236228
}
237229
}

retrofit/src/main/java/retrofit2/KotlinExtensions.kt

+59-67
Original file line numberDiff line numberDiff line change
@@ -25,95 +25,87 @@ import kotlin.coroutines.intrinsics.intercepted
2525
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
2626
import kotlin.coroutines.resume
2727
import kotlin.coroutines.resumeWithException
28+
import kotlinx.coroutines.CoroutineDispatcher
29+
import kotlinx.coroutines.withContext
2830

2931
inline fun <reified T: Any> Retrofit.create(): T = create(T::class.java)
3032

31-
suspend fun <T : Any> Call<T>.await(): T {
32-
return suspendCancellableCoroutine { continuation ->
33-
continuation.invokeOnCancellation {
34-
cancel()
35-
}
36-
enqueue(object : Callback<T> {
37-
override fun onResponse(call: Call<T>, response: Response<T>) {
38-
if (response.isSuccessful) {
39-
val body = response.body()
40-
if (body == null) {
41-
val invocation = call.request().tag(Invocation::class.java)!!
42-
val method = invocation.method()
43-
val e = KotlinNullPointerException("Response from " +
33+
@JvmOverloads
34+
suspend fun <T : Any> Call<T>.await(coroutineDispatcher: CoroutineDispatcher? = null): T {
35+
return withContext(coroutineDispatcher ?: Dispatchers.Default) {
36+
suspendCancellableCoroutine { continuation ->
37+
continuation.invokeOnCancellation {
38+
cancel()
39+
}
40+
enqueue(object : Callback<T> {
41+
override fun onResponse(call: Call<T>, response: Response<T>) {
42+
if (response.isSuccessful) {
43+
val body = response.body()
44+
if (body == null) {
45+
val invocation = call.request().tag(Invocation::class.java)!!
46+
val method = invocation.method()
47+
val e = KotlinNullPointerException("Response from " +
4448
method.declaringClass.name +
4549
'.' +
4650
method.name +
4751
" was null but response body type was declared as non-null")
48-
continuation.resumeWithException(e)
52+
continuation.resumeWithException(e)
53+
} else {
54+
continuation.resume(body)
55+
}
4956
} else {
50-
continuation.resume(body)
57+
continuation.resumeWithException(HttpException(response))
5158
}
52-
} else {
53-
continuation.resumeWithException(HttpException(response))
5459
}
55-
}
5660

57-
override fun onFailure(call: Call<T>, t: Throwable) {
58-
continuation.resumeWithException(t)
59-
}
60-
})
61+
override fun onFailure(call: Call<T>, t: Throwable) {
62+
continuation.resumeWithException(t)
63+
}
64+
})
65+
}
6166
}
6267
}
6368

6469
@JvmName("awaitNullable")
65-
suspend fun <T : Any> Call<T?>.await(): T? {
66-
return suspendCancellableCoroutine { continuation ->
67-
continuation.invokeOnCancellation {
68-
cancel()
69-
}
70-
enqueue(object : Callback<T?> {
71-
override fun onResponse(call: Call<T?>, response: Response<T?>) {
72-
if (response.isSuccessful) {
73-
continuation.resume(response.body())
74-
} else {
75-
continuation.resumeWithException(HttpException(response))
76-
}
70+
suspend fun <T : Any> Call<T?>.await(coroutineDispatcher: CoroutineDispatcher?): T? {
71+
return withContext(coroutineDispatcher ?: Dispatchers.Default) {
72+
suspendCancellableCoroutine { continuation ->
73+
continuation.invokeOnCancellation {
74+
cancel()
7775
}
76+
enqueue(object : Callback<T?> {
77+
override fun onResponse(call: Call<T?>, response: Response<T?>) {
78+
if (response.isSuccessful) {
79+
continuation.resume(response.body())
80+
} else {
81+
continuation.resumeWithException(HttpException(response))
82+
}
83+
}
7884

79-
override fun onFailure(call: Call<T?>, t: Throwable) {
80-
continuation.resumeWithException(t)
81-
}
82-
})
85+
override fun onFailure(call: Call<T?>, t: Throwable) {
86+
continuation.resumeWithException(t)
87+
}
88+
})
89+
}
8390
}
8491
}
8592

86-
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
87-
return suspendCancellableCoroutine { continuation ->
88-
continuation.invokeOnCancellation {
89-
cancel()
90-
}
91-
enqueue(object : Callback<T> {
92-
override fun onResponse(call: Call<T>, response: Response<T>) {
93-
continuation.resume(response)
94-
}
95-
96-
override fun onFailure(call: Call<T>, t: Throwable) {
97-
continuation.resumeWithException(t)
93+
@JvmOverloads
94+
suspend fun <T> Call<T>.awaitResponse(coroutineDispatcher: CoroutineDispatcher? = null): Response<T> {
95+
return withContext(coroutineDispatcher ?: Dispatchers.Default) {
96+
suspendCancellableCoroutine { continuation ->
97+
continuation.invokeOnCancellation {
98+
cancel()
9899
}
99-
})
100-
}
101-
}
100+
enqueue(object : Callback<T> {
101+
override fun onResponse(call: Call<T>, response: Response<T>) {
102+
continuation.resume(response)
103+
}
102104

103-
/**
104-
* Force the calling coroutine to suspend before throwing [this].
105-
*
106-
* This is needed when a checked exception is synchronously caught in a [java.lang.reflect.Proxy]
107-
* invocation to avoid being wrapped in [java.lang.reflect.UndeclaredThrowableException].
108-
*
109-
* The implementation is derived from:
110-
* https://github.com/Kotlin/kotlinx.coroutines/pull/1667#issuecomment-556106349
111-
*/
112-
internal suspend fun Exception.suspendAndThrow(): Nothing {
113-
suspendCoroutineUninterceptedOrReturn<Nothing> { continuation ->
114-
Dispatchers.Default.dispatch(continuation.context) {
115-
continuation.intercepted().resumeWithException(this@suspendAndThrow)
105+
override fun onFailure(call: Call<T>, t: Throwable) {
106+
continuation.resumeWithException(t)
107+
}
108+
})
116109
}
117-
COROUTINE_SUSPENDED
118110
}
119111
}

retrofit/src/main/java/retrofit2/Retrofit.java

+21-2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import java.util.concurrent.ConcurrentHashMap;
3535
import java.util.concurrent.Executor;
3636
import javax.annotation.Nullable;
37+
import kotlinx.coroutines.CoroutineDispatcher;
38+
import kotlinx.coroutines.Dispatchers;
3739
import okhttp3.HttpUrl;
3840
import okhttp3.OkHttpClient;
3941
import okhttp3.RequestBody;
@@ -74,6 +76,7 @@ public final class Retrofit {
7476
final int defaultCallAdapterFactoriesSize;
7577
final @Nullable Executor callbackExecutor;
7678
final boolean validateEagerly;
79+
final @Nullable CoroutineDispatcher coroutineDispatcher;
7780

7881
Retrofit(
7982
okhttp3.Call.Factory callFactory,
@@ -83,7 +86,8 @@ public final class Retrofit {
8386
List<CallAdapter.Factory> callAdapterFactories,
8487
int defaultCallAdapterFactoriesSize,
8588
@Nullable Executor callbackExecutor,
86-
boolean validateEagerly) {
89+
boolean validateEagerly,
90+
@Nullable CoroutineDispatcher coroutineDispatcher) {
8791
this.callFactory = callFactory;
8892
this.baseUrl = baseUrl;
8993
this.converterFactories = converterFactories; // Copy+unmodifiable at call site.
@@ -92,6 +96,7 @@ public final class Retrofit {
9296
this.defaultCallAdapterFactoriesSize = defaultCallAdapterFactoriesSize;
9397
this.callbackExecutor = callbackExecutor;
9498
this.validateEagerly = validateEagerly;
99+
this.coroutineDispatcher = coroutineDispatcher;
95100
}
96101

97102
/**
@@ -437,6 +442,7 @@ public static final class Builder {
437442
private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>();
438443
private @Nullable Executor callbackExecutor;
439444
private boolean validateEagerly;
445+
private @Nullable CoroutineDispatcher coroutineDispatcher;
440446

441447
public Builder() {}
442448

@@ -610,6 +616,18 @@ public Builder validateEagerly(boolean validateEagerly) {
610616
return this;
611617
}
612618

619+
/**
620+
* Suspending method call implementations will be run on this {@link CoroutineDispatcher}, if
621+
* provided. If no dispatcher is provided, calls are run on {@link Dispatchers#getDefault()}.
622+
*
623+
* <p>Network requests are still run on the {@link OkHttpClient}'s {@link okhttp3.Dispatcher},
624+
* but {@link okhttp3.Call.Factory} invocations will be run on this dispatcher.
625+
*/
626+
public Builder coroutineDispatcher(@Nullable CoroutineDispatcher coroutineDispatcher) {
627+
this.coroutineDispatcher = coroutineDispatcher;
628+
return this;
629+
}
630+
613631
/**
614632
* Create the {@link Retrofit} instance using the configured values.
615633
*
@@ -660,7 +678,8 @@ public Retrofit build() {
660678
unmodifiableList(callAdapterFactories),
661679
defaultCallAdapterFactories.size(),
662680
callbackExecutor,
663-
validateEagerly);
681+
validateEagerly,
682+
coroutineDispatcher);
664683
}
665684
}
666685
}

0 commit comments

Comments
 (0)