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

spekka.stateful.StatefulFlowBackend.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2022 Andrea Zito
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package spekka.stateful

import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors

import java.util.concurrent.TimeoutException
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration
import scala.reflect.ClassTag
import scala.util.Failure
import scala.util.Success
import scala.util.Try

/** A [[StatefulFlowBackend]] is responsible of managing the persistence of the state of a stateful
  * flow.
  *
  * It is not concerned to how/when the state is changed (which is controlled by the
  * [[StatefulFlowLogic]]), but is the ultimate component in charge of making sure that the state is
  * actually persisted and recovered.
  *
  * The currently supported stateful flow backends are:
  *
  *   - [[StatefulFlowBackend.EventBased]]
  *   - [[StatefulFlowBackend.DurableState]]
  */
sealed trait StatefulFlowBackend {

  /** Unique identifier of the backend implementation
    */
  val id: String
}

/** Namespace object for stateful flow backends
  */
object StatefulFlowBackend {

  /** A [[StatefulFlowBackend]] for [[StatefulFlowLogic.EventBased]] logics.
    *
    * The state is defined by the series of event that generated it. The backend is responsible of
    * persisting the individual events and recompute the state by re-applying the persisted events
    * starting from the initial empty state.
    *
    * @tparam State
    *   the type of state managed by the backend
    * @tparam Ev
    *   the type of event managed by the backend
    * @tparam BackendProtocol
    *   the internal protocol of the backend
    */
  trait EventBased[State, Ev, BackendProtocol] extends StatefulFlowBackend {

    /** The logic type compatible with this backend
      */
    type Logic[In, Command] = StatefulFlowLogic.EventBased[State, Ev, In, Command]

    private[spekka] def behaviorFor[In, Command](
        logic: StatefulFlowLogic.EventBased[State, Ev, In, Command],
        entityKind: String,
        entityId: String
      ): Behavior[StatefulFlowHandler.Protocol[In, Ev, Command, BackendProtocol]]

    /** Creates a [[StatefulFlowProps]] for this backend with the specified logic.
      *
      * @param logic
      *   An [[StatefulFlowLogic.EventBased]] logic to use together with this backend to create a
      *   stateful flow
      * @return
      *   [[StatefulFlowProps]] for this backend and the specified logic
      */
    def propsForLogic[In, Command](
        logic: Logic[In, Command]
      ): StatefulFlowProps[In, Ev, Command] =
      new StatefulFlowProps.EventBased[State, Ev, In, Command, BackendProtocol](logic, this)
  }

  /** A [[StatefulFlowBackend]] for [[StatefulFlowLogic.EventBasedAsync]] logics.
    *
    * The state is defined by the series of event that generated it. The backend is responsible of
    * persisting the individual events and recompute the state by re-applying the persisted events
    * starting from the initial empty state.
    *
    * @tparam State
    *   the type of state managed by the backend
    * @tparam Ev
    *   the type of event managed by the backend
    * @tparam BackendProtocol
    *   the internal protocol of the backend
    */
  trait EventBasedAsync[State, Ev, BackendProtocol] extends StatefulFlowBackend {

    /** The logic type compatible with this backend
      */
    type Logic[In, Command] = StatefulFlowLogic.EventBasedAsync[State, Ev, In, Command]

    private[spekka] def behaviorFor[In, Command](
        logic: StatefulFlowLogic.EventBasedAsync[State, Ev, In, Command],
        entityKind: String,
        entityId: String
      ): Behavior[StatefulFlowHandler.Protocol[In, Ev, Command, BackendProtocol]]

    /** Creates a [[StatefulFlowProps]] for this backend with the specified logic.
      *
      * @param logic
      *   An [[StatefulFlowLogic.EventBasedAsync]] logic to use together with this backend to create
      *   a stateful flow
      * @return
      *   [[StatefulFlowProps]] for this backend and the specified logic
      */
    def propsForLogic[In, Command](
        logic: Logic[In, Command]
      ): StatefulFlowProps[In, Ev, Command] =
      new StatefulFlowProps.EventBasedAsync[State, Ev, In, Command, BackendProtocol](logic, this)
  }

