IMHO one of the limitations of Java is resource management: garbage collections works well enough when it comes to Java objects, but sometimes some native resources are referenced and are not garbage collected, or they are garbage collected only at a later stage but the developer wants to to promptly release then.
The java solution? Closeable
, AutoCloseable
.
Well, they work decently but they have some clear limitation:
- They introduce an additional method to the interface.
- Every object owning a
Closeable
resource must beCloseable
itself if it doesn’t consume the resource. (for example,InputStreamReader
isCloseable
). - The resource can easily leak in different scenarios, for example during the transfer of the object reference to its owner:
class MyClass: Closeable {
private val file1 = FileInputStream()
private val file2 = FileInputStream() // If this fails then file1 is leaked
override fun close() {
file1.close()
file2.close() // This is never executed if file1.close fails
}
}
- One particular use case where they can easily leak without being notice is with coroutines. The following code looks safe, but it is not really. If the coroutine is cancelled during the
withContext
execution, theFileInputStream
is leaked.
suspend fun fileInputStream() = withContext(Dispatchers.IO) {
FileInputStream()
}
fun user() {
fileInputStream().use {
// do something
}
}
This issue doesn’t have a real resolution: Universal API for use of closeable resources with coroutines · Issue #1191 · Kotlin/kotlinx.coroutines · GitHub
In general, Closeable
works, but has some important limitations and makes the code prone to resource-leakage, bloated of close methods and with more compromises (a ByteArrayInputStream
must implement close
with NOOP).
The solution? It’s already present in the KEEP of context receivers: KEEP/context-receivers.md at master · Kotlin/KEEP · GitHub
interface AutoCloseScope {
fun defer(closeBlock: () -> Unit)
}
context(AutoCloseScope)
fun File.open(): InputStream
fun withAutoClose(block: context(AutoCloseScope) () -> Unit) {
val scope = AutoCloseScopeImpl() // Not shown here
try {
with(scope) { block() }
} finally {
scope.close()
}
}
// usage
withAutoClose {
val input = File("input.txt").open()
val config = File("config.txt").open()
// Work
// All files are closed at the end
}
Structured resources. Just like what coroutines did for concurrency, a new library can do for resource management.
This would move the resource registration from the interface of the object to the implementation:
interface MyInputStream {
fun read(): Int
// It doesn't have a close method or inherit from Closeable
}
class MyByteArrayInputStream: MyInputStream {
override fun read()
// No need for noop close
}
class MyInputStreamReader(
private val inputStream: MyInputStream
) {
// no close method, no Closeable interface
}
fun usage() {
val reader = MyInputStreamReader(MyByteArrayInputStream())
// no need to close anything :D
}
context(AutoCloseScope)
class MyFileInputStream: MyInputStream {
init {
defer { closeMyFile() }
}
}
fun usage2() {
withAutoClose {
val stream = MyFileInputStream()
}
// This is disallowed, it wont compile without an AutoCloseScope context
// val stream = MyFileInputStream()
}
This solution solves many of the problems:
- No additional close method is needed on the interface.
- Objects that are not owning the resource don’t have to be
Closeable
(such asInputStreamReader
) - It makes resource leaking much more difficult
context(AutoCloseScope)
class MyClass {
private val file1 = MyFileInputStream()
private val file2 = MyFileInputStream() // If this fails then file1 is NOT leaked
}
// release cannot leak as well
- It works well with coroutines
context(AutoCloseScope)
suspend fun fileInputStream() = withContext(Dispatchers.IO) {
MyFileInputStream()
}
// This is now safe and don't leak even during cancellation
suspend fun user() {
withAutoClose {
fileInputStream()
// do stuff
}
}
What is missing?
A more advanced solution that supports all the use cases, has a clear API and widely used.
Interoperability of different custom implementations of AutoCloseScope would be messy.
What I am suggesting with this post is an investigation around this possibilities and what it can potentially enable. Structured resource management could be a nice addition to the kotlin libraries and could tackle both the limitations of Closeable
and the kotlin-specific problems related to coroutines.