-
Notifications
You must be signed in to change notification settings - Fork 7.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add first-party Kotlin coroutine suspend support #2886
Conversation
2a7db7a
to
3fd655b
Compare
if (requestFactory.isKotlinSuspendFunction) { | ||
Type[] parameterTypes = method.getGenericParameterTypes(); | ||
Type continuationType = parameterTypes[parameterTypes.length - 1]; | ||
Type innerType = Utils.getParameterLowerBound(0, (ParameterizedType) continuationType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't love this name, just cause inner often means nested and not parameter. Maybe call it asyncResponseType?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by just using responseType
which cleaned this section up a bit
@@ -32,8 +35,28 @@ | |||
*/ | |||
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations( | |||
Retrofit retrofit, Method method, RequestFactory requestFactory) { | |||
CallAdapter<ResponseT, ReturnT> callAdapter = createCallAdapter(retrofit, method); | |||
Type responseType = callAdapter.responseType(); | |||
CallAdapter<ResponseT, ReturnT> callAdapter = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a wrinkle: asymmetry on how this is set up feels awkward. No suggestions on how to do it better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'm willing to muck up the internals instead of inventing some elaborate abstraction that will only be used by coroutines. If we ever need to generalize we can, but I don't want to jump that gun.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've refined this a bit by using subtypes. This method is still gnarly because of all the shared logic, but the created classes are small and focused and don't involve crazy conditionals
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code actually reminds me of Retrofit internals circa 2012-2013... it can be cleaned up it's just a matter of effort but since it's isolated here for now I'm not worried about it.
Object continuation = args[args.length - 1]; | ||
if (continuationWantsResponse) { | ||
//noinspection unchecked Guaranteed by parseAnnotations above. | ||
return (ReturnT) KotlinExtensions.awaitResponse(call, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function name looks like it’ll be blocking but my assumption is that it'll return a promise. I gotta read more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It actually returns a union type of COROUTINE_SUSPENDED | T
which is either a marker value indication suspension to unwind the stack or the result if it was able to be computed synchronously. Since we're invoking Call.enqueue
it will always return COROUTINE_SUSPENDED
.
inline fun <reified T> Retrofit.create(): T = create(T::class.java) | ||
|
||
suspend fun <T : Any> Call<T>.await(): T { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is neat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And public API for people to use if they already have Call
s! Makes it a win-win really.
enqueue(object : Callback<T> { | ||
override fun onResponse(call: Call<T>, response: Response<T>) { | ||
if (response.isSuccessful) { | ||
// TODO handle nullability |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Presumably the callers T
can itself be nullable or not – like the parameter will be String
or String?
but to this code it's all just T
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which is a real problem in practice since an endpoint declared as suspend fun foo(): String
can get an HTTP 204 which produces a null body and leaks that into an otherwise non-null type.
We need something like a hand-written Kotlin metadata parser for return type nullability that doesn't pull in any deps or support anything except looking for exactly what we need. That way it's 1 method and not 10,000.
@@ -98,6 +101,10 @@ static RequestFactory parseAnnotations(Retrofit retrofit, Method method) { | |||
+ ") doesn't match expected count (" + handlers.length + ")"); | |||
} | |||
|
|||
if (isKotlinSuspendFunction) { | |||
// The Continuation is the last parameter and the handlers array contains null at that index. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering aloud: could you symmetrically synthesize the return type from this argument? It’s how the coroutine and non-coroutine flows could fit together.
interface ResponseReturner<>T {
void success (Response<T> response);
void failure (Throwable t);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess where I'm going is to imagine coroutines is actually a more general case and we can model regular returns within the coroutine machinery. Though this would only be for API symmetry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you're describing sounds like the callback factory that I originally modeled. It seems like a lot of work for something I expect no one except coroutines to use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for me! None of it leaks into the public API anyway so it doesn’t much matter.
One other way of potentially looking at this:
The coroutines support synthesizes a return type of Call<T>
or Call<Response<T>>
and just calls await()
for you
isKotlinSuspendFunction = true; | ||
return null; | ||
} | ||
} catch (NoClassDefFoundError ignored) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sneaky!
|
||
/** | ||
* This code path can only be tested from Java because Kotlin does not allow you specify a raw | ||
* Response type. Win! We still test this codepath for completeness. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is wonderful.
Is there an easy way to test that Retrofit works without the optional dependencies? Would help to defend against accidental introductions of Kotlin dependencies.
|
||
server.enqueue(MockResponse().setResponseCode(404)) | ||
|
||
try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
kotlin.test.assertFailsWith !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requires an extra dependency. Maybe i'll upgrade to JUnit 4.13 beta in another PR which has assertThrows
The optional dependencies are implicitly tested by the other modules which do not include them. For example, if the |
d325a9e
to
77af488
Compare
Thumbs up. The implicit coverage is good but I still sort of want to add something small and explicit – a test that asserts Kotlin isn't on the classpath and then does some Retrofit stuff. That way if/when we add indirect Kotlin dependencies (such as via Okio 2!) then we have something to catch if we regress. |
77af488
to
0692026
Compare
3486d8f
to
c395666
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fantastic
kotlin 1.3 is now released with coroutines 1.0 |
Yes. It will still be a few weeks before this lands, but it's at least unblocked. |
From what I see, this MR completely bypasses call adapters when using I'm currently using custom call adapters for centralising parsing of error body (and then throwing appropriate exceptions), smilarly to the official retrofit2 sample. Any chance we get alternative to this, some kind of adapter that is injected between here? |
Would it be possible to implement this in such a way that stacktraces are properly augmented with actual call sites? There is WIP being done on this in This would apparently require using either of |
5f161d6
to
09c8f2c
Compare
suspend fun <T : Any> Call<T>.await(): T { | ||
return suspendCancellableCoroutine { continuation -> | ||
continuation.invokeOnCancellation { | ||
cancel() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
super cool
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should probably test this. Can do in follow-up.
09c8f2c
to
b761518
Compare
fantastic work. so do we need to add a call adapter for this to work? i didn't see any documentation about this. |
No
…On Wed, Feb 27, 2019 at 4:57 AM Saman Sattari ***@***.***> wrote:
fantastic work. so do we need to add a call adapter for this to work? i
didn't see any documentation about this.
—
You are receiving this because you modified the open/close state.
Reply to this email directly, view it on GitHub
<#2886 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAEEEUtmrsyMoFaVJHR_vBVwHrzJQBOFks5vRlaDgaJpZM4Wkl1S>
.
|
JakeWharton, many thanks for your great work! May you help us how to implement |
I am not exactly sure what to do here, because cancelling a deferred job of retrofit doesn't cancel the network call, the api call seems to move to completion whether I cancelled it or not, is there a newer version of retrofit I can use which has this feature? Currently I am on retrofit 2.5.0. |
@droidluv this seems unrelated because 2.5.0 doesn't contain this PR. This PR is available on master (2.5.1-SNAPSHOT). Perhaps open new issues for any problems you experience |
@bradynpoulsen I apologise if I was unclear and vague, I was brought here from here, I realised the fix was on 2.5.1-SNAPSHOT |
I have a question about retrofit 2.5.1-SNAPSHOT and canceling request. I downloaded this version of retrofit from the repository |
A cancelation test was added in #3029 which verifies that canceling the coroutine will propagate to the underlying |
@JakeWharton I saw this test. Here you have my project's environment. Gradle:
Routes:
Server:
Main Activity:
After invoking above code I got these logs:
Test scenario: So could you tell me what could be the problem? |
Hey, I want to use the suspend function in my production ready project, but the suspend function is not yet in the stable branch. When do you plan to release 2.5.1? What issues are blocking the release? |
@JakeWharton Any update on when should we expect a stable release of this feature? |
Nope. Releases aren't done based on dates.
…On Sun, Apr 28, 2019, 12:22 PM wlara ***@***.***> wrote:
@JakeWharton <https://github.com/JakeWharton> Any update on when should
we expect a stable release of this feature?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2886 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAAQIEJC5ROJZC3CUC4NTT3PSXFLBANCNFSM4FUSLVJA>
.
|
I noticed, that in 2.5.1 in cases with
your code will fails with message, that your ConverterFactory cannot deserialize |
Result is an RxJava adapter concept for now
…On Sun, Apr 28, 2019, 6:23 PM Pavel Shorokhov ***@***.***> wrote:
I also noticed, that in 2.5.1 in cases with suspend CallAdapters does not
calling. ConverterFactory calls directly. So if you want to use you custom
CallAdapters, for example for Result:
suspend getUser(): Result<User>
your code will fails with message, that your ConverterFactory cannot
deserialize Result object (without creating any CallAdapter).
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#2886 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAAQIEOLJBKKXHOLAE2NWGDPSYPXZANCNFSM4FUSLVJA>
.
|
Thanks @JakeWharton for this awesome work! Would you consider a milestone release? It may promote more testing and inclusion in some non-critical projects. |
No description provided.