https://kotlinlang.org logo
#coroutines
Title
# coroutines
d

diesieben07

01/30/2020, 9:09 PM
If I create a
Job
with a parent (parent is long-running), is it a memory/resource leak to not complete or cancel the Job? Will the parent hold on to it and prevent it from being garbage collected?
o

octylFractal

01/30/2020, 9:10 PM
yes
since the parent is required to cancel all child jobs when it itself is cancelled
d

diesieben07

01/30/2020, 9:12 PM
Hm. That sucks 😄 How do I best deal with this situation? The parent job is a
SupervisorJob
for a service implementation. When the service gets a request, I start a child job for that request. This handle is handed out to third party code, how do I ensure that I clean up my job if the third party code fails / throws an exception?
s

streetsofboston

01/30/2020, 9:20 PM
Do you return the
Job
of each request to the caller? If so, why?
d

diesieben07

01/30/2020, 9:24 PM
Copy code
// third party code
interface Callback {
  fun onMessage(message: Any)
  fun onComplete()
}

// my code
class MyService : ThirdPartyInterface {
  val job = SupervisorJob()
  
  override fun someOperation(responseListener: Callback): Callback {
    val requestJob = Job(job)
    return object : Callback {
       // handle onMessage, etc. here. write responses to responseListener
       // this spawns processing coroutines using requestJob
       // as such I must ensure to properly dispose of it...
    }
  }
}
o

octylFractal

01/30/2020, 9:26 PM
typically you should adapt the callback to a suspend function, see https://medium.com/@elizarov/callbacks-and-kotlin-flows-2b53aa2525cf
s

streetsofboston

01/30/2020, 9:26 PM
To avoid tracking your Jobs in your own code, use CoroutineScopes instead.
o

octylFractal

01/30/2020, 9:28 PM
tbh, I think that for this style of interface, you should be launching an independent job for each callback, with no parent
d

diesieben07

01/30/2020, 9:28 PM
I want a parent job for all requests though, how do I do that with CoroutineScope?
How do I then close this service?
If I dont have a parent...
o

octylFractal

01/30/2020, 9:29 PM
well,
CoroutineScope
always contains a Job
d

diesieben07

01/30/2020, 9:29 PM
I know that.
o

octylFractal

01/30/2020, 9:29 PM
you can pass
SupervisorJob
to have a parent
doesn't really change anything about the cancellation though
I think I don't know enough to say what the appropriate step is here
d

diesieben07

01/30/2020, 9:30 PM
Even if I use
CoroutineScope
it doesn't solve my problem, because now I need to properly dispose of that.
s

streetsofboston

01/30/2020, 9:30 PM
CoroutineScopes do solve your issues. They keep track of Jobs and cancel/dispose of them properly
d

diesieben07

01/30/2020, 9:30 PM
I know about coroutine scope and I know how it works. But it doesn't solve the problem of having to hand out stuff to other code, which is written in Java and not aware of coroutines.
s

streetsofboston

01/30/2020, 9:31 PM
Let me take a quick peek at your code snippet and see how you could use CoroutineScopes to manage Jobs
You should not return Jobs… return Futures instead (CompletableFutures)
d

diesieben07

01/30/2020, 9:33 PM
I can't return a future.
ThirdPartyInterface
is a callback-based API.
someOperation
is an implemenation of a third-party interface.
o

octylFractal

01/30/2020, 9:33 PM
is it possible for us to see the full interface?
c

Casey Brooks

01/30/2020, 9:34 PM
The reason you’d typically want to hold onto a
Job
is so that you can cancel it in response to application lifecycles. Otherwise, coroutines will tidy themselves up. To handle other code, you should find some way to “bind” it to the world of coroutines. You can wrap a callback-based API into a coroutine with `suspendCancellableCoroutine`:
Copy code
suspendCancellableCoroutine<String> { cont ->
    someJavaMethodWithACallback { cont.resume(it) }
}
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/suspend-cancellable-coroutine.html
And before you say "you should use a coroutine-aware grpc generator", that is what I am working on.
@Casey Brooks
suspendCancellableCoroutine
requires me to be in a suspending function already. And it is also just a one-off. There can be many messages going in and out of the callbacks
o

octylFractal

01/30/2020, 9:37 PM
it looks like Listener allows you to bind
onCancel
/
onComplete
I would just bind those to closing the job, and not worry about it if those never get called
d

diesieben07

01/30/2020, 9:37 PM
That's quite optimistic in a network scenario 😄
o

octylFractal

01/30/2020, 9:38 PM
if those don't get called, I would consider that a bug in grpc
considering: Cancellations can be caused by timeouts, explicit cancellation by the client, network errors, etc.
even in a network scenario, grpc claims
onCancel
will be called
c

Casey Brooks

01/30/2020, 9:38 PM
Flow
might be closer to what you’re wanting than a raw coroutine
s

streetsofboston

01/30/2020, 9:39 PM
Copy code
class MyService : ThirdPartyInterface {
    val job = SupervisorJob()
    override fun someOperation(responseListener: Callback): Disposable {
        val scope = CoroutineScope(job)
        return object : Disposable {
            override fun dispose() {
                scope.cancel()
            }

        }
        scope.launch {
            // handle onMessage, etc. here. write responses to responseListener
            // this spawns processing coroutines using requestJob
            // as such I must ensure to properly dispose of it...
        }
    }
}
d

diesieben07

01/30/2020, 9:39 PM
Again, you are changing what I am returning... which I can't do.
thank you Octavia, it seems I was a bit to pessimistic maybe. I'll follow your advice.
s

streetsofboston

01/30/2020, 9:39 PM
You need something to be cancelled by the caller.
It the caller can’t cancel it, you can still return the Callback and have it properly cleaned up on exceptions or finishing of the requests
Copy code
class MyService : ThirdPartyInterface {
    val scope = CoroutineScope(SupervisorJob())
    override fun someOperation(responseListener: Callback): Callback {
        scope.launch {
            // handle onMessage, etc. here. write responses to responseListener
            // this spawns processing coroutines using 'scope'
            // as such I must ensure to properly dispose of it...
        }
        return responseListener
    }
}
d

diesieben07

01/30/2020, 9:42 PM
Thanks everyone for your replies
s

streetsofboston

01/30/2020, 9:44 PM
In my last example code above, when code inside the
launch
throws an exception, or just finishes, any Jobs associated with it will be cancelled/clean-up and garbage collected. The CoroutineScope will take care of that You may want to install a CoroutineExceptionHandler in the constructor of he
CoroutineScope
as well, to handle any unhandled/un-caught exceptions….
d

diesieben07

01/30/2020, 9:46 PM
Yes, I know about CoroutineScope
s

streetsofboston

01/30/2020, 9:46 PM
But won’t it solve your problem of possibly leaking Jobs?
d

diesieben07

01/30/2020, 9:48 PM
My problem was that I thought I didn't have a "I am done" callback on my third party interface, when in fact I do.
s

streetsofboston

01/30/2020, 9:50 PM
Ah! I see! 🙂
4 Views