2
votes

While composing futures with a for-yield structure, some with side effects, some without, I introduced a race condition because a future depending on a side effect did not take the result of that side effecting future as an argument.

In short:

future b reads a value that is changed by a side effect from future a, but future a does not explicitly depend on the result of future b and could therefore happen before b finishes reading.

To solve the problem, my colleague introduced a dummy function taking as an argument the result of b and simply throwing it away. This was done to make the dependency explicit.

The actual code is here:

  val futureConnection:Future[(Either[String, (Connection)],Boolean)] =
    for {
      scannerUser <- scanner.orFail("Scanning user not found")
      scannedUser <- futureScannedUser.orFail("Scanned user not found")
      existsInAnyDirection <- connections.existsInAnyDirection(scannerUser, scannedUser)
      connection <- connections.createConnection(scannerUser, scannedUser, form.magicWord, existsInAnyDirection)
    } yield {
      (connection, existsInAnyDirection)
    }

In this case, future b is

connections.existsInAnyDirection(scannerUser, scannedUser)

and future a with the dummy parameter is

connections.createConnection(scannerUser, scannedUser, form.magicWord, existsInAnyDirection)

Notice that the parameter existsInAnyDirection is never used inside createConnection. This effectively creates the dependency graph that createConnection cannot be initiated before existsInAnyDirection is completed.

Now for the question:

Is there a more sane way to make the dependency explicit?


Bonus Info

My own digging tells me, that the Scala Futures simply don't handle side effects very well. The methods on the Future trait that deal with side effects return Unit, whereas there could very well be results to read from a side effecting operation, i.e. error codes, generated ID's, any other meta info, really.

1
I didn't understand why flatMap would not work. There is important difference in when you create Future - inside flatMap or outside - this will affect the time when Future starts. Perhaps that's the detail you missed and that's why you are getting the race condition. val f = Future { 5 }; val h = for { x <- f ... is not the same as val h = for { x <- Future { 5 } ... - the former starts the Future earlier than the latter.yǝsʞǝla
What I mean is that if scanner is a val then your Future is already started before you entered for comprehension. If it's a def then it didn't. Looks like you need to make your futureScannedUser a def or a function () => Future[?]yǝsʞǝla
It turns out, the fix introduced by my colleague was reordering the for yield expression as well as introducing the dummy parameter. The actual fix was the reordering, so my question was flawed.Felix

1 Answers

0
votes

Future is handling side effect of postponed computation like A => Future[B].

You tried to mix few different side effects but composed only one of them Future[_].

Try to choose second container, this can be Product or State, depends on your side effect and think in way of composing of side-effects (may be you will need modand transformers). And after your code can looks like (simplest cases):

for {
  scannerUser <- scanner.orFail("Scanning ...")
  (scannedUser, magicWord) <- futureScannedUser.orFail("Scanned ...")      
  connection <- connections.createConnection(scannerUser, scannedUser, magicWord)
} yield {
  (connection, existsInAnyDirection)
}

// OR

for {
  (scannerUser, state) <- scanner.orFail("Scanning ...")
  (scannedUser, nextState) <- futureScannedUser(state).orFail("Scanned ...")      
  connection <- connections.createConnection(scannerUser, scannedUser, nextState)
} yield {
  (connection, existsInAnyDirection)
}