From 05fc16e9f4c4239ef76f4c280904b8bcc9b8b745 Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Thu, 29 Jan 2026 10:45:35 +0100 Subject: [PATCH 1/2] Allow 'using' implementation support null resources and null return values. --- .../absa/pramen/core/utils/UsingUtils.scala | 30 +++++++++-------- .../core/tests/utils/UsingUtilsSuite.scala | 33 ++++++++++++++++++- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala b/pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala index 2e37c8a5..d60b0b1b 100644 --- a/pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala +++ b/pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala @@ -31,32 +31,34 @@ object UsingUtils { * is thrown with the closure's exception added as suppressed */ def using[T <: AutoCloseable,U](resource: => T)(action: T => U): U = { - var thrownException: Option[Throwable] = None - var suppressedException: Option[Throwable] = None + var actionExceptionOpt: Option[Throwable] = None val openedResource = resource - val result = try { - Option(action(openedResource)) + try { + return action(openedResource) } catch { case NonFatal(ex) => - thrownException = Option(ex) - None + actionExceptionOpt = Option(ex) } finally if (openedResource != null) { try openedResource.close() catch { - case NonFatal(ex) => suppressedException = Option(ex) + case NonFatal(closeException) => + actionExceptionOpt match { + case Some(actionException) => + actionException.addSuppressed(closeException) + throw actionException + case None => + throw closeException + } } } - (thrownException, suppressedException) match { - case (Some(thrown), Some(suppressed)) => - thrown.addSuppressed(suppressed) - throw thrown - case (Some(thrown), None) => throw thrown - case (None, Some(suppressed)) => throw suppressed - case (None, None) => result.getOrElse(throw new IllegalArgumentException("Action returned null")) + // It is not possible to return a valid value of type U at this point so the rest of code should return Nothing + actionExceptionOpt match { + case Some(ex) => throw ex + case None => throw new IllegalArgumentException("Unreachable code") } } } diff --git a/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala b/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala index 236eb326..11464745 100644 --- a/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala +++ b/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala @@ -54,7 +54,7 @@ class UsingUtilsSuite extends AnyWordSpec { assert(resource.closeCallCount == 1) } - "throw an exception on null resource" in { + "handle a null resource resource" in { var resourceWasNull = false UsingUtils.using(null: AutoCloseableSpy) { res => @@ -64,6 +64,37 @@ class UsingUtilsSuite extends AnyWordSpec { assert(resourceWasNull) } + "handle a null resource resource and action throw" in { + var exceptionThrown = false + var resourceWasNull = false + + try { + UsingUtils.using(null: AutoCloseableSpy) { res => + resourceWasNull = res == null + throw new RuntimeException("Action throws") + } + } catch { + case ex: Throwable => + exceptionThrown = true + assert(ex.getMessage.contains("Action throws")) + } + + assert(exceptionThrown) + assert(resourceWasNull) + } + + "handle a null return value" in { + var resourceWasNull = false + + val result = UsingUtils.using(null: AutoCloseableSpy) { res => + resourceWasNull = res == null + null: String + } + + assert(resourceWasNull) + assert(result == null) + } + "handle exceptions when a resource is created" in { var exceptionThrown = false var resource: AutoCloseableSpy = null From d9c108cd5a8bf4451958592a8956521d2e88ebb7 Mon Sep 17 00:00:00 2001 From: Ruslan Iushchenko Date: Thu, 29 Jan 2026 11:38:14 +0100 Subject: [PATCH 2/2] Simplify the code for Using mechanics. Thanks @coderabbitai! --- .../absa/pramen/core/utils/UsingUtils.scala | 33 +++++++------------ .../core/tests/utils/UsingUtilsSuite.scala | 4 +-- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala b/pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala index d60b0b1b..1c6aa421 100644 --- a/pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala +++ b/pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala @@ -16,13 +16,11 @@ package za.co.absa.pramen.core.utils -import scala.util.control.NonFatal - object UsingUtils { /** * Executes the given action with a resource that implements the AutoCloseable interface, ensuring * proper closure of the resource. Any exception that occurs during the action or resource closure - * is handled appropriately, with suppressed exceptions added where relevant. Null resources are not supported. + * is handled appropriately, with suppressed exceptions added where relevant. * * @param resource a lazily evaluated resource that implements AutoCloseable * @param action a function to be executed using the provided resource @@ -30,35 +28,28 @@ object UsingUtils { * @throws Throwable if either the action or resource closure fails. If both fail, the action's exception * is thrown with the closure's exception added as suppressed */ - def using[T <: AutoCloseable,U](resource: => T)(action: T => U): U = { - var actionExceptionOpt: Option[Throwable] = None + def using[T <: AutoCloseable, U](resource: => T)(action: T => U): U = { + var actionException: Throwable = null val openedResource = resource try { - return action(openedResource) + action(openedResource) } catch { - case NonFatal(ex) => - actionExceptionOpt = Option(ex) + case t: Throwable => + actionException = t + throw t } finally if (openedResource != null) { try openedResource.close() catch { - case NonFatal(closeException) => - actionExceptionOpt match { - case Some(actionException) => - actionException.addSuppressed(closeException) - throw actionException - case None => - throw closeException + case closeException: Throwable => + if (actionException != null) { + actionException.addSuppressed(closeException) + } else { + throw closeException } } } - - // It is not possible to return a valid value of type U at this point so the rest of code should return Nothing - actionExceptionOpt match { - case Some(ex) => throw ex - case None => throw new IllegalArgumentException("Unreachable code") - } } } diff --git a/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala b/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala index 11464745..f28ad44f 100644 --- a/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala +++ b/pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala @@ -54,7 +54,7 @@ class UsingUtilsSuite extends AnyWordSpec { assert(resource.closeCallCount == 1) } - "handle a null resource resource" in { + "handle a null resource" in { var resourceWasNull = false UsingUtils.using(null: AutoCloseableSpy) { res => @@ -64,7 +64,7 @@ class UsingUtilsSuite extends AnyWordSpec { assert(resourceWasNull) } - "handle a null resource resource and action throw" in { + "handle a null resource and action throw" in { var exceptionThrown = false var resourceWasNull = false