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.
flatMap
would not work. There is important difference in when you createFuture
- insideflatMap
or outside - this will affect the time whenFuture
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 asval h = for { x <- Future { 5 } ...
- the former starts theFuture
earlier than the latter. – yǝsʞǝlascanner
is aval
then yourFuture
is already started before you enteredfor
comprehension. If it's adef
then it didn't. Looks like you need to make yourfutureScannedUser
adef
or a function() => Future[?]
– yǝsʞǝla