All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ing.baker.runtime.model.recipeinstance.RecipeInstance.scala Maven / Gradle / Ivy

The newest version!
package com.ing.baker.runtime.model.recipeinstance

import cats.data.EitherT
import cats.effect.concurrent.Ref
import cats.effect.{ConcurrentEffect, Effect, IO, Sync, Timer}
import cats.implicits._
import com.ing.baker.il.CompiledRecipe
import com.ing.baker.il.failurestrategy.ExceptionStrategyOutcome
import com.ing.baker.runtime.model.{BakerComponents, FireSensoryEventRejection}
import com.ing.baker.runtime.model.recipeinstance.RecipeInstance.FatalInteractionException
import com.ing.baker.runtime.scaladsl.{EventFired, EventInstance, EventReceived, EventRejected, RecipeInstanceCreated}
import com.typesafe.scalalogging.LazyLogging
import fs2.Stream

import scala.collection.immutable.Seq
import scala.concurrent.duration._

object RecipeInstance {

  case class Config(idleTTL: Option[FiniteDuration] = Some(5.seconds),
                    ingredientsFilter: Seq[String] = Seq.empty)

  def empty[F[_]](recipe: CompiledRecipe, recipeInstanceId: String, settings: Config)(implicit components: BakerComponents[F], effect: Effect[F], timer: Timer[F]): F[RecipeInstance[F]] =
    for {
      timestamp <- timer.clock.realTime(MILLISECONDS)
      state <- Ref.of[F, RecipeInstanceState](RecipeInstanceState.empty(recipeInstanceId, recipe, timestamp))
      recipeInstanceCreated = RecipeInstanceCreated(timestamp, recipe.recipeId, recipe.name, recipeInstanceId)
      _ <- effect.delay(components.logging.recipeInstanceCreated(recipeInstanceCreated))
      _ <- components.eventStream.publish(recipeInstanceCreated)
    } yield RecipeInstance(recipeInstanceId, settings, state)

  class FatalInteractionException(message: String, cause: Throwable = null) extends RuntimeException(message, cause)
}

