Scala 2.13 introduced the scala.util.Using
utility which helps us deal with cleaning up resources before exiting a code block similar to Java’s try-with-resources functionality.
After using Using
for some time, a couple pain points have become apparent with respect to the Using.Manager.apply
factory method.
The Using.Manager.apply
method is used in the following way:
val result: Try[Long] = Using.Manager { (use: Using.Manager) =>
val file1 = use(new File("file1.txt"))
val file2 = use(new File("file2.txt"))
file1.length() + file2.length()
}
Every time the passed Manager
instance is applied, essentially a callback is pushed on the stack to close the resource after the codeblock returns, no matter if it returns through return or through exception.
Problem 1: everything is wrapped in Try, always
The first issue is that there is no way to use the manager in this way, without going through scala.util.Try
. So if you are in a section of code where you are dealing with errors by throwing them, you will have to remember to call .get
on the result each and every time. Forgetting to call .get
is very likely to happen, especially when in Unit
-returning methods (such as unit tests!), and at the very least are cumbersome and a pain point.
For instance, use in a ScalaTest test suite runs a high risk of accidentally swallowing errors in code such as:
test("doing something with files...") {
Using.Manager { use =>
val file = use(new File("f"))
file.length() mustBe 1024L
} // <-here! we forgot to do .get !!
}
I can testify that this has happened multiple times on my team, to myself and to others, and slipped through code review!! So for some weeks, we had test suites that were essentially testing nothing since they always passed.
I would propose that a new higher order method be added to either object Using
or object Manager
, don’t much care what name you choose, something like newScope
perhaps:
def newScope[A](f: Manager => A): A = (new Manager).manage(f)
test("doing something with files...") {
Using.newScope { use =>
val file = use(new File("f"))
file.length() mustBe 1024L
}
}
Problem 2: Missing a more flexible go-style defer
, to register general cleanup code
Sometimes you want to do some cleanup action(s) which are not exactly the “canonical” way to shut down a resource. For instance, maybe you temporarily have an ExecutorService
in your block, which doesn’t implement AutoCloseable
by the way. At the end, how you want to clean up the ExecutorService may vary by application and by use-case. Should I .shutdownNow()
or should I .shutdown(); .awaitTermination()
? The point is that often cleanup logic isn’t coupled to the compile-time type of a resource but deserves first class application code.
That use-case actually is supported by Using
, because the Manager.apply
takes in a SAM type Releasable[A]
, so any arbitrary callback can be passed in as the typeclass instance:
Using.Manager { use =>
val ec: ExecutorService = use(Executors.newSingleThreadExecutor()) { ec =>
ec.shutdown()
ec.awaitTermination(10L, TimeUnit.SECONDS)
}
}
But there’s just one sort of use-case which becomes a bit awkward and that is when you aren’t really opening and closing a single encapsulated resource object, but you have some other cleanup code to register anyways. Like, maybe you want to println("exiting!")
or maybe you want to collect all of your files you opened and zip them up and put them in an archive folder. For such general purpose cleanup code, the only way to register such actions is to find some “dummy” receiver, like ""
or ()
or whatever (null resources are not considered valid):
Using.Manager { use =>
use(())(_ => println("done!"))
val archiveDirectory: File = ???
val filesOpened = ListBuffer[File]()
use(1) { _ =>
zipUpAndArchive(filesOpened.toSeq)
}
use("") { _ =>
println("some other action...")
}
}
This is a little awkward, we’re not opening a resource with use()
, but that’s the only way to hook into the callback registration. I would propose that a method be exposed in the manager which lets users directly register callbacks. I’ll call this defer
after go’s defer statements:
Using.Manager { use =>
use.defer(println("done!"))
val archiveDirectory: File = ???
val filesOpened = ListBuffer[File]()
use.defer {
zipUpAndArchive(filesOpened.toSeq)
}
use.defer {
println("some other action...")
}
}
Problem 3: Thread Safety
I think for a utility that is to be used as a basic control structure, it’s worth considering if in this case it’s worth making the utility thread-safe. The Manager
class has 2 pieces of mutable state:
private var closed = false
private[this] var resources: List[Resource[_]] = Nil
It’s not protected by any synhronization though, worth considering if it should be so that even if a user passes the manager to an other thread there’s no chance of corruption. For instance, this code will sometimes allow one thread to register a resource, but then never closes it:
Using.Manager { use =>
new Thread(() => {
var i = 0
while(true) {
try {
use(i)(x => println(s"closed ${x}"))
println(s"registered ${i}")
i += 1
} catch { case NonFatal(_) => }
}
}).start()
Thread.sleep(30)
}
Verify in the stdout (272 is registered but never closed):
//...
registered 267
registered 268
registered 269
registered 270
registered 271
registered 272
closed 271
closed 270
closed 269
closed 268
closed 267
//...
This could be fixed with just some synchronized blocks.
Thanks folks!