If I create a `Job` with a parent (parent is long-...
# coroutines
d
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
yes
since the parent is required to cancel all child jobs when it itself is cancelled
d
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
Do you return the
Job
of each request to the caller? If so, why?
d
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
typically you should adapt the callback to a suspend function, see https://medium.com/@elizarov/callbacks-and-kotlin-flows-2b53aa2525cf
s
To avoid tracking your Jobs in your own code, use CoroutineScopes instead.
o
tbh, I think that for this style of interface, you should be launching an independent job for each callback, with no parent
d
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
well,
CoroutineScope
always contains a Job
d
I know that.
o
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
Even if I use
CoroutineScope
it doesn't solve my problem, because now I need to properly dispose of that.
s
CoroutineScopes do solve your issues. They keep track of Jobs and cancel/dispose of them properly
d
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
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
I can't return a future.
ThirdPartyInterface
is a callback-based API.
someOperation
is an implemenation of a third-party interface.
o
is it possible for us to see the full interface?
c
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
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
That's quite optimistic in a network scenario 😄
o
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
Flow
might be closer to what you’re wanting than a raw coroutine
s
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
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
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
Thanks everyone for your replies
s
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
Yes, I know about CoroutineScope
s
But won’t it solve your problem of possibly leaking Jobs?
d
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
Ah! I see! 🙂