1

I am trying to execute a future-returning operation on multiple items, and if any one of the operations fails, I need to execute custom rollback logic. However, the critical bit here is that the rollback logic MUST execute after all of the non-failing operations have completed.

Example:

  def fn(thing: Thing): EitherT[Future, Error, Thing] = {
    if (someCondition) {
      EitherT(Future.successful(Right(thing)))
    } else {
      EitherT(Future.successful(Left(error)))
    }
  }

  def rollback(): EitherT[...] = {
    // general cleanup
  }

  List(things).map(fn).sequence.leftMap {
    case err =>
      rollback()
      rrr
    
  }

Based on the response to this question, I understand that, if my operation were returning a Future[Thing], the sequence would complete as soon as any operation failed. However, using EitherT, I would think that, because all the Futures return a success (of either Left or Right), the sequence should not complete until all the operations' futures complete.

However, what I am seeing is that my rollback function is executing while the non-failed operations are still executing.

Is there an alternative to sequence I should be using here?

Thanks!

Sydney
  • 45
  • 6
  • Probably because `EiherT` should also short-circuit on the first **Left** as a normal `Either` would do; that is the joke of using the transformer. Just use a normal `Future[Either[Error, A]]` instead. Also `map` + `sequence` is `traverse` but I believe for futures the behavior is different. – Luis Miguel Mejía Suárez Jun 13 '21 at 22:06
  • @LuisMiguelMejíaSuárez thanks - Yes, I think that's true. It looks like Validation/ValidationNel would have been the way to go, but in our case, it's EitherT all the way down, so I have to do a bunch of ridiculous mapping to keep everything as a Successful(Right) until after the .sequence, and then map it back to the original Either. Ugly. – Sydney Jun 14 '21 at 00:26

1 Answers1

1

Futures run eagerly, and will keep going no matter what. Perhaps change fn signature to

def fn(thing: Thing): Future[Either[Error, Thing]]

and then sequence before constructing EitherT which should ensure all the Futures complete before next step in the chain

EitherT { 
  List(things)
    .map(fn)         // List[Future[Either[Error, Thing]]
    .sequence        // Future[List[Either[Error, Thing]]] (here all complete)
    .map(_.sequence) // Future[Either[Error, List[Thing]]]
}.leftMap { err =>
  rollback()
  rrr
}
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • Ah, also good thought. Sadly, pretty much every method in our system is EitherT. Given that, I am wondering if doing a collect instead of sequence would avoid the short circuit. – Sydney Jun 14 '21 at 04:23