2

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.

Felix
  • 8,385
  • 10
  • 40
  • 59
  • 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 Jan 26 '16 at 09:04
  • 1
    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 Jan 26 '16 at 09:13
  • 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 Jan 27 '16 at 09:27

1 Answers1

0

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)
}
Yuriy
  • 2,772
  • 15
  • 22