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