  /** A [[StatefulFlowBackend]] for [[StatefulFlowLogic.DurableState]] logics.
    *
    * @tparam State
    *   the type of state managed by the backend
    * @tparam BackendProtocol
    *   the internal protocol of the backend
    */
  trait DurableState[State, BackendProtocol] extends StatefulFlowBackend {

    /** The logic type compatible with this backend
      */
    type Logic[In, Out, Command] = StatefulFlowLogic.DurableState[State, In, Out, Command]

    private[spekka] def behaviorFor[In, Out, Command](
        logic: StatefulFlowLogic.DurableState[State, In, Out, Command],
        entityKind: String,
        entityId: String
      ): Behavior[StatefulFlowHandler.Protocol[In, Out, Command, BackendProtocol]]

    /** Creates a [[StatefulFlowProps]] for this backend with the specified logic.
      *
      * @param logic
      *   An [[StatefulFlowLogic.EventBased]] logic to use together with this backend to create a
      *   stateful flow
      * @return
      *   [[StatefulFlowProps]] for this backend and the specified logic
      */
    def propsForLogic[In, Out, Command](
        logic: Logic[In, Out, Command]
      ): StatefulFlowProps[In, Out, Command] =
      new StatefulFlowProps.DurableState[State, In, Out, Command, BackendProtocol](logic, this)
  }

  /** A [[StatefulFlowBackend]] for [[StatefulFlowLogic.DurableStateAsync]] logics.
    *
    * @tparam State
    *   the type of state managed by the backend
    * @tparam BackendProtocol
    *   the internal protocol of the backend
    */
  trait DurableStateAsync[State, BackendProtocol] extends StatefulFlowBackend {

    /** The logic type compatible with this backend
      */
    type Logic[In, Out, Command] = StatefulFlowLogic.DurableStateAsync[State, In, Out, Command]

    private[spekka] def behaviorFor[In, Out, Command](
        logic: StatefulFlowLogic.DurableStateAsync[State, In, Out, Command],
        entityKind: String,
        entityId: String
      ): Behavior[StatefulFlowHandler.Protocol[In, Out, Command, BackendProtocol]]

    /** Creates a [[StatefulFlowProps]] for this backend with the specified logic.
      *
      * @param logic
      *   An [[StatefulFlowLogic.EventBasedAsync]] logic to use together with this backend to create
      *   a stateful flow
      * @return
      *   [[StatefulFlowProps]] for this backend and the specified logic
      */
    def propsForLogic[In, Out, Command](
        logic: Logic[In, Out, Command]
      ): StatefulFlowProps[In, Out, Command] =
      new StatefulFlowProps.DurableStateAsync[State, In, Out, Command, BackendProtocol](logic, this)
  }

