Francis Reynders
03/16/2023, 5:03 PMonExit
method will be called). The Rescource.use
and resourceScope
functions only provide the resource in the lambda, however I would like to have the resource available between object initialization and onExit
, so during the lifecycle of the object). Is there a standard way to do so?
I was thinking of creating a CoroutineScope and lauching a coroutine in the init
part, running resourceScope there and waiting for cancellation. The onExit
method would cancel that scope. However that would mean the acquired resource would need to be defined as a lateinit var
rather than val since the compiler does not allow initialization from within the coroutine. It also seems a bit convoluted.
class Test() {
val scope = CoroutineScope(EmptyCoroutineContext)
lateinit var resource: String
init {
scope.launch {
resourceScope {
resource = install({"test"}, { _,_ -> Unit })
}
awaitCancellation()
}
}
If there is a better way I would love to hear about it.simon.vergauwen
03/16/2023, 5:42 PMhowever I would like to have the resource available between object initialization andwithonExit
onExit
do you mean the finalizer of the Resource
? Or is onExit
a close/finalizer method of Test
? The latter is a clear indicator that Test
is dependent on outer resource. So most commonly used pattern would be to create a smart constructor for Test
.
suspend fun ResourceScope.test(): Test {
val resource: String = install({"test"}, { _,_ -> Unit })
Test(resource)
}
Effectively moving the concern "up" closer to main
or what is typically called the edge of the world.
Another option, would be to rely on one of the unsafe constructors allocate
which turns Resource
into Pair<A, suspend (ExitCase) -> Unit>
and turns responsibility back to the user of calling suspend (ExitCase) -> Unit
. The custom CoroutineScope
here has the same issue, which is leaking the resource if cancel
is never called.
Note that in the snippet you shared the resource
is immediately closed, since awaitCancellation
is called outside of resourceScope { }
.Francis Reynders
03/16/2023, 5:44 PMonExit
would be part of the Test
class.
The reason for this is that the class would encapsulate the lifecycle of some session. Ideally this class would fully hide the fact that the resource is used, taking care of acquiring it and closing it at the end of it's lifecycle. The caller would not be concerned with those details.simon.vergauwen
03/16/2023, 5:46 PMThe customhere has the same issue, which is leaking the resource ifCoroutineScope
is never called.cancel
CoroutineScope
is also a Resource
🙃
https://github.com/47deg/gh-alerts-subscriptions-kotlin/blob/284e5c111b17cff9bc55a9eea2523e298568461b/src/main/kotlin/alerts/KtorUtils.kt#L31Francis Reynders
03/16/2023, 6:06 PMallocate
does not seem to work in init
since it's a suspending functionsimon.vergauwen
03/16/2023, 6:15 PMsuspend
from acquire
is moved up to allocate
. So you have 3 options:
suspend fun ResourceScope.test(): Test {
val resource: String = install({"test"}, { _,_ -> Unit })
Test(resource)
}
suspend fun test(): Test {
val (res, finalizer) =
resource({"test"}, { _,_ -> Unit }).allocate()
// call finalizer on `onExit` also suspend
Test(resource, finalizer)
}
fun test(): Test {
val resource = resource({"test"}, { _,_ -> Unit })
val (res, finalizer) = runBlocking { resource.allocate() }
// call finalizer on `onExit` also suspend
Test(resource, finalizer)
}
From top to bottom more to less desirable. use
from Kotlin Closeable
has the same issue.
It's hard to provide better information based on the requirements given 😅 but if you're using Test
to wrap a single JVM Closeable
you might be better of not using Resource
for this use-case.
Resource
truly shines when using larger dependency graphs that are composed of multiple resources, which often need to initialise and close on different dispatchers.
And if used in combination with something like SuspendApp
that ensures back-pressuring your (JVM) application until all resources are closed. This last one is probably the most annoying thing on the JVM with Closeable
you're responsible of attaching all critical things to ShutdownHook
etc. For example applications that open FileHandle
on the OS, but don't close them will result in the OS eventually running out of FileHandle
and will require a restart of the machine.Francis Reynders
03/16/2023, 6:21 PM😀 just shows it's indeed not the best solution. I guess encapsulating side effects is not really FP style. My idea was to fully encapsulate it and capture all errors within the object, so the user would not be concerned with it. I'll need to rethink this. Thanks a lot for your feedback.is also aCoroutineScope
Resource
simon.vergauwen
03/16/2023, 6:24 PM"test"
and provide an alternative API around it? That can still be done, but then I would opt for option: suspend fun ResourceScope.test(): Test {
or fun Test.Companion.create(): Resource<Test>
although that forces the Resource
on the user.
That way you can still wrap the resource "test", and provide an alternative API around it. Which returns Either
or handles errors accordingly. That is not counter FP in any way. I think the counter FP piece is trying to hide that Test
is a Resource
but that leaks out anyway since the user would need to call onExit
, no?class ExternalRes {
fun close(): Unit = Thread.sleep(100)
fun doSomething(): Int = throw RuntimeException("Boom!")
}
class Test(private val res: ExternalRes) {
// don't swallow but do something proper
// like send log to metric server
fun safe(): Int =
catch({ res.doSomething() }) { -1 }
companion object {
fun create(): Resource<Test> = resource {
val res = install({ ExternalRes() }) { res, _ -> res.close() }
Test(res)
}
}
}
suspend fun userCode() {
Test.create().use { test ->
println(test.doSomething()) // -1
}
resourceScope {
val test = Test.create().bind()
println(test.doSomething()) // -1
}
}
Francis Reynders
03/16/2023, 6:33 PMsimon.vergauwen
03/16/2023, 6:34 PM