Search code examples
scalaplayframeworkfuturethreadpoolexecutorexecutioncontext

Scala console application never exits when awaiting Futures


Whenever I run a scala process that uses the result of a Future (either through Await, map, onComplete, etc.), it never exits, forcing us to kill the process manually. This occurs whether I use extends App or just a standard def main(args: Array[String]) method.

It seems to me that it is related to the ThreadPoolExecutor that scala will spin up to execute the Future is hanging around at the end of the function, but I can't seem to get a handle to it to close it out.

For example, the following code will fail to exit:

object ExecuteApi extends App with StrictLogging{

  lazy val config = StratumConfiguration.setupConfiguration()
  lazy val apiEndpoint = config.getProperty("APIEndpoint").split("/").head
  lazy val packetApiPath = "packets/getpackets"

  val resourceNames = sys.env.getOrElse("ResourceNames", "").stripMargin.split("[\n\\s]+").map(_.trim).filterNot(_.isEmpty)
  val searchBody =
  s"""{
    |  "resourceNames": [
    |    "${(resourceNames).mkString("\",\"")}"
    |  ]
    |}""".stripMargin
    logger.info(searchBody)
    val responseFuture = AmazonAsyncApiGatewayHelper.executeHttpRequest(apiEndpoint, searchBody, Some(packetApiPath), Some("api"))
    val response = Await.result(responseFuture, Duration(25, TimeUnit.SECONDS))
    val layerDefinitions = Json.parse(response)
    println(Json.prettyPrint(layerDefinitions))
}

While this code exits just fine (the only change is the Async version that returns a future, which is then Awaited) :

object ExecuteAPI extends App with StrictLogging{

  lazy val config = Configuration.setupConfiguration()
  lazy val apiEndpoint = config.getProperty("APIEndpoint").split("/").head
  lazy val packetApiPath = "packets/getpackets"

  val resourceNames = sys.env.getOrElse("ResourceNames", "").stripMargin.split("\n").map(_.trim).filterNot(_.isEmpty)

  val searchBody =
  s"""{
    |  "resourceNames": [
    |    "${(resourceNames).mkString("\",\"")}"
    |  ]
    |}""".stripMargin
    logger.info(searchBody)
    val layersResponse = AmazonapiGatewayHelper.executeHttpRequest(apiEndpoint, searchBody, Some(packetApiPath), Some("api"))
    val layerDefinitions = Json.parse(layersResponse)
    println(Json.prettyPrint(layerDefinitions))
}

The code in AmazonAsyncApiGatewayHelper eventually creates the Future by executing the Play library HTTP client. However, we have seen this when executing Futures in other ways as well:

val request = wsClient.url(fullUrl)
    .withRequestTimeout(readTimeout)
val requestWithHeaders = headers.foldLeft(request) { (r, h) =>
  r.withHeaders(h)
}
val playResponseFuture = requestWithHeaders.post(requestBody)

Solution

  • We ended up finding the issue. The Play wsClient requires an actor system (starting in Play 2.5). We needed to manually terminate this actor system before we exited our main class. The code that exits looks like:

    object ExecuteAPI extends App with StrictLogging{
     try {
      lazy val config = Configuration.setupConfiguration()
      lazy val apiEndpoint = config.getProperty("APIEndpoint").split("/").head
      lazy val packetApiPath = "packets/getpackets"
    
      val resourceNames = sys.env.getOrElse("ResourceNames", "").stripMargin.split("\n").map(_.trim).filterNot(_.isEmpty)
    
      val searchBody =
      s"""{
        |  "resourceNames": [
        |    "${(resourceNames).mkString("\",\"")}"
        |  ]
        |}""".stripMargin
        logger.info(searchBody)
        val responseFuture = AmazonAsyncApiGatewayHelper.executeHttpRequest(apiEndpoint, searchBody, Some(packetApiPath), Some("api"))
        val response = Await.result(responseFuture, Duration(25, TimeUnit.SECONDS))
        val layerDefinitions = Json.parse(response)
        println(Json.prettyPrint(layerDefinitions))
     } finally {
        AmazonAsyncApiGatewayHelper.client.actorSystem.terminate()
     }
    }