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

endless.transaction.impl.logic.TransactionSideEffect.scala Maven / Gradle / Ivy

The newest version!
package endless.transaction.impl.logic

import cats.Show
import cats.effect.kernel.{Async, Temporal}
import cats.syntax.applicative.*
import cats.syntax.applicativeError.*
import cats.syntax.flatMap.*
import cats.syntax.functor.*
import cats.syntax.parallel.*
import cats.syntax.show.*
import cats.effect.kernel.implicits.parallelForGenSpawn
import endless.core.entity.SideEffect.Trigger
import endless.core.entity.SideEffect.Trigger.{AfterPersistence, AfterRead, AfterRecovery}
import endless.core.entity.{Effector, SideEffect}
import endless.transaction.Branch
import endless.transaction.Transaction.Status
import endless.transaction.impl.algebra.TransactionAlg
import endless.transaction.impl.data.TransactionState
import endless.transaction.impl.data.TransactionState.*
import endless.transaction.impl.helpers.RetryHelpers.*
import org.typelevel.log4cats.Logger

import scala.concurrent.duration.{Duration, FiniteDuration}

private[transaction] final class TransactionSideEffect[
    F[_]: Temporal: Logger,
    TID,
    BID: Show,
    Q,
    R
](
    timeoutSideEffect: TimeoutSideEffect[F],
    branchForID: BID => Branch[F, TID, Q, R]
)(implicit retryParameters: RetryParameters)
    extends SideEffect[
      F,
      TransactionState[TID, BID, Q, R],
      ({ type T[G[_]] = TransactionAlg[G, TID, BID, Q, R] })#T
    ] {

  def apply(
      trigger: Trigger,
      effector: Effector[
        F,
        TransactionState[TID, BID, Q, R],
        ({ type T[G[_]] = TransactionAlg[G, TID, BID, Q, R] })#T
      ]
  ): F[Unit] = {
    import effector.*

    lazy val branchEffect: F[Unit] = ifKnown {
      case preparing: Preparing[TID, BID, Q, R] if (trigger match {
            case AfterPersistence =>
              preparing.noVotesYet // having at least a vote means we already sent prepares
            case AfterRecovery => true
            case AfterRead     => false
          }) =>
        preparing.branches
          .filterNot(preparing.hasBranchAlreadyVoted)
          .map(zipWithBranch)
          .toList
          .parTraverse(prepareBranch(preparing).tupled)
          .void

      case committing: Committing[TID, BID, Q, R] if (trigger match {
            case AfterPersistence =>
              committing.noCommitsYet // having at least a commit means we already sent commits
            case AfterRecovery => true
            case AfterRead     => false
          }) =>
        committing.branches
          .filterNot(committing.hasBranchAlreadyCommitted)
          .map(zipWithBranch)
          .toList
          .parTraverse(commitBranch(committing).tupled)
          .void

      case aborting: Aborting[TID, BID, Q, R] if (trigger match {
            case AfterPersistence =>
              aborting.noAbortsYet // have at least an abort means we already sent aborts
            case AfterRecovery => true
            case AfterRead     => false
          }) =>
        aborting.branches
          .filterNot(aborting.hasBranchAlreadyAborted)
          .map(zipWithBranch)
          .toList
          .parTraverse(abortBranch(aborting).tupled)
          .void

      case _ => ().pure
    }

    def prepareBranch(
        state: Preparing[TID, BID, Q, R]
    )(branchID: BID, branch: Branch[F, TID, Q, R]) = branch
      .prepare(state.id, state.query)
      .flatMap(vote =>
        self
          .branchVoted(branchID, vote)
          .retryWithBackoff(warnAboutRetry("vote", branchID))
      )
      .handleErrorWith(throwable =>
        self
          .branchFailed(branchID, throwable.getMessage)
          .retryWithBackoff(warnAboutRetry("failure", branchID))
          .handleErrorWith(logDefinitiveFailure(branchID))
      )

    def commitBranch(state: Committing[TID, BID, Q, R])(
        branchID: BID,
        branch: Branch[F, TID, Q, R]
    ) = branch
      .commit(state.id)
      .flatMap(_ =>
        self
          .branchCommitted(branchID)
          .retryWithBackoff(warnAboutRetry("commit", branchID))
      )
      .handleErrorWith(throwable =>
        self
          .branchFailed(branchID, throwable.getMessage)
          .retryWithBackoff(warnAboutRetry("failure", branchID))
          .handleErrorWith(logDefinitiveFailure(branchID))
      )

    def abortBranch(
        state: Aborting[TID, BID, Q, R]
    )(branchID: BID, branch: Branch[F, TID, Q, R]) = {
      branch
        .abort(state.id)
        .flatMap(_ =>
          self
            .branchAborted(branchID)
            .retryWithBackoff(warnAboutRetry("abort", branchID))
        )
        .handleErrorWith(throwable =>
          self
            .branchFailed(branchID, throwable.getMessage)
            .retryWithBackoff(warnAboutRetry("failure", branchID))
            .handleErrorWith(logDefinitiveFailure(branchID))
        )
    }

    lazy val timeoutEffect =
      ifKnown(state => timeoutSideEffect.scheduleTimeoutAccordingTo(state.status))

    lazy val passivationEffect = ifKnown(state =>
      state.status match {
        case _: Status.Pending[R] => disablePassivation
        case _: Status.Final[R]   => enablePassivation()
      }
    )

    trigger match {
      case Trigger.AfterPersistence | Trigger.AfterRecovery =>
        branchEffect >> timeoutEffect >> passivationEffect
      case Trigger.AfterRead => passivationEffect
    }
  }

  private def logDefinitiveFailure(branchID: BID)(throwable: Throwable) = {
    Logger[F].error(throwable)(
      show"Definitive failure to notify transaction about failure for branch $branchID"
    )
  }

  private def warnAboutRetry(operation: String, branchID: BID): OnError[F] = (
      error: Throwable,
      delay: Duration,
      attemptNumber: Int,
      maxRetries: Int
  ) =>
    Logger[F].warn(error)(
      show"Failed to notify transaction about $operation for branch $branchID, retrying in $delay (attempt #$attemptNumber out of $maxRetries)"
    )

  private def zipWithBranch(branchID: BID) = branchID -> branchForID(branchID)
}

private[transaction] object TransactionSideEffect {
  def apply[F[_]: Async: Logger, TID, BID: Show, Q, R](
      timeout: Option[FiniteDuration],
      branchForID: BID => Branch[F, TID, Q, R],
      alg: TransactionAlg[F, TID, BID, Q, R]
  )(implicit retryParameters: RetryParameters): F[SideEffect[
    F,
    TransactionState[TID, BID, Q, R],
    ({ type T[G[_]] = TransactionAlg[G, TID, BID, Q, R] })#T
  ]] =
    TimeoutSideEffect[F](timeout, alg.timeout()).map(timeoutEffect =>
      new TransactionSideEffect[F, TID, BID, Q, R](timeoutEffect, branchForID)
    )

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy