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

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

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

import cats.data.NonEmptyList
import cats.syntax.either.*
import com.google.protobuf.ByteString
import endless.\/
import endless.core.protocol.{CommandSender, Decoder, Encoder, IncomingCommand}
import endless.protobuf.{ProtobufCommandProtocol, ProtobufDecoder}
import endless.transaction.Transaction.{
  AbortError,
  AbortReason,
  TooLateToAbort,
  TransactionFailed,
  Unknown
}
import endless.transaction.impl.algebra.TransactionAlg
import endless.transaction.impl.algebra.TransactionCreator.AlreadyExists
import endless.transaction.impl.logic.TransactionProtocol.{
  UnexpectedCommandException,
  UnexpectedReplyException
}
import endless.transaction.proto.commands.TransactionCommand.Command
import endless.transaction.proto.commands.*
import endless.transaction.proto.replies.*
import endless.transaction.proto.model
import endless.transaction.{BinaryCodec, Branch, Transaction, proto}

private[transaction] class TransactionProtocol[TID, BID, Q, R](implicit
    tidCodec: BinaryCodec[TID],
    bidCodec: BinaryCodec[BID],
    qCodec: BinaryCodec[Q],
    rCodec: BinaryCodec[R]
) extends ProtobufCommandProtocol[TID, ({ type T[G[_]] = TransactionAlg[G, TID, BID, Q, R] })#T] {

  def server[F[_]]
      : Decoder[IncomingCommand[F, ({ type T[G[_]] = TransactionAlg[G, TID, BID, Q, R] })#T]] =
    ProtobufDecoder[TransactionCommand].map(_.command match {
      case Command.Empty => throw new UnexpectedCommandException
      case Command.Create(CreateCommand(id, query, branches, _)) =>
        handleCommand[F, CreateReply, AlreadyExists.type \/ Unit](
          _.create(
            decodeTransactionID(id),
            decodeFromByteString(query),
            NonEmptyList.fromListUnsafe(branches.map(decodeBranchID).toList)
          ),
          {
            case Left(AlreadyExists) =>
              CreateReply(CreateReply.Reply.AlreadyExists(AlreadyExistsReply()))
            case Right(()) => CreateReply(CreateReply.Reply.Unit(UnitReply()))
          }
        )
      case Command.GetQuery(_) =>
        handleCommand[F, QueryReply, Unknown.type \/ Q](
          _.query,
          {
            case Left(Unknown) => QueryReply(QueryReply.Reply.Unknown(UnknownReply()))
            case Right(query)  => QueryReply(QueryReply.Reply.Query(encodeToByteString(query)))
          }
        )
      case Command.GetBranches(_) =>
        handleCommand[F, BranchesReply, Unknown.type \/ Set[BID]](
          _.branches,
          {
            case Left(Unknown) => BranchesReply(BranchesReply.Reply.Unknown(UnknownReply()))
            case Right(branches) =>
              BranchesReply(
                BranchesReply.Reply.Branches(
                  model.Branches(
                    branches.map(encodeBranchID).toSeq
                  )
                )
              )
          }
        )
      case Command.GetStatus(_) =>
        handleCommand[F, StatusReply, Unknown.type \/ Transaction.Status[R]](
          _.status,
          {
            case Left(Unknown) => StatusReply(StatusReply.Reply.Unknown(UnknownReply()))
            case Right(status) =>
              StatusReply(
                StatusReply.Reply.Status(
                  model.Status(
                    status match {
                      case Transaction.Status.Preparing =>
                        model.Status.Status.Preparing(model.Preparing())
                      case Transaction.Status.Committing =>
                        model.Status.Status.Committing(model.Committing())
                      case Transaction.Status.Committed =>
                        model.Status.Status.Committed(model.Committed())
                      case Transaction.Status.Failed(errors) =>
                        model.Status.Status.Failed(model.Failed(errors.toList))
                      case Transaction.Status.Aborting(reason) =>
                        model.Status.Status.Aborting(model.Aborting(encodeAbortReason(reason)))
                      case Transaction.Status.Aborted(reason) =>
                        model.Status.Status.Aborted(model.Aborted(encodeAbortReason(reason)))
                    }
                  )
                )
              )
          }
        )
      case Command.Abort(AbortCommand(reason, _)) =>
        handleCommand[F, AbortReply, AbortError \/ Unit](
          _.abort(Option.unless(reason.isEmpty)(decodeFromByteString[R](reason))),
          {
            case Left(TooLateToAbort) =>
              AbortReply(
                AbortReply.Reply.Error(
                  model.AbortError(model.AbortError.Error.TooLateToAbort(model.TooLateToAbort()))
                )
              )
            case Left(TransactionFailed) =>
              AbortReply(
                AbortReply.Reply.Error(
                  model.AbortError(
                    model.AbortError.Error.TransactionFailed(model.TransactionFailed())
                  )
                )
              )
            case Left(Unknown) =>
              AbortReply(
                AbortReply.Reply.Error(
                  model.AbortError(model.AbortError.Error.Unknown(model.Unknown()))
                )
              )
            case Right(()) => AbortReply(AbortReply.Reply.Unit(UnitReply()))
          }
        )
      case Command.BranchVoted(BranchVotedCommand(branchID, vote, _)) =>
        handleCommand[F, UnitReply, Unit](
          _.branchVoted(
            decodeBranchID(branchID),
            vote.vote match {
              case model.Vote.Vote.Commit(_) => Branch.Vote.Commit
              case model.Vote.Vote.Abort(model.Abort(reason, _)) =>
                Branch.Vote.Abort(decodeFromByteString(reason))
              case _ => throw new UnexpectedCommandException
            }
          ),
          _ => UnitReply()
        )
      case Command.BranchCommitted(BranchCommittedCommand(branchId, _)) =>
        handleCommand[F, UnitReply, Unit](
          _.branchCommitted(decodeBranchID(branchId)),
          _ => UnitReply()
        )
      case Command.BranchAborted(BranchAbortedCommand(branchId, _)) =>
        handleCommand[F, UnitReply, Unit](
          _.branchAborted(decodeBranchID(branchId)),
          _ => UnitReply()
        )
      case Command.BranchFailed(BranchFailedCommand(branchId, error, _)) =>
        handleCommand[F, UnitReply, Unit](
          _.branchFailed(decodeBranchID(branchId), error),
          _ => UnitReply()
        )
      case Command.TransactionTimeout(_) =>
        handleCommand[F, UnitReply, Unit](_.timeout(), _ => UnitReply())
    })

  def clientFor[F[_]](id: TID)(implicit
      sender: CommandSender[F, TID]
  ): TransactionAlg[F, TID, BID, Q, R] = new TransactionAlg[F, TID, BID, Q, R] {
    def create(id: TID, query: Q, branches: NonEmptyList[BID]): F[AlreadyExists.type \/ Unit] =
      sendCommand[F, TransactionCommand, CreateReply, AlreadyExists.type \/ Unit](
        id,
        TransactionCommand.of(
          Command.Create(
            CreateCommand(
              encodeTransactionID(id),
              encodeToByteString(query),
              branches.map(encodeBranchID).toList
            )
          )
        ),
        {
          case CreateReply(CreateReply.Reply.AlreadyExists(_), _) =>
            AlreadyExists.asLeft
          case CreateReply(CreateReply.Reply.Unit(_), _) =>
            ().asRight
          case CreateReply(CreateReply.Reply.Empty, _) =>
            throw new UnexpectedReplyException
        }
      )

    def query: F[Unknown.type \/ Q] =
      sendCommand[F, TransactionCommand, QueryReply, Unknown.type \/ Q](
        id,
        TransactionCommand.of(Command.GetQuery(GetQueryCommand())),
        {
          case QueryReply(QueryReply.Reply.Unknown(_), _) => Unknown.asLeft
          case QueryReply(QueryReply.Reply.Query(query), _) =>
            decodeFromByteString[Q](query).asRight
          case QueryReply(QueryReply.Reply.Empty, _) =>
            throw new UnexpectedReplyException
        }
      )

    def branches: F[Unknown.type \/ Set[BID]] =
      sendCommand[F, TransactionCommand, BranchesReply, Unknown.type \/ Set[BID]](
        id,
        TransactionCommand.of(Command.GetBranches(GetBranchesCommand())),
        {
          case BranchesReply(BranchesReply.Reply.Unknown(_), _) =>
            Unknown.asLeft
          case BranchesReply(BranchesReply.Reply.Branches(branches), _) =>
            branches.branches.map(decodeBranchID).toSet.asRight
          case BranchesReply(BranchesReply.Reply.Empty, _) =>
            throw new UnexpectedReplyException
        }
      )

    def status: F[Unknown.type \/ Transaction.Status[R]] =
      sendCommand[F, TransactionCommand, StatusReply, Unknown.type \/ Transaction.Status[
        R
      ]](
        id,
        TransactionCommand.of(Command.GetStatus(GetStatusCommand())),
        {
          case StatusReply(StatusReply.Reply.Unknown(_), _) =>
            Unknown.asLeft
          case StatusReply(StatusReply.Reply.Status(status), _) =>
            status.status match {
              case model.Status.Status.Preparing(_) =>
                Transaction.Status.Preparing.asRight
              case model.Status.Status.Committing(_) =>
                Transaction.Status.Committing.asRight
              case model.Status.Status.Committed(_) =>
                Transaction.Status.Committed.asRight
              case model.Status.Status.Failed(model.Failed(errors, _)) =>
                Transaction.Status.Failed(NonEmptyList.fromListUnsafe(errors.toList)).asRight
              case model.Status.Status.Aborting(model.Aborting(reason, _)) =>
                Transaction.Status.Aborting(decodeAbortReason(reason)).asRight
              case model.Status.Status.Aborted(model.Aborted(reason, _)) =>
                Transaction.Status.Aborted(decodeAbortReason(reason)).asRight
              case _ => throw new UnexpectedCommandException
            }
          case StatusReply(StatusReply.Reply.Empty, _) =>
            throw new UnexpectedReplyException
        }
      )

    def abort(reason: Option[R]): F[AbortError \/ Unit] =
      sendCommand[F, TransactionCommand, AbortReply, AbortError \/ Unit](
        id,
        TransactionCommand.of(
          Command.Abort(AbortCommand(reason.map(encodeToByteString[R]).getOrElse(ByteString.EMPTY)))
        ),
        {
          case AbortReply(AbortReply.Reply.Error(error), _) =>
            error.error match {
              case model.AbortError.Error.TooLateToAbort(_) =>
                TooLateToAbort.asLeft
              case model.AbortError.Error.TransactionFailed(_) =>
                TransactionFailed.asLeft
              case model.AbortError.Error.Unknown(_) =>
                Unknown.asLeft
              case model.AbortError.Error.Empty => throw new UnexpectedReplyException
            }
          case AbortReply(AbortReply.Reply.Unit(_), _) =>
            ().asRight
          case AbortReply(AbortReply.Reply.Empty, _) =>
            throw new UnexpectedReplyException
        }
      )

    def branchVoted(branch: BID, vote: Branch.Vote[R]): F[Unit] =
      sendCommand[F, TransactionCommand, UnitReply, Unit](
        id,
        TransactionCommand.of(
          Command.BranchVoted(
            BranchVotedCommand(
              encodeBranchID(branch),
              model.Vote(vote match {
                case Branch.Vote.Commit => model.Vote.Vote.Commit(model.Commit())
                case Branch.Vote.Abort(reason) =>
                  model.Vote.Vote.Abort(model.Abort(encodeToByteString(reason)))
              })
            )
          )
        ),
        _ => ()
      )

    def branchCommitted(branch: BID): F[Unit] =
      sendCommand[F, TransactionCommand, UnitReply, Unit](
        id,
        TransactionCommand.of(
          Command.BranchCommitted(BranchCommittedCommand(encodeBranchID(branch)))
        ),
        _ => ()
      )

    def branchAborted(branch: BID): F[Unit] =
      sendCommand[F, TransactionCommand, UnitReply, Unit](
        id,
        TransactionCommand.of(Command.BranchAborted(BranchAbortedCommand(encodeBranchID(branch)))),
        _ => ()
      )

    def branchFailed(branch: BID, error: String): F[Unit] =
      sendCommand[F, TransactionCommand, UnitReply, Unit](
        id,
        TransactionCommand.of(
          Command.BranchFailed(BranchFailedCommand(encodeBranchID(branch), error))
        ),
        _ => ()
      )

    def timeout(): F[Unit] = sendCommand[F, TransactionCommand, UnitReply, Unit](
      id,
      TransactionCommand.of(Command.TransactionTimeout(TransactionTimeoutCommand())),
      _ => ()
    )
  }

  private def encodeAbortReason(reason: AbortReason[R]) = {
    reason match {
      case AbortReason.Timeout =>
        model.AbortReason(
          model.AbortReason.Reason.Timeout(model.TimeoutAbortReason())
        )
      case AbortReason.Branches(reasons) =>
        model.AbortReason(
          model.AbortReason.Reason.BranchesAborted(
            model.BranchesAbortReason(reasons.map(encodeToByteString[R]).toList)
          )
        )
      case AbortReason.Client(reason) =>
        model.AbortReason(
          model.AbortReason.Reason.ClientAborted(
            model.ClientAbortReason(reason.map(encodeToByteString[R]).getOrElse(ByteString.EMPTY))
          )
        )
    }
  }

  private def decodeAbortReason(reason: model.AbortReason) = {
    reason.reason match {
      case model.AbortReason.Reason.Timeout(_) => AbortReason.Timeout
      case model.AbortReason.Reason.BranchesAborted(model.BranchesAbortReason(reasons, _))
          if reasons.nonEmpty =>
        AbortReason.Branches(
          NonEmptyList.fromListUnsafe(reasons.map(decodeFromByteString[R]).toList)
        )
      case model.AbortReason.Reason.ClientAborted(model.ClientAbortReason(reason, _)) =>
        AbortReason.Client(Option.unless(reason.isEmpty)(decodeFromByteString[R](reason)))
      case _ => throw new UnexpectedReplyException
    }
  }

  private def encodeBranchID(branch: BID) = proto.model.BranchID(encodeToByteString(branch))

  private def decodeBranchID(branch: proto.model.BranchID) = decodeFromByteString[BID](branch.value)

  private def decodeTransactionID(transactionID: proto.model.TransactionID) =
    decodeFromByteString[TID](transactionID.value)

  private def encodeTransactionID(transactionID: TID) =
    proto.model.TransactionID(encodeToByteString(transactionID))

  private def encodeToByteString[A](a: A)(implicit encoder: Encoder[A]): ByteString =
    ByteString.copyFrom(encoder.encode(a))

  private def decodeFromByteString[A](bytes: ByteString)(implicit decoder: Decoder[A]): A =
    decoder.decode(bytes.toByteArray)

}

private[transaction] object TransactionProtocol {
  final class UnexpectedCommandException extends RuntimeException("Unexpected command")
  final class UnexpectedReplyException extends RuntimeException("Unexpected reply")
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy