Olaf Gottschalk
07/08/2025, 3:18 PMServer
- which is defined as an interface
. So it looks along the lines of this:
interface Server {
fun prepare()
fun run()
fun shutdown()
}
In order to properly handle these as resources, I have some fun execute()
that does this:
fun execute() {
resourceScope {
servers.forEach {
install({ it.prepare() }, { _, _ -> it.shutdown() })
}
servers.forEach(Server::run)
}
}
Now, this assumes that all Server
classes basically can do all of there setup in prepare
and properly release all resources in shutdown
.
In some servers though, I would like to also use the resource concept and install things in the same resource scope to allow them to live alongside the servers and properly get shutdown as well. Those types of server implementations typically do not define any code in their shutdown
function, but they want to install more resources in the same resource scope.
That's why I extended my prepare
fun like this:
interface Server {
context(ResourceScope) fun prepare()
fun run()
fun shutdown()
}
To now execute my system, I need to manually bring the resource scope back into scope:
fun execute() {
resourceScope {
servers.forEach {
install({ context(this@resourceScope) { it.prepare() } }, { _, _ -> it.shutdown() })
}
servers.forEach(Server::run)
}
}
Now the big question: am I doing something wrong? Is it safe to hand in the same resource scope into the acquire step of a resource so this resource can also install resources in the same scope?
Note: the reason I do this is that I got several different types of Server implementations, some that need a "classic" prepare/shutdown step, others that are fine with installing resources!simon.vergauwen
07/08/2025, 3:26 PMAcquireStep
became redundant, but we should be able to remove it in a binary compatible way.Youssef Shoaib [MOD]
07/08/2025, 3:30 PMonRelease
, your code can be way shorter:
fun execute() = resourceScope {
servers.forEach {
it.prepare()
onRelease { _ -> it.shutdown() }
}
servers.forEach(Server::run)
}
This will do the correct release ordering as well btw (all components in prepare
are registered first, then the server itself, and so the server will be released first, then its components will be)Olaf Gottschalk
07/08/2025, 3:31 PMOlaf Gottschalk
07/08/2025, 3:32 PMYoussef Shoaib [MOD]
07/08/2025, 3:32 PMinstall
just runs the first lambda normally, then calls onRelease with the second, so registering things in the first lambda is equivalent to registering them beforeYoussef Shoaib [MOD]
07/08/2025, 3:33 PMYoussef Shoaib [MOD]
07/08/2025, 3:35 PMAcquireStep
, but as Simon said, we're locked in by binary compatibility (and maybe some source compatibility too?), but having it delegate is a nice middle ground.Alejandro Serrano.Mena
07/08/2025, 3:38 PMfun ResourceScope.prepare()
instead of using a context parameter would lead to a nicer API, and you would not need the intermediate context
injectionYoussef Shoaib [MOD]
07/08/2025, 3:44 PMallocated
and store the finalizer, and ensure it gets called on shutdown.Youssef Shoaib [MOD]
07/08/2025, 4:02 PMinterface Server {
context(_: ResourceScope) fun prepare()
fun run()
}
fun execute() = resourceScope {
servers.forEach { it.prepare() } // maybe can be written as a method reference, but I've sometimes had weird compilation issues; I believe that's how it's supposed to work tho in that it binds the contexts immediately
servers.forEach(Server::run)
}
And the server is responsible to add its hooks in prepare
, and so it can add a singular hook equivalent to shutdown
, or it can add whatever components it wants that themselves add hooks. The interface is minimal (I don't think it can be made smaller because you want to register them all first, and then run, so the only smaller one is something like context(_: ResourceScope) fun prepare(): () -> Unit
, which has the nice property that run
can't be called before prepare
, but it's a little overkill and needs a map
)
The important question though is do you really need to have this as a concern in the interface? You can have the smart constructors take in ResourceScope
directly. Server would then just have run
as its only method. The only reason to design it in the way you have it is if the servers need to exist outside of a ResourceScope
, but that seems very very unlikely. In fact, the smart constructor route is basically that single-function prepare
thing I mentioned above, but more idiomatic and open-endedOlaf Gottschalk
07/08/2025, 4:41 PMResourceScope
, but I want to prepare a Server
and need a ResourceScope
to do so.... I don't know why, but when watching various talks on receivers, extension functions and context receivers/parameters, this thought got me hooked quite a lot... since then, I try to be consistent, but it sometimes reads awkwardly when called. Sadly!Youssef Shoaib [MOD]
07/10/2025, 7:38 PM