I'm trying to close a resource that was lazily, as...
# coroutines
t
I'm trying to close a resource that was lazily, asynchronously created.
Copy code
val myResource: Deferred<Closeable> = coroutineScope.async(LAZY) { ... }
What is the best way to close that resource if it has been created? So far I have
Copy code
fun Deferred<Closeable>.close(coroutineScope: CoroutineScope) {
  when {
    isActive || isCompleted -> coroutineScope.launch { await().close() }
    else -> cancel()
  }
}
That seems really complicated for what I'm trying to do though
I'm wondering if I'm using
isActive
and
isCompleted
properly, and maybe if there is some standard-library function I missed
k
Copy code
suspend fun Deferred<Closeable>.close() {
  await().close()
}
t
^^ I considered that, but if my resource has not been created yet, then
await()
will create it just for it to be closed...
k
This is janky, but:
Copy code
fun Deferred<Closeable>.close() {
  val closeable = try {
    getCompleted()
  } catch (e: IllegalStateException) {
    // Suppress.
    null 
  }
  closeable?.close()
}
I wouldn’t ever use that in production code nor would I approve it in a PR, but I don’t know what your use case is. Hopefully this is helpful.
t
My use case is basically production code 😢
e
possibly
Copy code
myResource.invokeOnCompletion { e ->
    if (e == null) cleanup()
but LAZY is sorta tricky for other reasons: even if it's never started, its existence prolongs the parent scope/job
v
@Tian Tian098 there's a really cool idiom for this concept using flows. Essentially,
Copy code
flow {
  resource.use {
    emit(it)
    awaitCompletion()
  }
}
Now you have a cold flow that won't open the resource until you start collecting it (change
resource
to an expression that opens a file, for example). But the resource will only be closed after you're done working with it in downstream flow operators. You can work with the resource in
collect
, or change it with
map
and eventually ask for
first()
if you need the result outside the flow. To turn that into a
Deferred
, you can eventually do
async { resourceFlow.first() }
, but note that the resource will already be closed by the time you
await
this expression. Therefore, stateful transformations are a better fit for the flow operators, if you can live with the fact that only the result of all the operations is
Deferred
.
Of course, you can turn an existing
Deferred<Closeable>
into this idiom by awaiting it in the
flow {}
block.
t
Looking through the kotlin docs, I opted for something like
Copy code
val myResource = lazy { coroutineScope.async { ... } }
This turns out to be faster because it creates the coroutine lazily instead of
async(LAZY) { ... }
which creates the coroutine eagerly and just starts it lazily. The corresponding close function looks like
Copy code
fun Lazy<Deferred<Closeable>>.close(coroutineScope: CoroutineScope) {
  if (isInitialized()) coroutineScope.launch {
    value.await().close()
  }
}