scalaplayframeworkfuturefor-comprehensionconcurrent-processing

How to cancel a future action if another future did failed?


I have 2 futures (2 actions on db tables) and I want that before saving modifications to check if both futures have finished successfully.

Right now, I start second future inside the first (as a dependency), but I know is not the best option. I know I can use a for-comprehension to execute both futures in parallel, but even if one fail, the other will be executed (not tested yet)

firstFuture.dropColumn(tableName) match {
  case Success(_) => secondFuture.deleteEntity(entity)
  case Failure(e) => throw new Exception(e.getMessage)
}

// the  first future alters a table, drops a column
// the second future deletes a row from another table

in this case, if first future is executed successfully, the second can fail. I want to revert the update of first future. I heard about SQL transactions, seems to be something like that, but how?

val futuresResult = for {
  first <- firstFuture.dropColumn(tableName)
  second <- secondFuture.deleteEntity(entity)
} yield (first, second)

A for-comprehension is much better in my case because I don't have dependencies between these two futures and can be executed in parallel, but this not solve my problem, the result can be (success, success) or (failed, success) for example.


Solution

  • Regarding Future running sequentially vs in parallel:

    This is a bit tricky because Scala Future is designed to be eager. There are some other constructs across various Scala libraries that handle synchronous and asynchronous effects, such as cats IO, Monix Task, ZIO etc. which are designed in a lazy way, and they don't have this behaviour.

    The thing with Future being eager is that it will start the computation as soon as it is can. Here "start" means schedule it on an ExecutionContext that is either selected explicitly or present implicitly. While it's technically possible that the execution is stalled a bit in case the scheduler decides to do so, it will most likely be started almost instantaneously.

    So if you have a value of type Future, it's going to start running then and there. If you have a lazy value of type Future, or a function / method that returns a value of type Future, then it's not.

    But even if all you have are simple values (no lazy vals or defs), if the Future definition is done inside the for-comprehension, then it means it's part of a monadic flatMap chain (if you don't understand this, ignore it for now) and it will be run in sequence, not in parallel. Why? This is not specific to Futures; every for-comprehension has the semantics of being a sequential chain in which you can pass the result of the previous step to the next step. So it's only logical that you can't run something in step n + 1 if it depends on something from step n.

    Here's some code to demonstrate this.

    val program = for {
      _ <- Future { Thread.sleep(5000); println("f1") }
      _ <- Future { Thread.sleep(5000); println("f2") }
    } yield ()
    
    Await.result(program, Duration.Inf)
    

    This program will wait five seconds, then print "f1", then wait another five seconds, and then print "f2".

    Now let's take a look at this:

    val f1 = Future { Thread.sleep(5000); println("f1") }
    val f2 = Future { Thread.sleep(5000); println("f2") }
    
    val program = for {
      _ <- f1
      _ <- f2
    } yield ()
    
    Await.result(program, Duration.Inf)
    

    The program, however, will print "f1" and "f2" simultaneously after five seconds.

    Note that the sequence semantics are not really violated in the second case. f2 still has the opportunity to use the result of f1. But f2 is not using the result of f1; it's a standalone value that can be computed immediately (defined with a val). So if we change val f2 to a function, e.g. def f2(number: Int), then the execution changes:

    val f1 = Future { Thread.sleep(5000); println("f1"); 42 }
    def f2(number: Int) = Future { Thread.sleep(5000); println(number) }
    
    val program = for {
      number <- f1
      _ <- f2(number)
    } yield ()
    

    As you would expect, this will print "f1" after five seconds, and only then will the other Future start, so it will print "42" after another five seconds.

    Regarding transactions:

    As @cbley mentioned in the comment, this sounds like you want database transactions. For example, in SQL databases this has a very specific meaning and it ensures the ACID properties.

    If that's what you need, you need to solve it on the database layer. Future is too generic for that; it's just an effect type that models sync and async computations. When you see a Future value, just by looking at the type, you can't tell if it's the result of a database call or, say, some HTTP call.

    For example, doobie describes every database query as a ConnectionIO type. You can have multiple queries lined up in a for-comprehension, just how you would have with Future:

    val program = for {
      a <- database.getA()
      _ <- database.write("foo")
      b <- database.getB()
    } yield {
      // use a and b
    }
    

    But unlike our earlier examples, here getA() and getB() don't return a value of type Future[A], but ConnectionIO[A]. What's cool about that is that doobie completely takes care of the fact that you probably want these queries to be run in a single transaction, so if getB() fails, "foo" will not be committed to the database.

    So what you would do in that case is obtain the full description of your set of queries, wrap it into a single value program of type ConnectionIO, and once you want to actually run the transaction, you would do something like program.transact(myTransactor), where myTransactor is an instance of Transactor, a doobie construct that knows how to connect to your physical database.

    And once you transact, your ConnectionIO[A] is turned into a Future[A]. If the transaction failed, you'll have a failed Future, and nothing will be really committed to your database.

    If your database operations are independent of each other and can be run in parallel, doobie will also allow you to do that. Committing transactions via doobie, both in sequence and in parallel, is quite nicely explained in the docs.