case class RecipeInstance[F[_]](recipeInstanceId: String, config: RecipeInstance.Config, state: Ref[F, RecipeInstanceState]) extends LazyLogging {

  def fireEventStream(input: EventInstance, correlationId: Option[String])(implicit components: BakerComponents[F], effect: ConcurrentEffect[F], timer: Timer[F]): EitherT[F, FireSensoryEventRejection, Stream[F, EventInstance]] =
    for {
      currentTime <- EitherT.liftF(timer.clock.realTime(MILLISECONDS))
      currentState <- EitherT.liftF(state.get)
      initialExecution <- EitherT.fromEither[F](currentState.validateExecution(input, correlationId, currentTime))
        .leftSemiflatMap { case (rejection, reason)  =>
          for {
            eventRejected <- effect.delay(EventRejected(currentTime, recipeInstanceId, correlationId, input.name, rejection.asReason))
            _ <- effect.delay(components.logging.eventRejected(eventRejected))
            _ <- components.eventStream.publish(eventRejected)
          } yield rejection
        }
      _ <- EitherT.liftF(components.eventStream.publish(EventReceived(currentTime, currentState.recipe.name, currentState.recipe.recipeId, recipeInstanceId, correlationId, input.name)))
    } yield baseCase(initialExecution)
      .collect { case Some(output) => output.filterNot(config.ingredientsFilter) }

  def stopRetryingInteraction(interactionName: String)(implicit components: BakerComponents[F], effect: ConcurrentEffect[F], timer: Timer[F]): F[Unit] =
    for {
      transitionExecution <- getInteractionTransitionExecution(interactionName)
      _ <- state.update(_
        .removeRetryingExecution(transitionExecution.id)
        .recordFailedExecution(transitionExecution, ExceptionStrategyOutcome.BlockTransition))
    } yield ()

  def retryBlockedInteraction(interactionName: String)(implicit components: BakerComponents[F], effect: ConcurrentEffect[F], timer: Timer[F]): Stream[F, EventInstance] =
    Stream.force {
      for {
        transitionExecution <- getInteractionTransitionExecution(interactionName)
      } yield inductionStep(transitionExecution, Left(ExceptionStrategyOutcome.RetryWithDelay(0)))
    }
      .collect { case Some(output) => output }

  def resolveBlockedInteraction(interactionName: String, eventInstance: EventInstance)(implicit components: BakerComponents[F], effect: ConcurrentEffect[F], timer: Timer[F]): Stream[F, EventInstance] =
    Stream.force {
      for {
        transitionExecution <- getInteractionTransitionExecution(interactionName)
        newOutcome <- transitionExecution.validateEventForResolvingBlockedInteraction(eventInstance)
      } yield inductionStep(transitionExecution, Right(Some(newOutcome)))
    }
      .collect { case Some(output) => output }

  /** The "base case" is the very 1st step in the stream of executing transitions that create EventInstances  */
  private def baseCase(transitionExecution: TransitionExecution)(implicit components: BakerComponents[F], effect: ConcurrentEffect[F], timer: Timer[F]): Stream[F, Option[EventInstance]] =
    for {
      _ <- Stream.eval(state.update(_.addExecution(transitionExecution)))
      outcome <- Stream.eval(transitionExecution.execute)
      output <- inductionStep(transitionExecution, outcome)
    } yield output

  /** The "induction step" is the "repeating" 2nd, 3rd... nth step in the stream of executing transitions that create
    * EventInstances, notice the recursion when there exist enabled transitions, which are outcome of executing this step
    */
  private def inductionStep(finishedExecution: TransitionExecution, outcome: TransitionExecution.Outcome)(implicit components: BakerComponents[F], effect: ConcurrentEffect[F], timer: Timer[F]): Stream[F, Option[EventInstance]] =
    for {
      outputAndEnabledExecutions <- Stream.eval(handleExecutionOutcome(finishedExecution)(outcome))
      (first, enabledExecutions) = outputAndEnabledExecutions
      next <- enabledExecutions.foldLeft(Stream.emit(first).covary[F]) { (stream, enabled) =>
        stream merge Stream.force(
          enabled.execute.map(inductionStep(enabled, _)))
      }
    } yield next

  private def handleExecutionOutcome(finishedExecution: TransitionExecution)(outcome: TransitionExecution.Outcome)(implicit components: BakerComponents[F], effect: ConcurrentEffect[F], timer: Timer[F]): F[(Option[EventInstance], Set[TransitionExecution])] =
    outcome match {

      case Right(output) =>
        for {
          enabledExecutions <- state.modify(_.recordCompletedExecution(finishedExecution, output))
          _ <- scheduleIdleStop
        } yield output -> enabledExecutions

      case Left(ExceptionStrategyOutcome.Continue(eventName)) =>
        val output: EventInstance = EventInstance(eventName, Map.empty)
        for {
          enabledExecutions <- state.modify(_.recordFailedWithOutputExecution(finishedExecution, Some(output)))
          _ <- scheduleIdleStop
        } yield Some(output) -> enabledExecutions

      case Left(strategy @ ExceptionStrategyOutcome.BlockTransition) =>
        state
          .update(_.recordFailedExecution(finishedExecution, strategy))
          .as(None -> Set.empty[TransitionExecution])

      case Left(strategy @ ExceptionStrategyOutcome.RetryWithDelay(delay)) =>
        for {
          _ <- state.update(_
            .recordFailedExecution(finishedExecution, strategy)
            .addRetryingExecution(finishedExecution.id))
          _ <- effect.delay(components.logging.scheduleRetry(recipeInstanceId, finishedExecution.transition, delay))
          finalOutcome <- timer.sleep(delay.milliseconds) *> state.get.flatMap { currentState =>
            if (currentState.retryingExecutions.contains(finishedExecution.id)) {
              val currentTransitionExecution = currentState.executions(finishedExecution.id)
              currentTransitionExecution
                .execute
                .flatMap(handleExecutionOutcome(currentTransitionExecution))
                .flatTap(_ => state.update(_.removeRetryingExecution(finishedExecution.id)))
            } else
              effect.pure[(Option[EventInstance], Set[TransitionExecution])](None -> Set.empty)
          }
        } yield finalOutcome
    }

  private def scheduleIdleStop(implicit components: BakerComponents[F], effect: Effect[F], timer: Timer[F]): F[Unit] = {

    def schedule: F[Unit] =
      state.get.flatMap { currentState =>
        config.idleTTL match {
          case Some(idleTTL) if currentState.isInactive =>
            timer.sleep(idleTTL) *> confirmIdleStop(currentState.sequenceNumber, idleTTL)
          case _ => effect.unit
        }
      }

    def confirmIdleStop(sequenceNumber: Long, originalIdleTTL: FiniteDuration): F[Unit] =
      state.get.flatMap { currentState =>
        if (currentState.isInactive && currentState.sequenceNumber == sequenceNumber)
          components.recipeInstanceManager.idleStop(recipeInstanceId) *>
            effect.delay(components.logging.idleStop(recipeInstanceId, originalIdleTTL))
        else effect.unit
      }

    effect.runAsync(schedule)(_ => IO.unit).to[F]
  }

  private def getInteractionTransitionExecution(interactionName: String)(implicit effect: Sync[F]): F[TransitionExecution] =
    state.get.flatMap(_.getInteractionExecution(interactionName) match {
      case None =>
        effect.raiseError(new FatalInteractionException(s"No interaction with name $interactionName within instance state with id $recipeInstanceId"))
      case Some(interactionExecution) =>
        effect.pure(interactionExecution)
    })
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy