ARM4S - Automatic Resource Management for Scala

Branch Status
master Build Status
develop Build Status

Scaladocs

This library provides a way of succinctly dealing with resources in an exception safe manner. This library can provided identical exception handling and execution semantics to Java's try-with-resource statement and a substitute for Scala which does not have an equivalent construct natively.

More generally, managed resources are not limited to java.lang.AutoClosables. Any type with a on-finally or on-exception lifecycle, can be supported with user-defined implicit adapters. For instance a java.util.concurrent.ExecutorService may have a user-defined shutdown hook executed on-finally. See Comprehensive Example below.

Managed resources are treated as a singleton enumerator whose managed lifecycle is scoped to an applied expression (or block). Unlike Java's constructs, ARM4S's applied block can yield results, ensuring that resources are closed properly before returning.

For example: Manage multiple resources and yield a result

import io.tmos.arm.Implicits._
import java.io._
import java.net.{Socket, InetAddress, ServerSocket}

val line = for {
  socket <- new Socket(InetAddress.getLoopbackAddress, port).manage
  out <- new PrintWriter(socket.getOutputStream, true).manage
  in <- new BufferedReader(new InputStreamReader(socket.getInputStream)).manage
} yield {
  val line = in.readLine()
  out.println(line.toUpperCase)
  return line
}

For more examples see, the Examples section below.

Rationale

Manual management of resources have proven to be error prone, and when done "correctly" - ugly.

Refer to

For instance, if you are doing any of the following, then you should consider this library.

// don't do this
val r = new Resource(...)
try {
  doStuff1(r) // assume we throw an exception here
  doStuff2(r)
} finally {
  r.close() // what if we throw an exception here too?
}

Not good - we just masked (lost) the first 'important' exception with no indication as to whether our main try block completed normally, or if it did not, then whereabouts it did fail.

You may be tempted to wrap the close in another try/catch and log it so that the first exception isn't ever dropped.

// don't do this either
val r = new Resource(...)
try {
  ...
} finally {
  try {
    r.close() // close quietly
  } catch {
    case e => log.warn(e)
  }
}

Still Bad - If close() throws an exception, the application has no idea one was thrown on close and with no opportunity to fail fast and safely. Especially so, if the main try clause didn't throw any exception at all, in which case no exception is propagated.

Instead we should utilise Throwable.addSuppressed to propagate the 'first' important exception with any subsequent exceptions attached as 'suppressed'.

Now, that was for one resource. - What if you needed to close multiple resources in a finally block, each of which could independently throw an exception on close. Could you get it right? If you do - well done. But the next developer who reads is unlikely to understand it.

This is where this library comes in and does things correctly and succinctly, ensuring that the first exception thrown is the one that is propagated, and any subsequent exceptions thrown are added to the head exception as suppressed.

Exception Behaviour

This library differs from other Scala ARM libraries in that it has been designed with consideration for different exception scenarios and with the following goals regarding exception safe behaviour:

  1. The onFinally (by default delegates to close for java.lang.AutoClosabless) and onException management hooks of a resource method must be called even if the body throws any Throwable exception including fatal ones to ensure that no resources are leaked. Example of fatal exceptions include anything not matched by scala.util.control.NonFatal such as InterruptedException, ControlThrowable and VirtualMachineError. Though you should not try to handle such fatal errors, finally logic should still (attempted to) be executed regardless.

  2. The onFinally and onException execution hooks are permitted to throw any Throwable too, possibly additional to exceptions thrown from the main block.

  3. Any Throwable thrown by onFinally or onException should not mask any exception thrown firstly by the body, if any. Instead the secondary exception(s) thrown should be caught and recorded as a suppressed exception against the primary (currently throwing) exception.

  4. Lastly we differ slightly to Java's implementation of try-with-resources in which we permit the corner case where exceptions thrown onFinally or onException may be the same instance as thrown by the applied expression. Where Java will throw a new IllegalArgumentException on attempts to self suppress, we will silently drop repeated instances as usually it is not the users fault that an underlying or decorated resource used a cached exception rather than generate a new exception/stacktrace etc.

Including ARM4S in your project

In SBT:

libraryDependencies += "io.tmos" %% "arm4s" % "1.1.0"

In Maven:

<dependency>
    <groupId>io.tmos</groupId>
    <artifactId>arm4s_${scala.binary.version}</artifactId>
    <version>1.1.0</version>
</dependency>

Using ARM4S

There are two ways you can construct a managed resource

Explicitly

import io.tmos.arm.ArmMethods._
for (r <- manage(resource)) {
  ...
}

Or using implicit decorator methods

import io.tmos.arm.Implicits._
for (r <- resource.manage) {
   ...
}

Any resource of type T for which an implicit CanManage[T] adapter is provided in scope can be managed.

By default the implicit CloseOnFincally extends CanManage[AutoClosable] manager defined in the CanManage companion object is used, which calls close on-finally, unless a higher priority implicit is in scope. Alternatively closeOnFinally method may be used in place of manage method, To explicitly use this adapter.

import io.tmos.arm.ArmMethods._
for (r <- closeOnFinally(resource)) {...}

or using the implicit method

import io.tmos.arm.Implicits._
for (r <- resource.closeOnFinally) {...}

