NeedingHelp
NeedingHelp

Reputation: 31

Understanding Scala Play Actions and Futures

I've really been struggling with some of these concepts in Scala and Play. I want to update my database, and I think I need to wrap my database update function in a Future, but I don't know how to return the required play.api.mvc.Result.

I have a controller in Scala that returns some response:

def somePath = MyCustomAction.async(parse.tolerantJson) { implicit request =>
    request.body.validate[MyObject].map { myObject =>
        for {
            getSomething <- getSomethingFuture
            getSomethingElse <- getSomethingElseFuture
        } yield {
            if (getSomethingElse) {
                if (getSomething)
                    updateMyDatabase(myObject)//perform database request
                else
                    BadRequest("Invalid request")
            }
        } else {
            // do some other things
        }
    }
}

private [this] def updateMyDatabase(myObject: MyObject) {
    // do things to update the database
}

Should updateMyDatabase be returning a Result? Do I want to wrap it in a Future and check if it completed successfully? And if I check, do I then return the correct Result on the Success method?

Right now, I'm not understanding how to structure these things or how to actually implement a Future based solution.

Upvotes: 3

Views: 435

Answers (2)

Jamie
Jamie

Reputation: 6114

@soote's answer works, but one reason to have updateMyDatabase return a future is so you can unify error handling using Future#recover. Another reason is so you can reuse the method in other places, since Play kind of expects blocking operations to be done in Futures

If updateMyDatabase returns a Future[_] you can do something like this:

def somePath = MyCustomAction.async(parse.tolerantJson) { implicit request =>
    request.body.validate[MyObject].map { myObject =>
        val futResult = for {
            getSomething <- getSomethingFuture
            getSomethingElse <- getSomethingElseFuture
            if getSomethingElse
            if getSomething
            _ <- updateMyDatabase(myObject)
        } yield Ok("")

        futResult.recover {
            case e: Exception1 => BadRequest("bad request")
            case e: Exception2 => BadRequest("blah")
            case e => InternalServerError(e.getMessage)
        }
    }
}

private [this] def updateMyDatabase(myObject: MyObject): Future[Unit] = ???

You may be wondering how to more granularly handle errors. Here's one way:

def somePath = MyCustomAction.async(parse.tolerantJson) { implicit request =>
    request.body.validate[MyObject].map { myObject =>
        val futResult = for {
            getSomething <- getSomethingFuture
            getSomethingElse <- getSomethingElseFuture
            _ <- if(getSomethingElse && getSomething) {
                   updateMyDatabase(myObject)
                 } else {
                   Future.failed(new CustomException("Couldn't get something else"))
                 }
        } yield Ok("")

        futResult.recover {
            case e: CustomException => BadRequest("failed to get something else")
            case e: Exception2 => BadRequest("blah")
            case e => InternalServerError(e.getMessage)
        }
    }
}

Here's an alternate way:

def somePath = MyCustomAction.async(parse.tolerantJson) { implicit request =>
    request.body.validate[MyObject].map { myObject =>
        val futResult = for {
            getSomething <- getSomethingFuture
            getSomethingElse <- getSomethingElseFuture
            result <- if(getSomethingElse && getSomething) {
                        updateMyDatabase(myObject).map(_ => Ok(""))
                      } else {
                        Future.successful(BadRequest("failed to get something else"))
                      }
        } yield result

        futResult.recover {
            case e: Exception2 => BadRequest("blah")
            case e => InternalServerError(e.getMessage)
        }
    }
}

Upvotes: 0

soote
soote

Reputation: 3260

Your updateMyDatabase function should return some none Unit value in order to tell if it succeeded or not. There are multiple responses a database action can return:

  1. Database error, exception thrown
  2. Row not found, no update occured
  3. Row found, and updated

So a Try[Boolean] would be a good type to handle all of these scenarios.

private [this] def updateMyDatabase(myObject: MyObject): Try[Boolean] = {
    // do things to update the database
}

We can now match on the response, and return the correct Result type.

updateMyDatabase(myObject) match {
    case Failure(exception) => BadRequest
    case Success(b) => if (b) Ok else BadRequest
}

Since getSomethingFuture and getSomethingElseFutures are both returning Futures, you are already working within the context of a Future, and do not need to wrap any of your Results in a Future. The yield keyword will make sure that it rewraps anything in the yield body back into a Future.

Now you still need to handle the situation where getSomethingFuture or getSomethingElseFuture fail. To do this, you can use the recover function. So your final code will look something like this:

(for {
    getSomething <- getSomethingFuture
    getSomethingElse <- getSomethingElseFuture
} yield {
    // this code only executes if both futures are successful.
    updateMyDatabase(myObject) match {
        case Failure(exception) => BadRequest
        case Success(b) => if (b) Ok else BadRequest
    }
}) recover {
    // Here you can match on different exception types and handle them accordingly.
    // So throw a specific exception for each task if you need to handle their failures differently.
    case e: GetSomethingFutureFailed => BadRequest
    case e: GetSomethingElseFutureFailed => BadRequest
    case _ => BadRequest
}

From the play documentation: Note that you may be tempted to therefore wrap your blocking code in Futures. This does not make it non-blocking, it just means the blocking will happen in a different thread. You still need to make sure that the thread pool that you are using has enough threads to handle the blocking.

Also make sure you instruct your controller to inject an execution context like so:

import scala.concurrent.ExecutionContext
class AsyncController @Inject() (...)(implicit exec: ExecutionContext)

Upvotes: 1

Related Questions