Part III : Back to the Future
.. in which we return to a Futures-based design but this time compose them idiomatically
Now that a naive Future-based version is out it's time to make it look idiomatic in the third iteration. I was thinking about composing Futures with flatMap. I did not know how to chain multiple transaction rollbasks though. What I learned in the code review was unexpected.
A colleague caught me by surprise by showing that in a chain of Futures a recoverWith clause is called not just for the Future that failed but also for all the preceding ones.
A colleague caught me by surprise by showing that in a chain of Futures a recoverWith clause is called not just for the Future that failed but also for all the preceding ones.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** Simiular to ImperativeFutureSaga but somposes Futures idiomatically. */ | |
class FunctionalFuturesSaga(txs: Seq[ObjectStoreTx]) extends Function0[Future[TxContext]] { | |
def apply(): Future[TxContext] = executeHeadTx(txs, TxContext()) | |
private def executeHeadTx(txs: Seq[ObjectStoreTx], ctx: TxContext) : Future[TxContext] = { | |
txs match { | |
case Nil => Future.successful(ctx) | |
case tx :: tail => | |
tx. | |
execute(ctx). | |
flatMap {_ => executeHeadTx(tail, ctx)}. | |
recoverWith { case e: Exception => | |
e match { | |
case _: NestedTxException => // the actual failed tx has been logged | |
case _ => logger.error(s"Failed to execute ${tx.toString}") | |
} | |
tx.rollback() match { | |
case Success(_) => | |
case Failure(ue) => logger.error(s"Failed to roll back ${tx.toString}", ue) | |
} | |
Future.failed(new NestedTxException(e)) | |
} | |
} | |
} | |
/** To distinguish between the actual error happening and its propagation through the chain of Futures */ | |
private final class NestedTxException(e: Exception) extends RuntimeException(e) | |
} |
Another small difference is the nested exception class. The problem it solves is logging the transaction that failed. Without it every rolled back transaction would be logged. This cosmetic improvement doubles the size of the recoverWith body though. Without it this version looks really slick.
At this point I was happy with my pull request. But one thought spoiled the pleasure. We are dealing with a sequence of monadic abstractions. We use flatMap. Could there be some truly monadic approach to composing sagas? Yes, we are finally leaving common sense territory and going some controversial places.
At this point I was happy with my pull request. But one thought spoiled the pleasure. We are dealing with a sequence of monadic abstractions. We use flatMap. Could there be some truly monadic approach to composing sagas? Yes, we are finally leaving common sense territory and going some controversial places.
No comments:
Post a Comment