【问题标题】:Error handling and recovery when making 2 future based calls进行 2 个基于未来的调用时的错误处理和恢复
【发布时间】:2021-11-26 17:01:43
【问题描述】:

我正在进行 2 个调用,它们都返回以下内容:

    def processUser(..): Future[Either[Error, UserStatus]]
    def processTransactions(..): Future[Either[Error, TransactionStatus]]

所以我可以这样做:

    for {
       _ <- processUser(user)
       _ <- processTransaction(...)
    }

这两个调用不相互依赖,但processUser 必须在processTransaction 之前运行。

这是我的规则:

  1. 如果processUser返回错误,不要调用processTransaction
  2. 如果两者都成功且没有错误,则一切正常
  3. 如果 processUser 成功返回,但 processTransaction 失败并出现我想调用 unprocessUser 的错误。

我该怎么做?我猜使用 for 理解不太适合这种情况,哪种模式更理想?

注意

我没有使用猫库,只是使用带有 Futures 的普通 Scala 2.13.x。

【问题讨论】:

  • Future[Either[Error, ...Status]] 没有多大意义。如果这是您控制的代码,您可以简单地使Future 失败,而不是构建一个Either 来保存Error
  • @jwvh 异步错误和繁忙错误之间应该有区别,所以Future.successful(Left(...)) 是可能的。
  • 使用 failed Future 来保存错误意味着丢失可能的错误类型并有一些未知的异常类型来表示错误。最好仅将 Future 用于异步。

标签: scala future


【解决方案1】:

如果您使用 Cats,您可以使用称为 monad 转换器的东西,特别是 EitherT 用于您的情况:

import cats.implicits._
import cats.data.EitherT

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

type User
type UserStatus
type Transaction
type TransactionStatus

def processUser(user: User): Future[Either[Error, UserStatus]] = ???
def processTransaction(tx: Transaction): Future[Either[Error, TransactionStatus]] = ???
def unprocessUser(user: User): Future[Either[Error, TransactionStatus]]  = ???

val user : User = ???
val tx : Transaction = ???
def resCatsFuture(): Future[Either[Error, Unit]] =
  (for {
    userStatus <- EitherT(processUser(user))
    txStatus <- EitherT(processTransaction(tx))
      .handleErrorWith(e => EitherT(unprocessUser(user)))
  } yield ()).value

由于我们在 Cats 上,您还应该考虑将 Future 替换为 Cats Effect 中的 IO

import cats.effect._

def processUserIO(user: User): IO[Either[Error, UserStatus]] = ???
def processTransactionIO(tx: Transaction): IO[Either[Error, TransactionStatus]] = ???
def unprocessUserIO(user: User): IO[Either[Error, TransactionStatus]]  = ???

val resCatsIO: IO[Either[Error, Unit]] =
  (for {
    userStatus <- EitherT(processUserIO(user))
    txStatus <- EitherT(processTransactionIO(tx))
      .handleErrorWith(e => EitherT(unprocessUserIO(user)))
  } yield ()).value

或者使用ZIO 的输入错误作为 monad 转换器的替代方案:

import zio._

def processUserZIO(user: User): ZIO[Any, Error, UserStatus] = ???
def processTransactionZIO(tx: Transaction): ZIO[Any, Error, TransactionStatus] = ???
def unprocessUserZIO(user: User): ZIO[Any, Error, TransactionStatus]  = ???

val resZIO: ZIO[Any, Error, Unit] =
  for {
    userStatus <- processUserZIO(user)
    txStatus <- processTransactionZIO(tx)
      .catchAll(e => unprocessUserZIO(user))
  } yield ()

如果您想坚持使用原版 Scala,您可以在内部 Either 上使用 fold,并为 LeftRight 两种情况返回一个新的 Future,这样您就可以继续理解。

def unprocessUser2(user: User): Future[Transaction] = ???
def unprocessUser3(user: User): Future[Unit] = ???

val res: Future[Unit] =
  for {
    mUserStatus <- processUser(user)
    userStatus <- mUserStatus.fold(Future.failed, Future.successful) //you could also do `Future.fromTry(mUserStatus.toTry)` here
    mTxStatus <- processTransaction(tx)
    txStatus <- mTxStatus.fold(e => unprocessUser2(user), Future.successful)
    //or alternatively, depending on how you want to handle the `Left` and Right values
    _ <- mTxStatus.fold(e => unprocessUser3(user), tx => Future(println(tx)))
  } yield ()

//Redundant, but for consistency, if you want to keep to the same `Either` inner type.
val resEither: Future[Either[Error, Unit]] =
  for {
    mUserStatus <- processUser(user)
    userStatus <- mUserStatus.fold(Future.failed, Future.successful)
    mTxStatus <- processTransaction(tx)
    mTxStatus2 <- mTxStatus.fold(e => unprocessUser(user), tx => Future.successful(Right(tx)))
  } yield Right()

我对@9​​87654335@ 应该如何工作以及应该在哪里处理做出了很多假设。 Cats 和 ZIO 中还有其他错误处理方法可能更适合您的用例。

【讨论】:

  • 我没有使用猫 unfort.,只是简单的 jane scala 和期货。
  • @Blankman 添加了一个没有 Cats 或 ZIO 的解决方案
  • 谢谢,但是您更改了返回类型,它是:Future[Either[Error, Transaction]] 用于两个方法调用。
  • @Blankman 为保持一致性添加了另一个案例,基本上您只需将相关结果再次包装在 Right 中,但这没有必要,因为错误已被提升到 Future 无类型.
  • 这真的取决于unprocessUser实际上还必须返回什么。
【解决方案2】:

对于你想要的,我认为你不需要理解。试试这个(请注意这是直接写的答案,编译时可能会出现一些错误)

processUser(user) map {
   _ => processTransaction(user) flatMap {
       _ => // do nothing
   } recover {
     case e => unprocessUser()
   }
} recover {
  case e => {
    // throw err
  }
}

【讨论】:

    猜你喜欢
    • 2021-03-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-06-23
    • 2015-06-06
    • 2014-02-20
    • 2016-10-04
    • 2019-09-30
    相关资源
    最近更新 更多