  private[spekka] object SideEffectHandlingBehavior {
    trait ProtocolAdapter[P <: StatefulFlowHandler.BackendProtocol[P]] {
      def buildSideEffectCompleteMessage: P
      def extractSideEffectComplete: PartialFunction[StatefulFlowHandler.Protocol[_, _, _, P], Unit]
      def buildSideEffectFailureMessage(error: Throwable): P
      def extractSideEffectFailure
          : PartialFunction[StatefulFlowHandler.Protocol[_, _, _, P], Throwable]
    }

    private[spekka] def apply[
        State,
        In,
        Out,
        Command,
        Protocol <: StatefulFlowHandler.BackendProtocol[Protocol]
      ](self: ActorRef[StatefulFlowHandler.Protocol[In, Out, Command, Protocol]],
        beforeUpdateSideEffects: Seq[() => Future[_]],
        beforeUpdateSideEffectsFailure: Throwable => Behavior[
          StatefulFlowHandler.Protocol[In, Out, Command, Protocol]
        ],
        afterUpdateSideEffects: Seq[() => Future[_]],
        afterUpdateSideEffectsFailure: Throwable => Behavior[
          StatefulFlowHandler.Protocol[In, Out, Command, Protocol]
        ],
        success: () => Behavior[
          StatefulFlowHandler.Protocol[In, Out, Command, Protocol]
        ],
        stashBufferSize: Int
      )(implicit ex: ExecutionContext,
        adapter: ProtocolAdapter[Protocol]
      ): Behavior[StatefulFlowHandler.Protocol[In, Out, Command, Protocol]] = {

      def handleBeforeSideEffect() = {
        Future.sequence(beforeUpdateSideEffects.map(_.apply())).onComplete {
          case Success(_) => self ! adapter.buildSideEffectCompleteMessage
          case Failure(ex) => self ! adapter.buildSideEffectFailureMessage(ex)
        }
        waitForBeforeSideEffect
      }

      def handleAfterSideEffect() = {
        Future.sequence(afterUpdateSideEffects.map(_.apply())).onComplete {
          case Success(_) => self ! adapter.buildSideEffectCompleteMessage
          case Failure(ex) => self ! adapter.buildSideEffectFailureMessage(ex)
        }
        waitForAfterSideEffect
      }

      def waitForBeforeSideEffect
          : Behavior[StatefulFlowHandler.Protocol[In, Out, Command, Protocol]] =
        Behaviors.withStash(stashBufferSize) { buffer =>
          Behaviors.receiveMessage { msg =>
            adapter.extractSideEffectComplete
              .andThen { _ =>
                if (afterUpdateSideEffects.nonEmpty) handleAfterSideEffect()
                else buffer.unstashAll(success())
              }
              .orElse {
                adapter.extractSideEffectFailure.andThen { ex =>
                  buffer.unstashAll(beforeUpdateSideEffectsFailure(ex))
                }
              }
              .applyOrElse(
                msg,
                { m: StatefulFlowHandler.Protocol[In, Out, Command, Protocol] =>
                  buffer.stash(m)
                  Behaviors.same
                }
              )
          }
        }

      def waitForAfterSideEffect
          : Behavior[StatefulFlowHandler.Protocol[In, Out, Command, Protocol]] =
        Behaviors.withStash(stashBufferSize) { buffer =>
          Behaviors.receiveMessage { msg =>
            adapter.extractSideEffectComplete
              .andThen { _ =>
                buffer.unstashAll(success())
              }
              .orElse {
                adapter.extractSideEffectFailure.andThen { ex =>
                  buffer.unstashAll(afterUpdateSideEffectsFailure(ex))
                }
              }
              .applyOrElse(
                msg,
                { m: StatefulFlowHandler.Protocol[In, Out, Command, Protocol] =>
                  buffer.stash(m)
                  Behaviors.same
                }
              )
          }
        }

      if (beforeUpdateSideEffects.nonEmpty) handleBeforeSideEffect()
      else if (afterUpdateSideEffects.nonEmpty) handleAfterSideEffect()
      else success()
    }
  }

  private[spekka] object FutureBehavior {

    case class FutureReady[V](result: Try[V])

    def apply[T: ClassTag, V](
        timeout: FiniteDuration,
        future: Future[V]
      )(f: V => Behavior[T]
      ): Behavior[T] = {
      Behaviors
        .withStash[Any](Integer.MAX_VALUE) { stash =>
          Behaviors.setup[Any] { ctx =>
            val behaviorWithTimeoutF = Future.firstCompletedOf(
              List(
                future,
                akka.pattern.after(timeout)(
                  Future.failed(
                    new TimeoutException(
                      s"Timeout while waiting for behavior initialization: ${timeout}"
                    )
                  )
                )(ctx.system)
              )
            )(ctx.executionContext)

            ctx.pipeToSelf(behaviorWithTimeoutF)(FutureReady.apply)

            Behaviors.receiveMessage[Any] {
              case data: T =>
                stash.stash(data)
                Behaviors.same

              case FutureReady(valueT: Try[V @unchecked]) =>
                valueT match {
                  case Success(v) => stash.unstashAll(f(v).asInstanceOf[Behavior[Any]])
                  case Failure(ex) => throw ex
                }

              case _ =>
                Behaviors.same
            }
          }
        }
        .narrow[T]
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy