├── project ├── build.properties └── plugins.sbt ├── version.sbt ├── src ├── test │ ├── scala-2.13 │ │ └── io │ │ │ └── tmos │ │ │ └── arm │ │ │ └── Compat.scala │ ├── scala-2.10 │ │ └── io │ │ │ └── tmos │ │ │ └── arm │ │ │ └── Compat.scala │ ├── scala-2.11 │ │ └── io │ │ │ └── tmos │ │ │ └── arm │ │ │ └── Compat.scala │ ├── scala-2.12 │ │ └── io │ │ │ └── tmos │ │ │ └── arm │ │ │ └── Compat.scala │ └── scala │ │ └── io │ │ └── tmos │ │ └── arm │ │ ├── SimpleAutoCloseableTest.scala │ │ ├── ImplicitsSuite.scala │ │ ├── DefaultManagedResourceSuite.scala │ │ ├── CustomResourceSuite.scala │ │ └── ArmMethodsSuite.scala └── main │ └── scala │ └── io │ └── tmos │ └── arm │ ├── DefaultManagedResource.scala │ ├── ArmMethods.scala │ ├── Implicits.scala │ ├── ManagedResource.scala │ └── CanManage.scala ├── .gitignore ├── release-process.md ├── .travis.yml ├── CHANGELOG.md ├── LICENSE └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.3.8 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "1.1.2-SNAPSHOT" 2 | -------------------------------------------------------------------------------- /src/test/scala-2.13/io/tmos/arm/Compat.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | object Compat { 4 | val CollectionConverters = scala.jdk.CollectionConverters 5 | } 6 | -------------------------------------------------------------------------------- /src/test/scala-2.10/io/tmos/arm/Compat.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | object Compat { 4 | val CollectionConverters = scala.collection.JavaConverters 5 | } 6 | -------------------------------------------------------------------------------- /src/test/scala-2.11/io/tmos/arm/Compat.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | object Compat { 4 | val CollectionConverters = scala.collection.JavaConverters 5 | } 6 | -------------------------------------------------------------------------------- /src/test/scala-2.12/io/tmos/arm/Compat.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | object Compat { 4 | val CollectionConverters = scala.collection.JavaConverters 5 | } 6 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // https://github.com/jodersky/sbt-gpg 2 | addSbtPlugin("io.crashbox" % "sbt-gpg" % "0.2.0") 3 | 4 | // https://github.com/xerial/sbt-sonatype 5 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | 4 | # sbt specific 5 | .cache 6 | .history 7 | .lib/ 8 | dist/* 9 | target/ 10 | lib_managed/ 11 | src_managed/ 12 | project/boot/ 13 | project/plugins/project/ 14 | 15 | # Scala-IDE specific 16 | .scala_dependencies 17 | .worksheet 18 | 19 | # IntelliJ specific 20 | .idea/ 21 | 22 | # Project specific 23 | project/local.sbt 24 | -------------------------------------------------------------------------------- /src/test/scala/io/tmos/arm/SimpleAutoCloseableTest.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | import java.io.Closeable 4 | 5 | class SimpleAutoCloseableTest(val msg: String) extends Closeable { 6 | protected var closed = false 7 | def except(m: String = msg) = throw new RuntimeException(m) 8 | def isClosed: Boolean = closed 9 | override def close(): Unit = closed = true 10 | } 11 | -------------------------------------------------------------------------------- /release-process.md: -------------------------------------------------------------------------------- 1 | Update ~/.sbt/1.0/sonatype.sbt with credentials 2 | ``` 3 | credentials += Credentials( 4 | "Sonatype Nexus Repository Manager", 5 | "oss.sonatype.org", 6 | "__USERNAME__", 7 | "__PASSWORD__" 8 | ) 9 | ``` 10 | 11 | Then publish all versions of signed artifacts to staging repo 12 | ``` 13 | $ sbt 14 | sbt:arm4s> +publish 15 | ``` 16 | 17 | Then promote to release 18 | ``` 19 | sbt:arm4s> sonatypeRelease 20 | ``` 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | sudo: false 3 | jdk: 4 | - openjdk8 5 | - openjdk11 6 | scala: 7 | - 2.10.7 8 | - 2.11.12 9 | - 2.12.10 10 | - 2.13.1 11 | branches: 12 | only: 13 | - master 14 | - develop 15 | - /^release\/.*$/ 16 | - /^feature\/.*$/ 17 | script: 18 | - sbt ++$TRAVIS_SCALA_VERSION test doc 19 | cache: 20 | directories: 21 | - $HOME/.cache/coursier 22 | - $HOME/.ivy2/cache 23 | - $HOME/.sbt 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.1 (Unreleased) 4 | - Added cross build for Scala 2.13.1, 2.12.10 5 | 6 | ## [1.1.0](https://github.com/tmoschou/arm4s/releases/tag/v1.1.0) (2018-10-30) 7 | - Added cross build for Scala 2.13-M5 8 | - Support explicitly passing the CanManage object to `ImplicitManageable`'s methods [#5](https://github.com/tmoschou/arm4s/issues/5) 9 | - `CanManage`'s `onFinally` and`onException` now defaults to no-op implementation [#6](https://github.com/tmoschou/arm4s/issues/6) 10 | 11 | ## [1.0.0](https://github.com/tmoschou/arm4s/releases/tag/v1.0.0) (2018-09-27) 12 | New API Improvements [#3](https://github.com/tmoschou/arm4s/pull/3): 13 | - CanManage/DefaultManagedResource now supports on-exception lifecycle hooks 14 | - Implicit decorator methods to now explicitly convert a resource into a 15 | managed one, over implicit conversions. 16 | 17 | ## [0.2.0](https://github.com/tmoschou/arm4s/releases/tag/v0.2.0) (2017-01-30) 18 | - Move implicits package-object to Implicits object. [#1](https://github.com/tmoschou/arm4s/issues/1) 19 | 20 | ## [0.1.0](https://github.com/tmoschou/arm4s/releases/tag/v0.1.0) (2017-01-12) 21 | - Initial release 22 | -------------------------------------------------------------------------------- /src/test/scala/io/tmos/arm/ImplicitsSuite.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class ImplicitsSuite extends AnyFunSuite { 6 | 7 | test("has implicit manage converter") { 8 | import Implicits._ 9 | val resource = new SimpleAutoCloseableTest("msg") 10 | for (r <- resource.manage) {} 11 | assert(resource.isClosed) 12 | } 13 | 14 | test("has implicit closeOnFinally converter") { 15 | import Implicits._ 16 | val resource = new SimpleAutoCloseableTest("msg") 17 | for (r <- resource.closeOnFinally) {} 18 | assert(resource.isClosed) 19 | } 20 | 21 | test("has implicit closeOnException converter") { 22 | import Implicits._ 23 | val resource = new SimpleAutoCloseableTest("msg") 24 | try { 25 | for (r <- resource.closeOnException) { 26 | r.except() 27 | } 28 | } catch { 29 | case _: Throwable => 30 | } 31 | assert(resource.isClosed) 32 | } 33 | 34 | test("can supply different managers to different resource instances of the same type") { 35 | import Implicits._ 36 | 37 | def onFinally[R](f: R => Unit): CanManage[R] = new CanManage[R] { 38 | override def onFinally(r: R): Unit = f(r) 39 | } 40 | 41 | var onFinally1 = false 42 | var onFinally2 = false 43 | 44 | for { 45 | _ <- ().manage(onFinally[Unit](_ => onFinally1 = true)) 46 | _ <- ().manage(onFinally[Unit](_ => onFinally2 = true)) 47 | } () 48 | 49 | assert(onFinally1) 50 | assert(onFinally2) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Terry Moschou 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/main/scala/io/tmos/arm/DefaultManagedResource.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | /** 4 | * The default implementation of a [[ManagedResource]]. 5 | * 6 | * @param r the resource passed by name 7 | * @tparam R the type of the resource to pass to the main body 8 | * @tparam S the type of the resource under management 9 | */ 10 | class DefaultManagedResource[R, -S >: R](r: => R)( 11 | implicit canManage: CanManage[S] 12 | ) extends ManagedResource[R] { 13 | 14 | override def map[B](f: R => B) : B = { 15 | val resource = r // construct resource 16 | var throwing: Throwable = null 17 | try { 18 | f(resource) 19 | } catch { 20 | case e1: Throwable => 21 | throwing = e1 22 | try { 23 | canManage.onException(resource) 24 | } catch { 25 | case e2: Throwable => 26 | if (e2 != e1) { 27 | e1.addSuppressed(e2) 28 | } 29 | } 30 | throw e1 31 | } finally { 32 | if (throwing != null) { 33 | try { 34 | canManage.onFinally(resource) 35 | } catch { 36 | case e: Throwable => 37 | // We differ in Java's implementation in that we silently drop 38 | // attempts to self-suppress exceptions rather than throw 39 | // IllegalArgumentException since it is almost always not the users 40 | // fault that an underlying resource uses a cached exception 41 | // rather than generate a new exception/stacktrace etc. I consider this 42 | // an oversight in Java's implementation; changing it however was floated 43 | // http://mail.openjdk.java.net/pipermail/core-libs-dev/2014-May/026742.html 44 | if (e != throwing) { 45 | throwing.addSuppressed(e) 46 | } 47 | } 48 | } else { 49 | canManage.onFinally(resource) 50 | } 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/scala/io/tmos/arm/ArmMethods.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | /** 4 | * Methods for management of a resource. 5 | * 6 | * For implicit management see `io.tmos.arm.Implicits`. 7 | */ 8 | object ArmMethods { 9 | 10 | /** 11 | * Manages a generic resource. 12 | * 13 | * Requires an implicit `CanManage[S]` in scope. A default manager for 14 | * `AutoClosable`s is provided (See `CanManage.CloseOnFinally`) and the 15 | * default behaviour is equivalently the same as `closeOnFinally` 16 | * 17 | * @param r the resource to managed passed by-name 18 | * @tparam R the type of the resource passed to the applied expression 19 | * @tparam S the type of the resource passed to the manager 20 | * 21 | * @return a managed resource 22 | */ 23 | def manage[R, S >: R : CanManage](r: => R): ManagedResource[R] = new DefaultManagedResource[R, S](r) 24 | 25 | /** 26 | * Manage an `AutoClosable`. This will call `AutoClosable.close` during the 27 | * on-finally execution lifecycle. The on-exception lifecycle hook is a no-op. 28 | * 29 | * @param r the resource the manage 30 | * @tparam R the type of the resource passed to the applied expression 31 | * @return a managed resource 32 | */ 33 | def closeOnFinally[R <: AutoCloseable](r: => R): ManagedResource[R] = new DefaultManagedResource(r) 34 | 35 | /** 36 | * Manage an `AutoClosable`. This will call `AutoClosable.close` during the 37 | * on-exception execution lifecycle. The on-finally lifecycle hook is a no-op. 38 | * 39 | * @param r the resource the manage 40 | * @tparam R the type of the resource passed to the applied expression 41 | * @return a managed resource 42 | */ 43 | def closeOnException[R <: AutoCloseable](r: => R): ManagedResource[R] = { 44 | implicit val manager: CanManage[AutoCloseable] = CanManage.CloseOnException 45 | new DefaultManagedResource(r) 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/io/tmos/arm/Implicits.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | /** 4 | * Implicit Methods for management of a resource. 5 | * 6 | * For explicit management see `io.tmos.arm.ArmMethods`. 7 | */ 8 | object Implicits { 9 | 10 | /** 11 | * Provide implicit methods to manage a generic resource. 12 | * 13 | * Also requires an implicit `CanManage[S]` in scope. A default manager for 14 | * `AutoClosable`s is provided (See `CanManage.CloseOnFinally`) and the 15 | * default behaviour is equivalently the same as `closeOnFinally` 16 | * 17 | * @param r the resource to managed passed by-name 18 | * @tparam R the type of the resource passed to the applied expression 19 | */ 20 | implicit class ImplicitManageable[R](r: => R) { 21 | 22 | /** 23 | * Converts this resource into a generic managed resource. 24 | * 25 | * @tparam S the type of the resource passed to the manager 26 | * @return the managed resource 27 | */ 28 | def manage[S >: R](implicit canManage: CanManage[S]): ManagedResource[R] = ArmMethods.manage(r) 29 | } 30 | 31 | /** 32 | * Provide implicit methods to manage an AutoClosable resource. 33 | * 34 | * @param r the resource to managed passed by-name 35 | * @tparam R the type of the resource passed to the applied expression 36 | */ 37 | implicit class ImplicitAutoClosable[R <: AutoCloseable](r: => R) { 38 | 39 | /** 40 | * Convert this AutoClosable into a managed resource. Will call 41 | * `AutoClosable.close` during the on-finally execution lifecycle. 42 | * The on-exception lifecycle hook is a no-op. 43 | * 44 | * @return the managed resource 45 | */ 46 | def closeOnFinally: ManagedResource[R] = ArmMethods.closeOnFinally(r) 47 | 48 | /** 49 | * Convert this AutoClosable into a managed resource. Will call 50 | * `AutoClosable.close` during the on-exception execution lifecycle. 51 | * The on-finally lifecycle hook is a no-op. 52 | * 53 | * @return the managed resource 54 | */ 55 | def closeOnException: ManagedResource[R] = ArmMethods.closeOnException(r) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/scala/io/tmos/arm/ManagedResource.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | /** 4 | * A resource that is managed. 5 | * 6 | * Only one implementation is provided currently [[DefaultManagedResource]]. Subclasses only need to provide 7 | * [[ManagedResource.map]] 8 | * 9 | * @tparam A the type of the resource to manage 10 | */ 11 | trait ManagedResource[+A]{ 12 | 13 | /** 14 | * Allows the resource to be used imperatively in `yield`-ing `for`-comprehensions. 15 | * 16 | * For example 17 | * {{{ 18 | * import io.tmos.arm._ 19 | * for (r <- managed(new Resource)) yield { 20 | * ... 21 | * } 22 | * }}} 23 | * 24 | * @param f the function to execute of which the resource is managed 25 | * @tparam B the return type of the function 26 | * @return the result of the function 27 | */ 28 | def map[B](f: A => B): B 29 | 30 | /** 31 | * Allows the resource to be used imperatively in ''stacked'' `yield`-ing `for`-comprehensions. 32 | * 33 | * For example 34 | * {{{ 35 | * import io.tmos.arm._ 36 | * for { 37 | * a <- managed(new Resource1) 38 | * b <- managed(new Resource2) 39 | * } yield { 40 | * ... 41 | * } 42 | * }}} 43 | * which translates to 44 | * {{{ 45 | * managed(new Resource1) flatMap { a => 46 | * managed(new Resource2) map { b => 47 | * ... 48 | * } 49 | * } 50 | * }}} 51 | * 52 | * Default implementation is to call [[map]] 53 | * 54 | * @param f the function to execute of which the resource is managed 55 | * @tparam B the return type of the function 56 | * @return the result of the function 57 | */ 58 | def flatMap[B](f: A => B): B = map(f) 59 | 60 | /** 61 | * Allows the resource to be used imperatively in `for`-comprehensions. 62 | * 63 | * For example 64 | * {{{ 65 | * import io.tmos.arm._ 66 | * for (a <- managed(new Resource1)) { 67 | * ... 68 | * } 69 | * }}} 70 | * 71 | * Default implementation is to call [[map]] 72 | * 73 | * @param f the function to execute of which the resource is managed 74 | */ 75 | def foreach(f: A => Unit): Unit = map(f) 76 | } 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/test/scala/io/tmos/arm/DefaultManagedResourceSuite.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | import org.scalatest.funsuite.AnyFunSuite 4 | 5 | class DefaultManagedResourceSuite extends AnyFunSuite { 6 | 7 | case object SimpleResource 8 | 9 | class Manager[T] extends CanManage[T] { 10 | var onExceptionCalled: Boolean = false 11 | var onFinallyCalled: Boolean = false 12 | override def onException(r: T): Unit = { 13 | assert(!onFinallyCalled) 14 | onExceptionCalled = true 15 | } 16 | override def onFinally(r: T): Unit = { 17 | onFinallyCalled = true 18 | } 19 | } 20 | 21 | test("under normal operation, only onFinally should be called") { 22 | implicit val manager: Manager[SimpleResource.type] = new Manager[SimpleResource.type] 23 | val managedResource = new DefaultManagedResource(SimpleResource) 24 | managedResource.map(_ => ()) 25 | assert(!manager.onExceptionCalled) 26 | assert(manager.onFinallyCalled) 27 | } 28 | 29 | test("if an exception is thrown by the applied expr onException should be called") { 30 | implicit val manager: Manager[SimpleResource.type] = new Manager[SimpleResource.type] 31 | val managedResource = new DefaultManagedResource(SimpleResource) 32 | try { 33 | managedResource.map(_ => throw new IllegalArgumentException) 34 | throw new IllegalStateException 35 | } catch { 36 | case e: IllegalArgumentException => 37 | } 38 | assert(manager.onExceptionCalled) 39 | assert(manager.onFinallyCalled) 40 | } 41 | 42 | test("if exceptions are thrown onException and onFinally, they should be marked as suppressed") { 43 | implicit val manager: CanManage[SimpleResource.type] = new CanManage[SimpleResource.type] { 44 | override def onException(r: SimpleResource.type): Unit = throw new RuntimeException("onException") 45 | override def onFinally(r: SimpleResource.type): Unit = throw new RuntimeException("onFinally") 46 | } 47 | val managedResource = new DefaultManagedResource(SimpleResource) 48 | try { 49 | managedResource.map(_ => throw new IllegalArgumentException) 50 | throw new IllegalStateException 51 | } catch { 52 | case e: IllegalArgumentException => 53 | assert(e.getMessage == null) 54 | val suppressed = e.getSuppressed 55 | assert(suppressed(0).getMessage == "onException") 56 | assert(suppressed(1).getMessage == "onFinally") 57 | } 58 | } 59 | 60 | test("cached exception should be silently dropped") { 61 | val e = new IllegalArgumentException 62 | implicit val manager: CanManage[SimpleResource.type] = new CanManage[SimpleResource.type] { 63 | override def onException(r: SimpleResource.type): Unit = throw e 64 | override def onFinally(r: SimpleResource.type): Unit = throw e 65 | } 66 | val managedResource = new DefaultManagedResource(SimpleResource) 67 | try { 68 | managedResource.map(_ => throw e) 69 | throw new IllegalStateException 70 | } catch { 71 | case e: RuntimeException => 72 | } 73 | assert(e.getSuppressed.isEmpty) 74 | assert(e.getCause == null) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/scala/io/tmos/arm/CanManage.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | /** 4 | * For encapsulating the management logic of a resource. 5 | * 6 | * Default logic for any `java.lang.AutoClosable` is provided by the companion object, 7 | * which may be imported into current scope as implicits. 8 | * 9 | * Other types may be provided in scope by the user. For example 10 | * {{{ 11 | * import java.util.concurrent._ 12 | * import io.tmos.arm.Implicits._ 13 | * 14 | * implicit val manager: CanManage[ExecutorService] = new CanManage[ExecutorService] { 15 | * override def onFinally(pool: ExecutorService): Unit = { 16 | * pool.shutdown() // Disable new tasks from being submitted 17 | * try { 18 | * if (!pool.awaitTermination(10, TimeUnit.SECONDS)) { // wait for normal termination 19 | * pool.shutdownNow() // force terminate 20 | * if (!pool.awaitTermination(10, TimeUnit.SECONDS)) // wait for forced termination 21 | * throw new RuntimeException("ExecutorService did not terminate") 22 | * } 23 | * } catch { 24 | * case _: InterruptedException => 25 | * pool.shutdownNow() // (Re-)Cancel if current thread also interrupted 26 | * Thread.currentThread().interrupt() // Preserve interrupt status 27 | * } 28 | * } 29 | * override def onException(r: ExecutorService): Unit = {} 30 | * } 31 | * 32 | * for (manage(executorService) <- Executors.newSingleThreadExecutor.manage) { ... } 33 | * }}} 34 | * 35 | * @tparam R the type of the resource to manage 36 | */ 37 | trait CanManage[-R] { 38 | 39 | /** 40 | * Execution hook called after the managed block. 41 | * 42 | * This execution hook is called regardless if an exception is thrown. 43 | * 44 | * Usually resources are released or closed in the lifecycle. 45 | * 46 | * Implementors are free to permit exceptions thrown from this method, however 47 | * it is strongly advised to not have the 48 | * method throw `java.lang.InterruptedException`. This exception interacts with a thread's 49 | * interrupted status, and runtime misbehavior is likely to occur if an `java.lang.InterruptedException` 50 | * is suppressed. More generally, if it would cause problems for an exception to be suppressed, 51 | * the AutoCloseable.close method should not throw it." 52 | * 53 | * @param r the resource being managed 54 | */ 55 | def onFinally(r: R): Unit = {} 56 | 57 | /** 58 | * Execution hook called when an exception is thrown from the managed 59 | * block. This is executed prior to [onFinally]. 60 | * 61 | * Implementors are free to permit exceptions thrown from this method, however 62 | * note that any new exceptions thrown will be added as 63 | * a suppressed exception of the currently throwing exception. 64 | * Thus it is strongly advised that implementors do not throw any exceptions 65 | * if it would cause problems for an exception to be suppressed. 66 | * 67 | * @param r the resource being managed 68 | */ 69 | def onException(r: R): Unit = {} 70 | } 71 | 72 | 73 | /** 74 | * Companion object to the CanManage type trait. 75 | * 76 | * Contains common implementations of CanManage for AutoClosable Resources 77 | */ 78 | object CanManage { 79 | 80 | /** 81 | * Always call close on a AutoClosable after applied block, 82 | * regardless if block throws an exception or not. This is identical to 83 | * Java's try-with-resources. 84 | */ 85 | implicit object CloseOnFinally extends CanManage[AutoCloseable] { 86 | override def onFinally(r: AutoCloseable): Unit = if (r != null) r.close() 87 | } 88 | 89 | /** 90 | * Call close on a resource only if a exception is thrown in the applied block. 91 | * This is useful for instance if closing a resource needs to be delegated 92 | * elsewhere under normal circumstances, but abnormal circumstances should be 93 | * handled in the current scope. Such examples may include managing a resource 94 | * across threads. 95 | */ 96 | object CloseOnException extends CanManage[AutoCloseable] { 97 | override def onException(r: AutoCloseable): Unit = if (r != null) r.close() 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/test/scala/io/tmos/arm/CustomResourceSuite.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | import java.io.{BufferedReader, InputStreamReader, PrintWriter} 4 | import java.net.{InetAddress, ServerSocket, Socket, SocketException} 5 | import java.util.concurrent.{Callable, CompletableFuture, ExecutorService, Executors, TimeUnit} 6 | 7 | import org.scalatest.funsuite.AnyFunSuite 8 | 9 | class CustomResourceSuite extends AnyFunSuite { 10 | 11 | import Implicits._ 12 | 13 | implicit val execServiceManager: CanManage[ExecutorService] = new CanManage[ExecutorService] { 14 | override def onFinally(pool: ExecutorService): Unit = { 15 | pool.shutdown() // Disable new tasks from being submitted 16 | try { 17 | if (!pool.awaitTermination(10, TimeUnit.SECONDS)) { // wait for normal termination 18 | pool.shutdownNow() // force terminate 19 | if (!pool.awaitTermination(10, TimeUnit.SECONDS)) // wait for forced termination 20 | throw new RuntimeException("ExecutorService did not terminate") 21 | } 22 | } catch { 23 | case _: InterruptedException => 24 | pool.shutdownNow() // (Re-)Cancel if current thread also interrupted 25 | Thread.currentThread().interrupt() // Preserve interrupt status 26 | // It is very important that we do not propagate InterruptedException 27 | // as it may be added as it may be suppressed if an earlier 28 | // exception trumps its. This is the same generally for any close method. 29 | // See https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html#close-- 30 | // for details. 31 | } 32 | } 33 | override def onException(r: ExecutorService): Unit = {} 34 | } 35 | 36 | test("complex example of custom manager for Executor service and delegated resource management") { 37 | 38 | import Compat.CollectionConverters._ 39 | 40 | val serverSocketFuture = new CompletableFuture[ServerSocket] 41 | 42 | val callable = new Callable[Unit] { 43 | 44 | override def call(): Unit = { 45 | // Note that ServerSocket.accept() blocks but, does not throw 46 | // InterruptedException. Instead, to terminate the event loop, we need 47 | // to close server socket asynchronously. We delegate closing of server 48 | // socket to the main thread under normal circumstances, but in the 49 | // event this thread has thrown an exception, we will close. 50 | for (ss <- new ServerSocket(0, 0, InetAddress.getLoopbackAddress).closeOnException) { 51 | serverSocketFuture.complete(ss) 52 | while (!Thread.interrupted()) { // main event loop 53 | try { 54 | for { 55 | connection <- ss.accept.closeOnFinally // block here 56 | out <- new PrintWriter(connection.getOutputStream, true).closeOnFinally 57 | in <- new BufferedReader(new InputStreamReader(connection.getInputStream)).closeOnFinally 58 | line <- in.lines().iterator().asScala 59 | } out.println(line.toUpperCase) 60 | } catch { 61 | case _: SocketException if ss.isClosed => 62 | // at this point server socket has been closed via the main thread 63 | // we set interrupt status and terminate the event loop / thread 64 | Thread.currentThread().interrupt() 65 | } 66 | } 67 | } 68 | } 69 | } 70 | 71 | // manage an executor service using the user defined CanManage 72 | val completedFuture = for ( 73 | executorService <- Executors.newSingleThreadExecutor().manage 74 | ) yield { 75 | val future = executorService.submit(callable) 76 | for (ss <- serverSocketFuture.get.closeOnFinally) { 77 | val upperPhrase = for { 78 | s <- new Socket(InetAddress.getLoopbackAddress, ss.getLocalPort).closeOnFinally 79 | out <- new PrintWriter(s.getOutputStream, true).closeOnFinally 80 | in <- new BufferedReader(new InputStreamReader(s.getInputStream)).closeOnFinally 81 | } yield { 82 | out.println("hello") 83 | out.println("world") 84 | in.readLine() + ' ' + in.readLine() 85 | } 86 | assert(upperPhrase === "HELLO WORLD") 87 | } 88 | // at this point the callable event loop is terminating 89 | future 90 | } 91 | // at this point the callable event loop has been terminated 92 | 93 | assert(!completedFuture.isCancelled) 94 | assert(completedFuture.isDone) 95 | completedFuture.get // should not block or throw any error 96 | 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/test/scala/io/tmos/arm/ArmMethodsSuite.scala: -------------------------------------------------------------------------------- 1 | package io.tmos.arm 2 | 3 | import java.io.Closeable 4 | 5 | import io.tmos.arm.ArmMethods._ 6 | 7 | import scala.collection.mutable 8 | import org.scalatest.wordspec.AnyWordSpec 9 | 10 | class ArmMethodsSuite extends AnyWordSpec { 11 | 12 | "AutoClosable resources" when { 13 | 14 | "using default CanManage manager" should { 15 | 16 | "be the CloseOnFinally implementation" in { 17 | val manager: CanManage[Closeable] = implicitly[CanManage[Closeable]] 18 | assert(manager == CanManage.CloseOnFinally) 19 | } 20 | 21 | "be closed in reverse order when foreach-ing over them" in { 22 | var resource1: SimpleAutoCloseableTest = null 23 | var resource2: SimpleAutoCloseableTest = null 24 | var resource3: SimpleAutoCloseableTest = null 25 | 26 | resource1 = new SimpleAutoCloseableTest("1") { 27 | override def close(): Unit = { 28 | super.close() 29 | assert(resource2.isClosed) 30 | assert(resource3.isClosed) 31 | } 32 | } 33 | 34 | resource2 = new SimpleAutoCloseableTest("2") { 35 | override def close(): Unit = { 36 | super.close() 37 | assert(!resource1.isClosed) 38 | assert(resource3.isClosed) 39 | } 40 | } 41 | 42 | resource3 = new SimpleAutoCloseableTest("3") { 43 | override def close(): Unit = { 44 | super.close() 45 | assert(!resource1.isClosed) 46 | assert(!resource2.isClosed) 47 | } 48 | } 49 | 50 | for { 51 | r1 <- manage(resource1) 52 | r2 <- manage(resource2) 53 | r3 <- manage(resource3) 54 | } { 55 | assert(!r1.isClosed) 56 | assert(!r2.isClosed) 57 | assert(!r3.isClosed) 58 | } 59 | 60 | assert(resource1.isClosed) 61 | assert(resource2.isClosed) 62 | assert(resource3.isClosed) 63 | } 64 | 65 | "be closed in reverse order when yield-ing over them" in { 66 | var resource1: SimpleAutoCloseableTest = null 67 | var resource2: SimpleAutoCloseableTest = null 68 | var resource3: SimpleAutoCloseableTest = null 69 | 70 | resource1 = new SimpleAutoCloseableTest("1") { 71 | override def close(): Unit = { 72 | super.close() 73 | assert(resource2.isClosed) 74 | assert(resource3.isClosed) 75 | } 76 | } 77 | 78 | resource2 = new SimpleAutoCloseableTest("2") { 79 | override def close(): Unit = { 80 | super.close() 81 | assert(!resource1.isClosed) 82 | assert(resource3.isClosed) 83 | } 84 | } 85 | 86 | resource3 = new SimpleAutoCloseableTest("3") { 87 | override def close(): Unit = { 88 | super.close() 89 | assert(!resource1.isClosed) 90 | assert(!resource2.isClosed) 91 | } 92 | } 93 | 94 | val value = for { 95 | r1 <- manage(resource1) 96 | r2 <- manage(resource2) 97 | r3 <- manage(resource3) 98 | } yield r1.msg + r2.msg + r3.msg 99 | 100 | assert(resource1.isClosed) 101 | assert(resource2.isClosed) 102 | assert(resource3.isClosed) 103 | assert(value === "123") 104 | } 105 | 106 | "propagate exception thrown in main body and be closed" in { 107 | val resource = new SimpleAutoCloseableTest("msg") 108 | try { 109 | for (r <- manage(resource)) 110 | r.except("from main body") 111 | } catch { 112 | case e: RuntimeException => 113 | assert(e.getMessage === "from main body") 114 | } 115 | assert(resource.isClosed) 116 | } 117 | 118 | "propagate exception thrown on close" in { 119 | val resource = new SimpleAutoCloseableTest("msg") { 120 | override def close(): Unit = { 121 | super.close() 122 | except("onClose") 123 | } 124 | } 125 | try { 126 | for (r <- manage(resource)) yield { 127 | r.msg 128 | } 129 | } catch { 130 | case e: RuntimeException => 131 | assert(e.getMessage === "onClose") 132 | } 133 | assert(resource.isClosed) 134 | } 135 | 136 | "propagate first exception thrown in body with subsequent exception thrown on close attached" in { 137 | val resource = new SimpleAutoCloseableTest("msg") { 138 | override def close(): Unit = { 139 | super.close() 140 | except("second") 141 | } 142 | } 143 | try { 144 | for (r <- manage(resource)) 145 | r.except("first") 146 | } catch { 147 | case e: RuntimeException => 148 | assert(e.getMessage === "first") 149 | val suppressed = e.getSuppressed 150 | assert(suppressed.length === 1) 151 | assert(suppressed(0).getMessage === "second") 152 | } 153 | assert(resource.isClosed) 154 | } 155 | 156 | "handle orthogonal type variance in CanManage and body lambda" in { 157 | 158 | trait SimpleTraitTest { 159 | val msg: String 160 | } 161 | 162 | class PolyCloseableTest(override val msg: String) 163 | extends SimpleAutoCloseableTest(msg) 164 | with SimpleTraitTest { 165 | } 166 | 167 | val resource: PolyCloseableTest = new PolyCloseableTest("msg") 168 | 169 | // there is no type lineage between SimpleTestTrait and AutoClosable 170 | 171 | // use map so we can pass lambda accepting SimpleTraitTest super class 172 | val msg = manage(resource).map((myTrait: SimpleTraitTest) => myTrait.msg) 173 | 174 | assert(msg == "msg") 175 | assert(resource.isClosed) 176 | 177 | } 178 | 179 | } 180 | 181 | "using closeOnException CanManage implicit" should { 182 | 183 | implicit val canManage: CanManage[AutoCloseable] = CanManage.CloseOnException 184 | 185 | "not close when not excepting" in { 186 | 187 | val (r1, r2, r3) = for { 188 | r1 <- manage(new SimpleAutoCloseableTest("1")) 189 | r2 <- manage(new SimpleAutoCloseableTest("2")) 190 | r3 <- manage(new SimpleAutoCloseableTest("3")) 191 | } yield (r1, r2, r3) 192 | 193 | assert(!r1.isClosed) 194 | assert(!r2.isClosed) 195 | assert(!r3.isClosed) 196 | 197 | // because we should always clean up resources 198 | for { 199 | _ <- closeOnFinally(r1) 200 | _ <- closeOnFinally(r2) 201 | _ <- closeOnFinally(r3) 202 | } {} 203 | 204 | assert(r1.isClosed) 205 | assert(r2.isClosed) 206 | assert(r3.isClosed) 207 | 208 | } 209 | 210 | "close all if exception thrown at any point" in { 211 | 212 | // only for asserting that resources are closed 213 | val resources = mutable.ArrayBuffer.empty[SimpleAutoCloseableTest] 214 | 215 | def buildResource(msg: String): SimpleAutoCloseableTest = { 216 | resources += new SimpleAutoCloseableTest(msg) 217 | resources.last 218 | } 219 | 220 | try { 221 | for { 222 | _ <- manage(buildResource("1")) 223 | _ <- manage(buildResource("2")) 224 | _ <- manage(buildResource("3")) 225 | } { 226 | throw new RuntimeException("ops") 227 | } 228 | } catch { 229 | case e: Throwable => 230 | assert(e.getMessage == "ops") 231 | } 232 | 233 | assert(resources.nonEmpty) 234 | for (r <- resources) { 235 | assert(r.isClosed) 236 | } 237 | 238 | } 239 | 240 | } 241 | 242 | } 243 | 244 | "different resources" when { 245 | "of the same type" should { 246 | "be able to use different managers" in { 247 | 248 | def onFinally[R](f: R => Unit): CanManage[R] = new CanManage[R] { 249 | override def onFinally(r: R): Unit = f(r) 250 | } 251 | 252 | var onFinally1 = false 253 | var onFinally2 = false 254 | 255 | for { 256 | _ <- manage(())(onFinally[Unit](_ => onFinally1 = true)) 257 | _ <- manage(())(onFinally[Unit](_ => onFinally2 = true)) 258 | } () 259 | 260 | assert(onFinally1) 261 | assert(onFinally2) 262 | } 263 | 264 | } 265 | 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ARM4S - Automatic Resource Management for Scala 2 | 3 | |Branch|Status| 4 | |:-----|:----:| 5 | |*master*|[![Build Status](https://travis-ci.com/tmoschou/arm4s.svg?branch=master)](https://travis-ci.com/tmoschou/arm4s)| 6 | |*develop*|[![Build Status](https://travis-ci.com/tmoschou/arm4s.svg?branch=develop)](https://travis-ci.com/tmoschou/arm4s)| 7 | 8 | [![Scaladocs](https://www.javadoc.io/badge/io.tmos/arm4s_2.12.svg?label=Scaladoc)](https://www.javadoc.io/doc/io.tmos/arm4s_2.12) 9 | 10 | This library provides a way of succinctly dealing with resources in an exception 11 | safe manner. This library can provided identical exception handling and 12 | execution semantics to Java's `try`-with-resource statement and a substitute for Scala 13 | which does not have an equivalent construct natively. 14 | 15 | More generally, managed resources are not limited to `java.lang.AutoClosable`s. 16 | Any type with a **on-finally** or **on-exception** lifecycle, can be supported with 17 | user-defined implicit adapters. For instance a `java.util.concurrent.ExecutorService` 18 | may have a user-defined shutdown hook executed on-finally. See Comprehensive Example below. 19 | 20 | Managed resources are treated as a singleton enumerator whose managed lifecycle is 21 | scoped to an applied expression (or block). Unlike Java's constructs, ARM4S's 22 | applied block can yield results, ensuring that resources are closed properly 23 | before returning. 24 | 25 | For example: Manage multiple resources and yield a result 26 | 27 | ```scala 28 | import io.tmos.arm.Implicits._ 29 | import java.io._ 30 | import java.net.{Socket, InetAddress, ServerSocket} 31 | 32 | val line = for { 33 | socket <- new Socket(InetAddress.getLoopbackAddress, port).manage 34 | out <- new PrintWriter(socket.getOutputStream, true).manage 35 | in <- new BufferedReader(new InputStreamReader(socket.getInputStream)).manage 36 | } yield { 37 | val line = in.readLine() 38 | out.println(line.toUpperCase) 39 | return line 40 | } 41 | ``` 42 | 43 | For more examples see, the Examples section below. 44 | 45 | # Rationale 46 | 47 | Manual management of resources have proven to be error prone, and when done 48 | "correctly" - ugly. 49 | 50 | Refer to 51 | * Joshua Bloch's Original 52 | [Proposal for ARM](http://mail.openjdk.java.net/pipermail/coin-dev/2009-February/000011.html). 53 | * Oracle's tech article on 54 | [Try-with-resources](http://www.oracle.com/technetwork/articles/java/trywithresources-401775.html). 55 | 56 | For instance, if you are doing any of the following, then you should consider this library. 57 | 58 | ```scala 59 | // don't do this 60 | val r = new Resource(...) 61 | try { 62 | doStuff1(r) // assume we throw an exception here 63 | doStuff2(r) 64 | } finally { 65 | r.close() // what if we throw an exception here too? 66 | } 67 | ``` 68 | 69 | Not good - we just masked (lost) the first 'important' exception with no 70 | indication as to whether our main try block completed normally, or if it 71 | did not, then whereabouts it did fail. 72 | 73 | You may be tempted to wrap the close in another try/catch and log it 74 | so that the first exception isn't ever dropped. 75 | 76 | ```scala 77 | // don't do this either 78 | val r = new Resource(...) 79 | try { 80 | ... 81 | } finally { 82 | try { 83 | r.close() // close quietly 84 | } catch { 85 | case e => log.warn(e) 86 | } 87 | } 88 | ``` 89 | 90 | Still Bad - If `close()` throws an exception, the application has no idea one was thrown on close and 91 | with no opportunity to fail fast and safely. Especially so, if the main try clause didn't 92 | throw any exception at all, in which case _no_ exception is propagated. 93 | 94 | Instead we should utilise `Throwable.addSuppressed` to propagate the 'first' important exception 95 | with any subsequent exceptions attached as 'suppressed'. 96 | 97 | Now, that was for one resource. - What if you needed to close multiple resources 98 | in a finally block, each of which could independently throw an exception on close. 99 | Could you get it right? If you do - well done. 100 | But the next developer who reads is unlikely to understand it. 101 | 102 | This is where this library comes in and does things *correctly* and *succinctly*, 103 | ensuring that the first exception thrown is the one that 104 | is propagated, and any subsequent exceptions thrown are added to the head exception as suppressed. 105 | 106 | ## Exception Behaviour 107 | This library differs from other Scala ARM libraries in that it has been designed with consideration for different 108 | exception scenarios and with the following goals regarding exception safe behaviour: 109 | 110 | 1. The `onFinally` (by default delegates to `close` for `java.lang.AutoClosables`s) and `onException` management 111 | hooks of a resource method must be called even if the body throws _any_ `Throwable` exception 112 | including fatal ones to ensure that no resources are leaked. Example of fatal exceptions include anything not 113 | matched by `scala.util.control.NonFatal` such as `InterruptedException`, `ControlThrowable` and 114 | `VirtualMachineError`. Though you should not try to handle such fatal errors, finally logic should still 115 | (attempted to) be executed regardless. 116 | 117 | 2. The `onFinally` and `onException` execution hooks are permitted to throw any `Throwable` too, 118 | possibly additional to exceptions thrown from the main block. 119 | 120 | 3. Any `Throwable` thrown by `onFinally` or `onException` should not mask any exception thrown firstly by the 121 | body, if any. Instead the secondary exception(s) thrown should be caught and recorded as a 122 | suppressed exception against the primary (currently throwing) exception. 123 | 124 | 4. Lastly we differ slightly to Java's implementation of `try`-with-resources in which we permit 125 | the corner case where exceptions thrown `onFinally` or `onException` may be the same instance 126 | as thrown by the applied expression. Where Java will throw a new `IllegalArgumentException` on 127 | attempts to self suppress, we will silently drop repeated instances as usually it is not the 128 | users fault that an underlying or decorated resource used a cached exception rather than 129 | generate a new exception/stacktrace etc. 130 | 131 | ## Including ARM4S in your project 132 | 133 | In SBT: 134 | ```scala 135 | libraryDependencies += "io.tmos" %% "arm4s" % "1.1.0" 136 | ``` 137 | In Maven: 138 | ```xml 139 | 140 | io.tmos 141 | arm4s_${scala.binary.version} 142 | 1.1.0 143 | 144 | ``` 145 | ## Using ARM4S 146 | 147 | There are two ways you can construct a managed resource 148 | 149 | Explicitly 150 | ```scala 151 | import io.tmos.arm.ArmMethods._ 152 | for (r <- manage(resource)) { 153 | ... 154 | } 155 | ``` 156 | 157 | Or using implicit decorator methods 158 | ```scala 159 | import io.tmos.arm.Implicits._ 160 | for (r <- resource.manage) { 161 | ... 162 | } 163 | ``` 164 | 165 | Any resource of type `T` for which an implicit `CanManage[T]` adapter is provided in scope can be managed. 166 | 167 | By default the implicit `CloseOnFincally extends CanManage[AutoClosable]` 168 | manager defined in the CanManage companion object is used, which calls `close` 169 | on-finally, unless a higher priority implicit is in scope. 170 | Alternatively `closeOnFinally` method may be used in place of `manage` method, 171 | To explicitly use this adapter. 172 | 173 | ```scala 174 | import io.tmos.arm.ArmMethods._ 175 | for (r <- closeOnFinally(resource)) {...} 176 | ``` 177 | 178 | or using the implicit method 179 | ```scala 180 | import io.tmos.arm.Implicits._ 181 | for (r <- resource.closeOnFinally) {...} 182 | ``` 183 | 184 | Managed resources may be composed together/chained in a monadic manner that allows for optionally yielding 185 | results or imperatively using `for`-comprehensions. 186 | 187 | ## Examples 188 | Using 189 | [For-Comprehensions](https://www.scala-lang.org/files/archive/spec/2.12/06-expressions.html#for-comprehensions-and-for-loops) 190 | ```scala 191 | import io.tmos.arm.ArmMethods._ 192 | val jsonMap: Map[String, Any] = for { 193 | inputStream <- manage(new FileInputStream("data.json")) 194 | } yield { 195 | JsonMethods.parse(inputStream).extract[Map[String, Any]] 196 | } 197 | ``` 198 | 199 | We could also write in the following fluid style 200 | ```scala 201 | import io.tmos.arm.Implicits._ 202 | val jsonMap: Map[String, Any] = new FileInputStream("data.json") 203 | .manage 204 | .map(JsonMethods.parse(_)) 205 | .extract[Map[String, Any]] 206 | ``` 207 | Or if composing multiple resources this can be done easily too 208 | ```scala 209 | import io.tmos.arm.ArmMethods._ 210 | val result = for { 211 | a <- manage(new A) 212 | b <- manage(a.getB) 213 | c <- manage(new C(b)) 214 | } yield { 215 | // ... 216 | } 217 | ``` 218 | 219 | Resources will be safely managed in reverse declaration order even if a later enumerator 220 | declaration threw an exception prior to the main body. For example if `new C(b)` 221 | thew an exception, then `b` followed by `a`'s on-exception/on-finally lifecycle 222 | hooks will be executed. 223 | 224 | There may be cases where you need to override the default behaviour for AutoClosable and 225 | close resources safely _only_ on-exception, such as when needing to construct 226 | multiple resources atomically or delegating close to a different thread/scope. 227 | This can be achieved using the predefined `CloseOnException` manager. 228 | 229 | ```scala 230 | import io.tmos.arm.ArmMethods._ 231 | 232 | // this implicit now has higher priority then the default CloseOnFinally 233 | implicit val canManage: CanManage[AutoCloseable] = CanManage.CloseOnException 234 | 235 | def openAll(): (A, B, C) = for { 236 | a <- manage(new A) 237 | b <- manage(new B) 238 | c <- manage(new C) 239 | } yield (a,b,c) 240 | ``` 241 | 242 | We also provided `closeOnException` method similarly to `closeOnFinally`, for 243 | explicit usage of this adapter without needing to import it as a higher priority 244 | implicit. 245 | 246 | ## Comprehensive Example 247 | 248 | Here is a comprehensive (hypothetical) example of managing multiple resources implicitly, 249 | including an `ExectorService` which we define `onFinally` logic for. 250 | This sample code runs a server socket in a separate thread echoing back text it 251 | receives in uppercase. 252 | 253 | ```scala 254 | import io.tmos.arm.Implicits._ 255 | import scala.collection.JavaConverters._ 256 | 257 | implicit val manager: CanManage[ExecutorService] = new CanManage[ExecutorService] { 258 | override def onFinally(pool: ExecutorService): Unit = { 259 | pool.shutdown() // Disable new tasks from being submitted 260 | try { 261 | if (!pool.awaitTermination(10, TimeUnit.SECONDS)) { // wait for normal termination 262 | pool.shutdownNow() // force terminate 263 | if (!pool.awaitTermination(10, TimeUnit.SECONDS)) // wait for forced termination 264 | throw new RuntimeException("ExecutorService did not terminate") 265 | } 266 | } catch { 267 | case _: InterruptedException => 268 | pool.shutdownNow() // (Re-)Cancel if current thread also interrupted 269 | Thread.currentThread().interrupt() // Preserve interrupt status 270 | // It is very important that we do not propagate InterruptedException 271 | // as it may be added as it may be suppressed if an earlier 272 | // exception trumps its. This is the same generally for any close method. 273 | // See https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html#close-- 274 | // for details. 275 | } 276 | } 277 | override def onException(r: ExecutorService): Unit = {} 278 | } 279 | 280 | val serverSocketFuture = new CompletableFuture[ServerSocket] 281 | 282 | val callable = new Callable[Unit] { 283 | override def call(): Unit = { 284 | // Note that ServerSocket.accept() blocks but, does not throw 285 | // InterruptedException. Instead, to terminate the event loop, we need 286 | // to close server socket asynchronously. We delegate closing of server 287 | // socket to the main thread under normal circumstances, but in the 288 | // event this thread has thrown an exception, we will close. 289 | for (ss <- new ServerSocket(0, 0, InetAddress.getLoopbackAddress).closeOnException) { 290 | serverSocketFuture.complete(ss) 291 | while (!Thread.interrupted()) { // main event loop 292 | try { 293 | for { 294 | connection <- ss.accept.closeOnFinally // block here 295 | out <- new PrintWriter(connection.getOutputStream, true).closeOnFinally 296 | in <- new BufferedReader(new InputStreamReader(connection.getInputStream)).closeOnFinally 297 | line <- in.lines().iterator().asScala 298 | } out.println(line.toUpperCase) 299 | } catch { 300 | case _: SocketException if ss.isClosed => 301 | // at this point server socket has been closed via the main thread 302 | // we set interrupt status and terminate the event loop / thread 303 | Thread.currentThread().interrupt() 304 | } 305 | } 306 | } 307 | } 308 | } 309 | 310 | // manage an executor service using the user defined CanManage 311 | val completedFuture = for ( 312 | executorService <- Executors.newSingleThreadExecutor().manage 313 | ) yield { 314 | val future = executorService.submit(callable) 315 | for (ss <- serverSocketFuture.get.closeOnFinally) { 316 | val upperPhrase = for { 317 | s <- new Socket(InetAddress.getLoopbackAddress, ss.getLocalPort).closeOnFinally 318 | out <- new PrintWriter(s.getOutputStream, true).closeOnFinally 319 | in <- new BufferedReader(new InputStreamReader(s.getInputStream)).closeOnFinally 320 | } yield { 321 | out.println("hello") 322 | out.println("world") 323 | in.readLine() + ' ' + in.readLine() 324 | } 325 | assert(upperPhrase === "HELLO WORLD") 326 | } 327 | // at this point the callable event loop is terminating 328 | future 329 | } 330 | // at this point the callable event loop has been terminated 331 | 332 | assert(!completedFuture.isCancelled) 333 | assert(completedFuture.isDone) 334 | completedFuture.get // should not block or throw any error 335 | ``` 336 | --------------------------------------------------------------------------------