Managed resources may be composed together/chained in a monadic manner that allows for optionally yielding results or imperatively using for-comprehensions.

Examples

Using For-Comprehensions

import io.tmos.arm.ArmMethods._
val jsonMap: Map[String, Any] = for {
  inputStream <- manage(new FileInputStream("data.json"))
} yield {
  JsonMethods.parse(inputStream).extract[Map[String, Any]]
}

We could also write in the following fluid style

import io.tmos.arm.Implicits._
val jsonMap: Map[String, Any] = new FileInputStream("data.json")
 .manage
 .map(JsonMethods.parse(_))
 .extract[Map[String, Any]]

Or if composing multiple resources this can be done easily too

import io.tmos.arm.ArmMethods._
val result = for {
  a <- manage(new A)
  b <- manage(a.getB)
  c <- manage(new C(b))
} yield {
  // ...
}

Resources will be safely managed in reverse declaration order even if a later enumerator declaration threw an exception prior to the main body. For example if new C(b) thew an exception, then b followed by a's on-exception/on-finally lifecycle hooks will be executed.

There may be cases where you need to override the default behaviour for AutoClosable and close resources safely only on-exception, such as when needing to construct multiple resources atomically or delegating close to a different thread/scope. This can be achieved using the predefined CloseOnException manager.

import io.tmos.arm.ArmMethods._

// this implicit now has higher priority then the default CloseOnFinally
implicit val canManage: CanManage[AutoCloseable] = CanManage.CloseOnException

def openAll(): (A, B, C) = for {
  a <- manage(new A)
  b <- manage(new B)
  c <- manage(new C)
} yield (a,b,c)

We also provided closeOnException method similarly to closeOnFinally, for explicit usage of this adapter without needing to import it as a higher priority implicit.

Comprehensive Example

Here is a comprehensive (hypothetical) example of managing multiple resources implicitly, including an ExectorService which we define onFinally logic for. This sample code runs a server socket in a separate thread echoing back text it receives in uppercase.

import io.tmos.arm.Implicits._
import scala.collection.JavaConverters._

implicit val manager: CanManage[ExecutorService] = new CanManage[ExecutorService] {
  override def onFinally(pool: ExecutorService): Unit = {
    pool.shutdown() // Disable new tasks from being submitted
    try {
      if (!pool.awaitTermination(10, TimeUnit.SECONDS)) { // wait for normal termination
        pool.shutdownNow() // force terminate
        if (!pool.awaitTermination(10, TimeUnit.SECONDS)) // wait for forced termination
          throw new RuntimeException("ExecutorService did not terminate")
      }
    } catch {
      case _: InterruptedException =>
        pool.shutdownNow() // (Re-)Cancel if current thread also interrupted
        Thread.currentThread().interrupt() // Preserve interrupt status
        // It is very important that we do not propagate InterruptedException
        // as it may be added as it may be suppressed if an earlier
        // exception trumps its. This is the same generally for any close method.
        // See https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html#close--
        // for details.
    }
  }
  override def onException(r: ExecutorService): Unit = {}
}

val serverSocketFuture = new CompletableFuture[ServerSocket]

val callable = new Callable[Unit] {
  override def call(): Unit = {
    // Note that ServerSocket.accept() blocks but, does not throw
    // InterruptedException. Instead, to terminate the event loop, we need
    // to close server socket asynchronously. We delegate closing of server
    // socket to the main thread under normal circumstances, but in the
    // event this thread has thrown an exception, we will close.
    for (ss <- new ServerSocket(0, 0, InetAddress.getLoopbackAddress).closeOnException) {
      serverSocketFuture.complete(ss)
      while (!Thread.interrupted()) { // main event loop
        try {
          for {
            connection <- ss.accept.closeOnFinally // block here
            out <- new PrintWriter(connection.getOutputStream, true).closeOnFinally
            in <- new BufferedReader(new InputStreamReader(connection.getInputStream)).closeOnFinally
            line <- in.lines().iterator().asScala
          } out.println(line.toUpperCase)
        } catch {
          case _: SocketException if ss.isClosed =>
            // at this point server socket has been closed via the main thread
            // we set interrupt status and terminate the event loop / thread
            Thread.currentThread().interrupt()
        }
      }
    }
  }
}

// manage an executor service using the user defined CanManage
val completedFuture = for (
  executorService <- Executors.newSingleThreadExecutor().manage
) yield {
  val future = executorService.submit(callable)
  for (ss <- serverSocketFuture.get.closeOnFinally) {
    val upperPhrase = for {
      s <- new Socket(InetAddress.getLoopbackAddress, ss.getLocalPort).closeOnFinally
      out <- new PrintWriter(s.getOutputStream, true).closeOnFinally
      in <- new BufferedReader(new InputStreamReader(s.getInputStream)).closeOnFinally
    } yield {
      out.println("hello")
      out.println("world")
      in.readLine() + ' ' + in.readLine()
    }
    assert(upperPhrase === "HELLO WORLD")
  }
  // at this point the callable event loop is terminating
  future
}
// at this point the callable event loop has been terminated

assert(!completedFuture.isCancelled)
assert(completedFuture.isDone)
completedFuture.get // should not block or throw any error