From 0857cffcd6b61dc359b1d88e44a33c47ee3613b4 Mon Sep 17 00:00:00 2001 From: "Wessel W. Bakker" Date: Mon, 25 Jul 2022 16:11:57 +0200 Subject: [PATCH] Feature/improve error handling (#1290) * - Fixed unexpected errors returning success result. * - Added TimeoutException as a baker exception, and convert timeout exceptions from akka or BakerF into this new type of exception. * - Added UnknownBakerException if baker exception could not be decoded. - Disabled stack trace if the baker exception is created in the json decoder (otherwise the stacktrace points to the decoder) - Updated unit test and fixed its warnings. * - Rename method to be consistent. * - Removed addition that was not supposed to be there. Co-authored-by: Wessel W. Bakker --- .../ing/bakery/baker/StateRuntimeSpec.scala | 30 ++--- bakery/dashboard/package.json | 2 +- .../ing/bakery/baker/BakeryExecutorJava.scala | 7 +- .../ing/baker/runtime/akka/AkkaBaker.scala | 33 +++--- .../baker/runtime/common/BakerException.scala | 111 +++++++++++------- .../com/ing/baker/runtime/model/BakerF.scala | 25 +++- .../runtime/serialization/JsonDecoders.scala | 3 +- 7 files changed, 132 insertions(+), 79 deletions(-) diff --git a/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala b/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala index 0cde9b60c..e11a91ed1 100644 --- a/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala +++ b/bakery/baker-state-k8s/src/test/scala/com/ing/bakery/baker/StateRuntimeSpec.scala @@ -7,7 +7,7 @@ import com.ing.baker.runtime.akka.{AkkaBaker, AkkaBakerConfig} import com.ing.baker.runtime.common.BakerException.NoSuchProcessException import com.ing.baker.runtime.common.{BakerException, SensoryEventStatus} import com.ing.baker.runtime.model.{InteractionInstance, InteractionManager} -import com.ing.baker.runtime.scaladsl.{Baker, EventInstance, InteractionInstanceDescriptor, InteractionInstanceInput} +import com.ing.baker.runtime.scaladsl.{Baker, EventInstance, InteractionInstanceInput} import com.ing.baker.types._ import com.ing.bakery.baker.mocks.KubeApiServer import com.ing.bakery.mocks.{EventListener, RemoteInteraction} @@ -72,9 +72,9 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { test("Adding a recipe directly") { context => for { - _ <- io(context.client.addRecipe(recipe, true)) + _ <- io(context.client.addRecipe(recipe, validate = true)) allRecipesBefore <- io(context.client.getAllRecipes) - _ <- io(context.client.addRecipe(otherRecipe, true)) + _ <- io(context.client.addRecipe(otherRecipe, validate = true)) allRecipesAfter <- io(context.client.getAllRecipes) } yield { allRecipesBefore.values.map(_.compiledRecipe.name).toSet shouldBe Set("ItemReservation.recipe") @@ -112,8 +112,8 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { _ <- context.remoteInteractionKubernetes.respondsWithReserveItems() _ <- context.kubeApiServer.deployInteraction() _ <- awaitForInteractionDiscovery(context) - _ <- io(context.client.addRecipe(recipe, true)) - _ <- io(context.client.addRecipe(SimpleRecipe.compiledRecipe, true)) + _ <- io(context.client.addRecipe(recipe, validate = true)) + _ <- io(context.client.addRecipe(SimpleRecipe.compiledRecipe, validate = true)) recipeInformation <- io(context.client.getRecipe(recipeId)) interactionInformation <- io(context.client.getInteraction("ReserveItems")) noSuchRecipeError <- io(context.client @@ -133,7 +133,7 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { "ItemsReserved" -> Map("reservedItems" -> RecordType(Seq(RecordField("items", ListType(RecordType(Seq(RecordField("itemId", CharArray))))), RecordField("data", ByteArray)))) )) - noSuchRecipeError shouldBe Some(BakerException.NoSuchRecipeException("nonexistent")) + noSuchRecipeError shouldBe Some(BakerException.NoSuchRecipeException("nonexistent", disableStackTrace = true)) allRecipes.get(recipeId).map(_.compiledRecipe.name) shouldBe Some(recipe.name) allRecipes.get(SimpleRecipe.compiledRecipe.recipeId).map(_.compiledRecipe.name) shouldBe Some("Simple") } @@ -170,7 +170,7 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { .recover { case e: BakerException => Some(e) }) state <- io(context.client.getRecipeInstanceState(recipeInstanceId)) } yield { - e shouldBe Some(BakerException.ProcessAlreadyExistsException(recipeInstanceId)) + e shouldBe Some(BakerException.ProcessAlreadyExistsException(recipeInstanceId, disableStackTrace = true)) state.recipeInstanceId shouldBe recipeInstanceId } } @@ -185,7 +185,7 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { .bake("nonexistent", recipeInstanceId) .map(_ => None) .recover { case e => Some(e) }) - } yield e shouldBe Some(BakerException.NoSuchRecipeException("nonexistent")) + } yield e shouldBe Some(BakerException.NoSuchRecipeException("nonexistent", disableStackTrace = true)) } test("Baker.getRecipeInstanceState (fails with NoSuchProcessException)") { context => @@ -198,7 +198,7 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { .map(_ => None) .recover { case e => Some(e) }) } yield { - e shouldBe Some(NoSuchProcessException("nonexistent")) + e shouldBe Some(NoSuchProcessException("nonexistent", disableStackTrace = true)) } } @@ -242,8 +242,8 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { serverState <- io(context.client.getRecipeInstanceState(recipeInstanceId)) _ <- context.remoteInteractionKubernetes.didNothing } yield { - result shouldBe Some(BakerException.IllegalEventException("No event with name 'nonexistent' found in recipe 'ItemReservation.recipe'")) - serverState.events.map(_.name) should not contain ("OrderPlaced") + result shouldBe Some(BakerException.IllegalEventException("No event with name 'nonexistent' found in recipe 'ItemReservation.recipe'", disableStackTrace = true)) + serverState.events.map(_.name) should not contain "OrderPlaced" } } @@ -310,7 +310,7 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { } } yield { state1 should contain("OrderPlaced") - state1 should not contain ("ItemsReserved") + state1 should not contain "ItemsReserved" state2 should contain("OrderPlaced") state2 should contain("ItemsReserved") } @@ -339,7 +339,7 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { } } yield { state1 should contain("OrderPlaced") - state1 should not contain ("ItemsReserved") + state1 should not contain "ItemsReserved" state2 should contain("OrderPlaced") state2 should contain("ItemsReserved") eventState shouldBe Some("resolution-item") @@ -364,9 +364,9 @@ class StateRuntimeSpec extends BakeryFunSpec with Matchers { } } yield { state1 should contain("OrderPlaced") - state1 should not contain ("ItemsReserved") + state1 should not contain "ItemsReserved" state2 should contain("OrderPlaced") - state2 should not contain ("ItemsReserved") + state2 should not contain "ItemsReserved" } } } diff --git a/bakery/dashboard/package.json b/bakery/dashboard/package.json index e14b02df2..8fd8aec54 100644 --- a/bakery/dashboard/package.json +++ b/bakery/dashboard/package.json @@ -48,4 +48,4 @@ "ts-node": "^8.10.2", "typescript": "~4.6.4" } -} \ No newline at end of file +} diff --git a/bakery/state/src/main/scala/com/ing/bakery/baker/BakeryExecutorJava.scala b/bakery/state/src/main/scala/com/ing/bakery/baker/BakeryExecutorJava.scala index 717718a3f..049c6b7f2 100644 --- a/bakery/state/src/main/scala/com/ing/bakery/baker/BakeryExecutorJava.scala +++ b/bakery/state/src/main/scala/com/ing/bakery/baker/BakeryExecutorJava.scala @@ -10,7 +10,7 @@ import io.circe.generic.auto._ import io.circe.parser.parse import java.nio.charset.Charset -import java.util.Optional +import java.util.{Optional, UUID} import java.util.concurrent.{CompletableFuture => JFuture} import scala.compat.java8.FutureConverters.FutureOps import scala.concurrent.{ExecutionContext, Future} @@ -26,8 +26,9 @@ class BakeryExecutorJava(bakery: Bakery) extends LazyLogging { }.recover { case e: BakerException => BakerResult(e) case e: Throwable => - logger.error(s"Unexpected exception happened when calling Baker", e) - BakerResult(e.getMessage) + val errorId = UUID.randomUUID().toString + logger.error(s"Unexpected exception happened when calling Baker (id='$errorId').", e) + BakerResult(BakerException.UnexpectedException(errorId)) }.map(bakerResultEncoder.apply(_).noSpaces) } diff --git a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBaker.scala b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBaker.scala index b4467673d..d8f23c9a4 100644 --- a/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBaker.scala +++ b/core/akka-runtime/src/main/scala/com/ing/baker/runtime/akka/AkkaBaker.scala @@ -14,7 +14,7 @@ import com.ing.baker.runtime.akka.actor.recipe_manager.RecipeManagerProtocol import com.ing.baker.runtime.akka.actor.recipe_manager.RecipeManagerProtocol.RecipeFound import com.ing.baker.runtime.akka.internal.CachingInteractionManager import com.ing.baker.runtime.common.BakerException._ -import com.ing.baker.runtime.common.{BakerException, InteractionExecutionFailureReason, RecipeRecord, SensoryEventStatus} +import com.ing.baker.runtime.common.{InteractionExecutionFailureReason, RecipeRecord, SensoryEventStatus} import com.ing.baker.runtime.recipe_manager.{ActorBasedRecipeManager, DefaultRecipeManager, RecipeManager} import com.ing.baker.runtime.scaladsl._ import com.ing.baker.runtime.{javadsl, scaladsl} @@ -195,7 +195,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker InteractionExecutionResult.Failure(InteractionExecutionFailureReason.TIMEOUT, Some(interactionInstance.name), None)))))( timer = cats.effect.IO.timer(config.system.dispatcher), cs = cats.effect.IO.contextShift(config.system.dispatcher)) } - .unsafeToFuture() + .unsafeToFuture().javaTimeoutToBakerTimeout("executeSingleInteraction") /** * Creates a process instance for the given recipeId with the given RecipeInstanceId as identifier @@ -205,7 +205,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker * @return */ override def bake(recipeId: String, recipeInstanceId: String): Future[Unit] = { - processIndexActor.ask(CreateProcess(recipeId, recipeInstanceId))(config.timeouts.defaultBakeTimeout).flatMap { + processIndexActor.ask(CreateProcess(recipeId, recipeInstanceId))(config.timeouts.defaultBakeTimeout).javaTimeoutToBakerTimeout("bake").flatMap { case _: Initialized => Future.successful(()) case ProcessAlreadyExists(_) => @@ -222,7 +222,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker correlationId = correlationId, timeout = config.timeouts.defaultProcessEventTimeout, reaction = FireSensoryEventReaction.NotifyWhenReceived - ))(config.timeouts.defaultProcessEventTimeout).flatMap { + ))(config.timeouts.defaultProcessEventTimeout).javaTimeoutToBakerTimeout("fireEventAndResolveWhenReceived").flatMap { // TODO MOVE THIS TO A FUNCTION case FireSensoryEventRejection.InvalidEvent(_, message) => Future.failed(IllegalEventException(message)) @@ -247,7 +247,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker correlationId = correlationId, timeout = config.timeouts.defaultProcessEventTimeout, reaction = FireSensoryEventReaction.NotifyWhenCompleted(waitForRetries = true) - ))(config.timeouts.defaultProcessEventTimeout).flatMap { + ))(config.timeouts.defaultProcessEventTimeout).javaTimeoutToBakerTimeout("fireEventAndResolveWhenCompleted").flatMap { case FireSensoryEventRejection.InvalidEvent(_, message) => Future.failed(IllegalEventException(message)) case FireSensoryEventRejection.NoSuchRecipeInstance(recipeInstanceId0) => @@ -271,7 +271,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker correlationId = correlationId, timeout = config.timeouts.defaultProcessEventTimeout, reaction = FireSensoryEventReaction.NotifyOnEvent(waitForRetries = true, onEvent) - ))(config.timeouts.defaultProcessEventTimeout).flatMap { + ))(config.timeouts.defaultProcessEventTimeout).javaTimeoutToBakerTimeout("fireEventAndResolveOnEvent").flatMap { case FireSensoryEventRejection.InvalidEvent(_, message) => Future.failed(IllegalEventException(message)) case FireSensoryEventRejection.NoSuchRecipeInstance(recipeInstanceId0) => @@ -299,7 +299,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker reaction = FireSensoryEventReaction.NotifyBoth( waitForRetries = true, completeReceiver = futureRef.ref) - ))(config.timeouts.defaultProcessEventTimeout).flatMap { + ))(config.timeouts.defaultProcessEventTimeout).javaTimeoutToBakerTimeout("fireEvent").flatMap { case FireSensoryEventRejection.InvalidEvent(_, message) => Future.failed(IllegalEventException(message)) case FireSensoryEventRejection.NoSuchRecipeInstance(recipeInstanceId0) => @@ -316,7 +316,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker Future.successful(status) } val futureCompleted = - futureRef.future.flatMap { + futureRef.future.javaTimeoutToBakerTimeout("fireEvent").flatMap { case FireSensoryEventRejection.InvalidEvent(_, message) => Future.failed(IllegalEventException(message)) case FireSensoryEventRejection.NoSuchRecipeInstance(recipeInstanceId0) => @@ -341,7 +341,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker * @return */ override def retryInteraction(recipeInstanceId: String, interactionName: String): Future[Unit] = { - processIndexActor.ask(RetryBlockedInteraction(recipeInstanceId, interactionName))(config.timeouts.defaultProcessEventTimeout).map(_ => ()) + processIndexActor.ask(RetryBlockedInteraction(recipeInstanceId, interactionName))(config.timeouts.defaultProcessEventTimeout).javaTimeoutToBakerTimeout("retryInteraction").map(_ => ()) } /** @@ -352,7 +352,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker * @return */ override def resolveInteraction(recipeInstanceId: String, interactionName: String, event: EventInstance): Future[Unit] = { - processIndexActor.ask(ResolveBlockedInteraction(recipeInstanceId, interactionName, event))(config.timeouts.defaultProcessEventTimeout).map(_ => ()) + processIndexActor.ask(ResolveBlockedInteraction(recipeInstanceId, interactionName, event))(config.timeouts.defaultProcessEventTimeout).javaTimeoutToBakerTimeout("resolveInteraction").map(_ => ()) } /** @@ -361,7 +361,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker * @return */ override def stopRetryingInteraction(recipeInstanceId: String, interactionName: String): Future[Unit] = { - processIndexActor.ask(StopRetryingInteraction(recipeInstanceId, interactionName))(config.timeouts.defaultProcessEventTimeout).map(_ => ()) + processIndexActor.ask(StopRetryingInteraction(recipeInstanceId, interactionName))(config.timeouts.defaultProcessEventTimeout).javaTimeoutToBakerTimeout("stopRetryingInteraction").map(_ => ()) } /** @@ -375,9 +375,10 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker * @return An index of all processes */ override def getAllRecipeInstancesMetadata: Future[Set[RecipeInstanceMetadata]] = { - Future.successful(config.bakerActorProvider + Future(config.bakerActorProvider .getAllProcessesMetadata(processIndexActor)(system, config.timeouts.defaultInquireTimeout) .map(p => RecipeInstanceMetadata(p.recipeId, p.recipeInstanceId, p.createdDateTime)).toSet) + .javaTimeoutToBakerTimeout("getAllRecipeInstancesMetadata") } /** @@ -389,6 +390,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker override def getRecipeInstanceState(recipeInstanceId: String): Future[RecipeInstanceState] = processIndexActor .ask(GetProcessState(recipeInstanceId))(Timeout.durationToTimeout(config.timeouts.defaultInquireTimeout)) + .javaTimeoutToBakerTimeout("getRecipeInstanceState") .flatMap { case instance: InstanceState => Future.successful(instance.state.asInstanceOf[RecipeInstanceState]) case Uninitialized(id) => Future.failed(NoSuchProcessException(id)) @@ -433,7 +435,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker @throws[NoSuchProcessException]("If the process is not found") override def getVisualState(recipeInstanceId: String, style: RecipeVisualStyle = RecipeVisualStyle.default): Future[String] = { for { - getRecipeResponse <- processIndexActor.ask(GetCompiledRecipe(recipeInstanceId))(config.timeouts.defaultInquireTimeout) + getRecipeResponse <- processIndexActor.ask(GetCompiledRecipe(recipeInstanceId))(config.timeouts.defaultInquireTimeout).javaTimeoutToBakerTimeout("getVisualState") processState <- getRecipeInstanceState(recipeInstanceId) response <- getRecipeResponse match { case RecipeFound(compiledRecipe, _) => @@ -506,6 +508,7 @@ class AkkaBaker private[runtime](config: AkkaBakerConfig) extends scaladsl.Baker * Attempts to gracefully shutdown the baker system. */ @nowarn - override def gracefulShutdown: Future[Unit] = - Future.successful(GracefulShutdown.gracefulShutdownActorSystem(system, config.timeouts.defaultShutdownTimeout)) + override def gracefulShutdown(): Future[Unit] = + Future(GracefulShutdown.gracefulShutdownActorSystem(system, config.timeouts.defaultShutdownTimeout)) + .javaTimeoutToBakerTimeout("gracefulShutdown").map(_ => ()) } diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/BakerException.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/BakerException.scala index 7a55bb20b..4d0729f41 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/BakerException.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/common/BakerException.scala @@ -1,65 +1,96 @@ package com.ing.baker.runtime.common -import scala.annotation.nowarn +import scala.concurrent.{ExecutionContext, Future} import scala.util.{Failure, Success, Try} -@nowarn -sealed abstract class BakerException(val message: String = "An exception occurred at Baker", val enum: Int, val cause: Throwable = null) - extends RuntimeException(message, cause) +sealed abstract class BakerException(val message: String = "An exception occurred at Baker", + val `enum`: Int, + val cause: Throwable = null, + // If the exception is created in a json decoder, instead of at the original place it + // is thrown, disable the stack trace, as will point to the decoder, which is incorrect/misleading. + disableStackTrace: Boolean = false + ) + extends RuntimeException(message, cause, false, !disableStackTrace) object BakerException { // TODO this has to be renamed to NoSuchRecipeInstanceException - case class NoSuchProcessException(recipeInstanceId: String) - extends BakerException(s"Recipe instance $recipeInstanceId does not exist in the index", 1) + case class NoSuchProcessException(recipeInstanceId: String, disableStackTrace: Boolean = false) + extends BakerException(s"Recipe instance $recipeInstanceId does not exist in the index", 1, disableStackTrace = disableStackTrace) // TODO this has to be renamed to RecipeInstanceDeletedException - case class ProcessDeletedException(recipeInstanceId: String) - extends BakerException(s"Recipe instance $recipeInstanceId is deleted", 2) + case class ProcessDeletedException(recipeInstanceId: String, disableStackTrace: Boolean = false) + extends BakerException(s"Recipe instance $recipeInstanceId is deleted", 2, disableStackTrace = disableStackTrace) - case class RecipeValidationException(validationErrors: String) - extends BakerException(validationErrors, 3) + case class RecipeValidationException(validationErrors: String, disableStackTrace: Boolean = false) + extends BakerException(validationErrors, 3, disableStackTrace = disableStackTrace) - case class ImplementationsException(implementationErrors: String) - extends BakerException(implementationErrors, 4) + case class ImplementationsException(implementationErrors: String, disableStackTrace: Boolean = false) + extends BakerException(implementationErrors, 4, disableStackTrace = disableStackTrace) - case class NoSuchRecipeException(recipeId: String) - extends BakerException(s"No recipe found for recipe with id: $recipeId", 5) + case class NoSuchRecipeException(recipeId: String, disableStackTrace: Boolean = false) + extends BakerException(s"No recipe found for recipe with id: $recipeId", 5, disableStackTrace = disableStackTrace) // TODO this has to be renamed to RecipeInstanceAlreadyExistsException - case class ProcessAlreadyExistsException(recipeInstanceId: String) - extends BakerException(s"Process '$recipeInstanceId' already exists.", 6) + case class ProcessAlreadyExistsException(recipeInstanceId: String, disableStackTrace: Boolean = false) + extends BakerException(s"Process '$recipeInstanceId' already exists.", 6, disableStackTrace = disableStackTrace) - case class IllegalEventException(reason: String) - extends BakerException(reason, 7) + case class IllegalEventException(reason: String, disableStackTrace: Boolean = false) + extends BakerException(reason, 7, disableStackTrace = disableStackTrace) - case class SingleInteractionExecutionFailedException(reason: String) - extends BakerException(reason, 8) + case class SingleInteractionExecutionFailedException(reason: String, disableStackTrace: Boolean = false) + extends BakerException(reason, 8, disableStackTrace = disableStackTrace) + + case class UnexpectedException(errorId: String, disableStackTrace: Boolean = false) + extends BakerException(s"Unexpected exception happened. Please look for '$errorId' in the logs.", 9, disableStackTrace = disableStackTrace) + + case class TimeoutException(operationName: String, disableStackTrace: Boolean = false) + extends BakerException(s"'$operationName' duration exceeded timeout", 10, disableStackTrace = disableStackTrace) + + // To be used if a serialized baker exception cannot be deserialized into a specific exception. + // This can happen when a bakery-state version is higher and contains more exceptions than the bakery-client. + case class UnknownBakerException(underlyingErrorMessage: String, disableStackTrace: Boolean = false) + extends BakerException(underlyingErrorMessage, 11, disableStackTrace = disableStackTrace) - @nowarn def encode(bakerException: BakerException): (String, Int) = bakerException match { - case e @ NoSuchProcessException(recipeInstanceId) => (recipeInstanceId, e.enum) - case e @ ProcessDeletedException(recipeInstanceId) => (recipeInstanceId, e.enum) - case e @ RecipeValidationException(validationErrors) => (validationErrors, e.enum) - case e @ ImplementationsException(implementationErrors) => (implementationErrors, e.enum) - case e @ NoSuchRecipeException(recipeId) => (recipeId, e.enum) - case e @ ProcessAlreadyExistsException(recipeInstanceId) => (recipeInstanceId, e.enum) - case e @ IllegalEventException(reason) => (reason, e.enum) - case e @ SingleInteractionExecutionFailedException(reason) => (reason, e.enum) + case e @ NoSuchProcessException(recipeInstanceId, _) => (recipeInstanceId, e.`enum`) + case e @ ProcessDeletedException(recipeInstanceId, _) => (recipeInstanceId, e.`enum`) + case e @ RecipeValidationException(validationErrors, _) => (validationErrors, e.`enum`) + case e @ ImplementationsException(implementationErrors, _) => (implementationErrors, e.`enum`) + case e @ NoSuchRecipeException(recipeId, _) => (recipeId, e.`enum`) + case e @ ProcessAlreadyExistsException(recipeInstanceId, _) => (recipeInstanceId, e.`enum`) + case e @ IllegalEventException(reason, _) => (reason, e.`enum`) + case e @ SingleInteractionExecutionFailedException(reason, _) => (reason, e.`enum`) + case e @ UnexpectedException(errorId, _) => (errorId, e.`enum`) + case e @ TimeoutException(operationName, _) => (operationName, e.`enum`) + case e @ UnknownBakerException(underlyingErrorMessage, _) => (underlyingErrorMessage, e.`enum`) + case be if be.isInstanceOf[BakerException] => (be.message, be.`enum`) } - @nowarn - def decode(message: String, enum: Int): Try[BakerException] = - enum match { - case 1 => Success(NoSuchProcessException(message)) - case 2 => Success(ProcessDeletedException(message)) - case 3 => Success(RecipeValidationException(message)) - case 4 => Success(ImplementationsException(message)) - case 5 => Success(NoSuchRecipeException(message)) - case 6 => Success(ProcessAlreadyExistsException(message)) - case 7 => Success(IllegalEventException(message)) - case 8 => Success(SingleInteractionExecutionFailedException(message)) + def decode(message: String, `enum`: Int): Try[BakerException] = + `enum` match { + case 1 => Success(NoSuchProcessException(message, disableStackTrace = true)) + case 2 => Success(ProcessDeletedException(message, disableStackTrace = true)) + case 3 => Success(RecipeValidationException(message, disableStackTrace = true)) + case 4 => Success(ImplementationsException(message, disableStackTrace = true)) + case 5 => Success(NoSuchRecipeException(message, disableStackTrace = true)) + case 6 => Success(ProcessAlreadyExistsException(message, disableStackTrace = true)) + case 7 => Success(IllegalEventException(message, disableStackTrace = true)) + case 8 => Success(SingleInteractionExecutionFailedException(message, disableStackTrace = true)) + case 9 => Success(UnexpectedException(message, disableStackTrace = true)) + case 10 => Success(TimeoutException(message, disableStackTrace = true)) + case 11 => Success(UnknownBakerException(message, disableStackTrace = true)) case _ => Failure(new IllegalArgumentException(s"No BakerException with enum flag $enum")) } + + def decodeOrUnknownBakerException(message: String, `enum`: Int): BakerException = + decode(message, `enum`).getOrElse(UnknownBakerException(message, disableStackTrace = true)) + + implicit class TimeoutExceptionHelper[A](val f : Future[A]) { + def javaTimeoutToBakerTimeout(operationName: String)(implicit executor: ExecutionContext) : Future[A] = + f.recoverWith { + case _ : java.util.concurrent.TimeoutException => Future.failed(BakerException.TimeoutException(operationName)) + } + } } diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerF.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerF.scala index 65f7c9a09..5be65e2a7 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerF.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/model/BakerF.scala @@ -8,7 +8,7 @@ import com.ing.baker.il.failurestrategy.ExceptionStrategyOutcome import com.ing.baker.il.{RecipeVisualStyle, RecipeVisualizer} import com.ing.baker.runtime.common import com.ing.baker.runtime.common.LanguageDataStructures.ScalaApi -import com.ing.baker.runtime.common.{InteractionExecutionFailureReason, RecipeRecord, SensoryEventStatus} +import com.ing.baker.runtime.common.{BakerException, InteractionExecutionFailureReason, RecipeRecord, SensoryEventStatus} import com.ing.baker.runtime.model.recipeinstance.RecipeInstance import com.ing.baker.runtime.scaladsl.{Baker => DeprecatedBaker, _} import com.ing.baker.types.Value @@ -67,6 +67,10 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override type InteractionExecutionResultType = InteractionExecutionResult + private def javaTimeoutToBakerTimeout[A](operationName: String) : PartialFunction[Throwable, F[A]] = { + case _ : java.util.concurrent.TimeoutException => effect.raiseError(BakerException.TimeoutException(operationName)) + } + /** * Adds a recipe to baker and returns a recipeId for the recipe. * @@ -80,6 +84,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con .recipeManager .addRecipe(recipeRecord.recipe, !recipeRecord.validate || config.allowAddingRecipeWithoutRequiringInstances) .timeout(config.addRecipeTimeout) + .recoverWith(javaTimeoutToBakerTimeout("addRecipe")) /** * Returns the recipe information for the given RecipeId @@ -90,6 +95,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override def getRecipe(recipeId: String): F[RecipeInformation] = components.recipeManager.getRecipe(recipeId) .timeout(config.inquireTimeout) + .recoverWith(javaTimeoutToBakerTimeout("getRecipe")) override def getRecipeVisual(recipeId: String, style: RecipeVisualStyle): F[String] = @@ -104,6 +110,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override def getAllRecipes: F[Map[String, RecipeInformation]] = components.recipeManager.getAllRecipes .timeout(config.inquireTimeout) + .recoverWith(javaTimeoutToBakerTimeout("getAllRecipes")) override def getInteraction(interactionName: String): F[Option[InteractionInstanceDescriptor]] = @@ -135,6 +142,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con } .timeoutTo(config.executeSingleInteractionTimeout, effect.pure(InteractionExecutionResult(Left(InteractionExecutionResult.Failure( InteractionExecutionFailureReason.TIMEOUT, Some(interactionInstance.name), None))))) + .recoverWith(javaTimeoutToBakerTimeout("executeSingleInteraction")) } @@ -153,6 +161,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override def bake(recipeId: String, recipeInstanceId: String): F[Unit] = components.recipeInstanceManager.bake(recipeId, recipeInstanceId, config.recipeInstanceConfig) .timeout(config.bakeTimeout) + .recoverWith(javaTimeoutToBakerTimeout("bake")) /** * Notifies Baker that an event has happened and waits until the event was accepted but not executed by the process. @@ -169,6 +178,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con components.recipeInstanceManager .fireEventAndResolveWhenReceived(recipeInstanceId, event, correlationId) .timeout(config.processEventTimeout) + .recoverWith(javaTimeoutToBakerTimeout("fireEventAndResolveWhenReceived")) /** * Notifies Baker that an event has happened and waits until all the actions which depend on this event are executed. @@ -185,6 +195,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con components.recipeInstanceManager .fireEventAndResolveWhenCompleted(recipeInstanceId, event, correlationId) .timeout(config.processEventTimeout) + .recoverWith(javaTimeoutToBakerTimeout("fireEventAndResolveWhenCompleted")) /** * Notifies Baker that an event has happened and waits until an specific event has executed. @@ -202,6 +213,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con components.recipeInstanceManager .fireEventAndResolveOnEvent(recipeInstanceId, event, onEvent, correlationId) .timeout(config.processEventTimeout) + .recoverWith(javaTimeoutToBakerTimeout("fireEventAndResolveOnEvent")) /** * Notifies Baker that an event has happened and provides 2 async handlers, one for when the event was accepted by @@ -219,9 +231,9 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con val (onReceive, onComplete) = components.recipeInstanceManager.fireEvent(recipeInstanceId, event, correlationId).toIO.unsafeRunSync() new EventResolutionsF[F] { override def resolveWhenReceived: F[SensoryEventStatus] = - onReceive.timeout(config.processEventTimeout) + onReceive.timeout(config.processEventTimeout).recoverWith(javaTimeoutToBakerTimeout("fireEvent")) override def resolveWhenCompleted: F[SensoryEventResult] = - onComplete.timeout(config.processEventTimeout) + onComplete.timeout(config.processEventTimeout).recoverWith(javaTimeoutToBakerTimeout("fireEvent")) } } @@ -262,6 +274,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override def getAllRecipeInstancesMetadata: F[Set[RecipeInstanceMetadata]] = components.recipeInstanceManager.getAllRecipeInstancesMetadata .timeout(config.inquireTimeout) + .recoverWith(javaTimeoutToBakerTimeout("getAllRecipeInstancesMetadata")) /** * Returns the process state. @@ -272,6 +285,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override def getRecipeInstanceState(recipeInstanceId: String): F[RecipeInstanceState] = components.recipeInstanceManager.getRecipeInstanceState(recipeInstanceId) .timeout(config.inquireTimeout) + .recoverWith(javaTimeoutToBakerTimeout("getRecipeInstanceState")) /** * Returns all provided ingredients for a given RecipeInstance id. @@ -309,6 +323,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override def getVisualState(recipeInstanceId: String, style: RecipeVisualStyle = RecipeVisualStyle.default): F[String] = components.recipeInstanceManager.getVisualState(recipeInstanceId, style) .timeout(config.inquireTimeout) + .recoverWith(javaTimeoutToBakerTimeout("getVisualState")) private def doRegisterEventListener(listenerFunction: (RecipeEventMetadata, EventInstance) => Unit, processFilter: String => Boolean): F[Unit] = registerBakerEventListener { @@ -348,6 +363,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override def registerBakerEventListener(listenerFunction: BakerEvent => Unit): F[Unit] = components.eventStream.subscribe(listenerFunction) .timeout(config.inquireTimeout) + .recoverWith(javaTimeoutToBakerTimeout("registerBakerEventListener")) /** * Retries a blocked interaction. @@ -358,6 +374,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con components.recipeInstanceManager.retryBlockedInteraction(recipeInstanceId, interactionName) .flatMap(_.compile.drain) .timeout(config.processEventTimeout) + .recoverWith(javaTimeoutToBakerTimeout("retryInteraction")) /** * Resolves a blocked interaction by specifying it's output. @@ -370,6 +387,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con components.recipeInstanceManager.resolveBlockedInteraction(recipeInstanceId, interactionName, event) .flatMap(_.compile.drain) .timeout(config.processEventTimeout) + .recoverWith(javaTimeoutToBakerTimeout("resolveInteraction")) /** * Stops the retrying of an interaction. @@ -379,6 +397,7 @@ abstract class BakerF[F[_]](implicit components: BakerComponents[F], effect: Con override def stopRetryingInteraction(recipeInstanceId: String, interactionName: String): F[Unit] = components.recipeInstanceManager.stopRetryingInteraction(recipeInstanceId, interactionName) .timeout(config.processEventTimeout) + .recoverWith(javaTimeoutToBakerTimeout("stopRetryingInteraction")) def translate[G[_]](mapK: F ~> G, comapK: G ~> F)(implicit components: BakerComponents[G], effect: ConcurrentEffect[G], timer: Timer[G]): BakerF[G] = new BakerF[G] { diff --git a/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/JsonDecoders.scala b/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/JsonDecoders.scala index c567a1fe9..0a1e032fe 100644 --- a/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/JsonDecoders.scala +++ b/core/baker-interface/src/main/scala/com/ing/baker/runtime/serialization/JsonDecoders.scala @@ -104,8 +104,7 @@ object JsonDecoders extends LazyLogging { for { message <- c.downField("message").as[String] enum <- c.downField("enum").as[Int] - exception <- BakerException.decode(message, `enum`).toEither.left.map(DecodingFailure.fromThrowable(_, List.empty)) - } yield exception + } yield BakerException.decodeOrUnknownBakerException(message, `enum`) } implicit val interactionInstanceInputDecoder: Decoder[InteractionInstanceInput] = deriveDecoder[InteractionInstanceInput]