Hello! Does anyone know how to ensure that a suspe...
# coroutines
r
Hello! Does anyone know how to ensure that a suspend lambda parameter is really
suspend
and not a non-suspend one ? If possible it could be an annotation where the ensuring would happen. My use case is:
Copy code
suspend fun <T> execute(request: suspend () -> T): T {
  // code here
}

...
//code using 'execute'
execute(mySuspendLambda) // OK
execute { 1+1 } // warning or error
I know that Kotlin reflect lib brings
request::reflect.isSuspend
but I want to check that at compile time and not runtime. And this lib is not a small one.
j
Why would you want to enforce that? Any non-suspending piece of code can artificially be made suspend. Nothing guarantees that a
suspend
function really suspends
r
Yep I know: any lambda can be converted this way
suspend { myLambda() }
. In fact in a refactorization of our API interface we forgot to add the
suspend
key word in some methods which are not testable in our dev environment. We got some Retrofit runtime error for one method testable in dev env (luckily!) So I would like to ensure that it won't happen again with such compile check. If someone workaround the
execute
method with the suspend conversion, it's fine. But I wondered if it is possible to have such a strict check of a suspend or non suspend lambda :)
s
What Joffrey said. Even if the provided lambda was declared as suspend, there is no guarantee that that lambda would actually suspend at all. Eg
{ 1+someBlockingFunction() }
is exactly the same as
suspend { 1+someBlockingFunction() }
. If the lambda's code does or must suspend at some point in time, then it calls 'suspend' code and the lambda itself must be declared as 'suspend'. Eg this is not possible:
{ 1+someSuspendFuntion() }
and it must be
suspend { 1+someSuspendFuntion() }
.
r
So is it possible to the compiler to know if the
request: suspend () -> T
is a simple
() -> T
?
s
Not sure if it can examine the actual type of the value/lambda passed to
request
or not, but I'm not sure what you are trying to solve. Even if a suspend lambda was passed to
request
, it still can contain (just) blocking (and non-suspending) code, which can never be cancelled.
And if the value/lambda passed to
request
must be able to be cancelled, it must be able to call other suspend functions and that can only happen if the passed value/lambda is declared as suspend as well.
r
Yep but
execute { ... /* non suspend like 1 + 1 */ ... }
and
execute { ... delay(100) ... }
are both allowed now . In this 2nd example AS/IDEA show a ⏸️ icon in the left column specifically on the
delay
part (I was wondering how it is done). I think I will pass on this enforcement for now, thank for your time 👍
k
You'll have to pass on that enforcement for now, and for ever, there's no way to know if the lambda will actually suspend, the code there could be blocking but not suspend as Anton and Joffrey mentioned, and under hood all suspend functions eventually invoke "blocking" code but on separate contexts (e.g. the
IO
dispatcher), so even if you wanted calls to suspend functions nothing forbids someone to do the following with your 1+1 example:
Copy code
suspend fun onePlusOne() = withContext(<http://Dispatchers.IO|Dispatchers.IO>) { 1 + 1 }

suspend fun execute<T>(request: suspend () -> T): T {
    // ...
}

execute(mySuspendLambda) // OK
execute { onePlusOne() } // OK? but still running "blocking" code, on a different context
j
In fact in a refactorization of our API interface we forgot to add the
suspend
key word in some methods which are not testable in our dev environment.
Then I think what you should try to fix instead is to make those functions testable, and tested
s
@Rajar Also note that if you pass a plain
{ 1 + 1 }
to the
execute
function's
request
parameter, the
request
parameter still gets a suspend lambda! The compiler just wraps it inside a suspend lambda
Ie
{ 1 + 1}
just becomes
suspend { passedLambda.invoke() }
..... (where
passedLambda
is
{ 1 + 1}
)
r
Then I think what you should try to fix instead is to make those functions testable, and tested
Yep, a way to do so would be to ensure each
@POST/@GET/etc.
method is a suspend one though a test.
k
Yes, also note that even if you receive a blocking call inside your lambda what matters is where you actually execute your suspend function, i.e. the coroutine context, for this case if your
execute
function expects to run IO operations (such as network calls) you can also switch the context with
withContext(<http://Dispatchers.IO|Dispatchers.IO>)
so that
execute
is main thread safe
s
where you actually execute your suspend function
Yup. Coroutines can not cancel blocking code.
j
Depends on the blocking code. If it supports thread interruption, the coroutine cancellations can be translated to interruptions with
runInterruptible
k
Yeah, but in the end
runInterruptible
is wrapping the code in a suspend function, hence creating a suspend point where Kotlin can cancel the execution
s
Yup, but that is a special case, when io is blocking or something similar, something that can be interrupted. But when you do
while (true) { }
, then that while loop will not get cancelled, runInterruptable or not.
k
You can make the
while (true) {}
cancellable by making the suspend function cooperative adding a suspend point in between iterations, as easy as calling
delay
or
yield
s
You can, but without those suspension points it is not cancellable. With these suspension points, the code is not longer entirely blocking, it became suspendable.
j
Yup, but that is a special case, when io is blocking or something similar, something that can be interrupted
Yep, that's why I said that it depends on the blocking code 🙂
In any case, I believe this discussion is off topic. The point is that OP is trying to fix a missing test issue by trying to artificially forbid a perfectly valid code to run, to help detect misuse. I don't think that's the right approach.
Yep, a way to do so would be to ensure each
@POST/@GET/etc.
method is a suspend one though a test.
@Rajar It still seems a bit artificial. Why do you need these functions to be
suspend
? If it is because it affects something else, then maybe you should test that this "something else" works as expected instead of trying to verify the presence of
suspend
here
r
I agree it's a bit artificial (but could solve a misusage) and now off topic. The right approach would be dedicated tests for specific oauth environments (unavailable to devs) and integration tests for the Retrofit part knowing the call adapter we are using (a.k.a the default one).
141 Views