Add --port-file command line option to JSON API (#5454)

* Adding `--port-file` support

* ``--port-file`` support

* Updating docs

changelog_begin

[JSON API] Add support for ``--port-file`` command line option.
``--http-port 0 --port-file ./json-api.port`` will pick up a free port
and write it into ``./json-api.port` file.

changelog_end

* reformatting

* Usage grammar

* use bimap

* Adding `PortFiles` utility for creating and deleting port files on JVM exit

* Adding scaladoc explaining that the port file should be deleted on

JVM termination.

* Updating usage and docs to reflect that the file must be unique and

will be deleted on graceful shutdown

* Relying on `java.nio.file.FileAlreadyExistsException` to determine the

case when failed due to the nonunique file name.

* toString instead of Exception.getMessage

java.nio exception's getMessage can be just a file name, need the class
name to capture the error context.

* updatePortFile -> createPortFile

* write to file instead of write into file
This commit is contained in:
Leonid Shlyapnikov 2020-04-08 14:48:11 -04:00 committed by GitHub
parent 1ddcd3c096
commit 29e1931f17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 134 additions and 13 deletions

View File

@ -96,6 +96,7 @@ trait JsonApiFixture
"localhost",
0,
None,
None,
None)(
jsonApiActorSystem,
jsonApiMaterializer,

View File

@ -72,7 +72,9 @@ From a DAML project directory:
--address <value>
IP address that HTTP JSON API service listens on. Defaults to 127.0.0.1.
--http-port <value>
HTTP JSON API service port number
HTTP JSON API service port number. A port number of 0 will let the system pick an ephemeral port. Consider specifying `--port-file` option with port number 0.
--port-file <value>
Optional unique file name where to write the allocated HTTP port number. If process terminates gracefully, this file will be deleted automatically. Used to inform clients in CI about which port HTTP JSON API listens on. Defaults to none, that is, no file gets created.
--application-id <value>
Optional application ID to use for ledger registration. Defaults to HTTP-JSON-API-Gateway
--package-reload-interval <value>

View File

@ -40,6 +40,7 @@ da_scala_library(
"//ledger/ledger-api-auth",
"//ledger/ledger-api-common",
"//libs-scala/auth-utils",
"//libs-scala/ports",
"@maven//:com_chuusai_shapeless_2_12",
"@maven//:com_github_scopt_scopt_2_12",
"@maven//:com_lihaoyi_sourcecode_2_12",
@ -81,6 +82,7 @@ da_scala_binary(
"//ledger/ledger-api-auth",
"//ledger/ledger-api-common",
"//libs-scala/auth-utils",
"//libs-scala/ports",
"@maven//:ch_qos_logback_logback_classic",
"@maven//:com_chuusai_shapeless_2_12",
"@maven//:com_github_scopt_scopt_2_12",

View File

@ -23,6 +23,7 @@ private[http] final case class Config(
ledgerPort: Int,
address: String = InetAddress.getLoopbackAddress.getHostAddress,
httpPort: Int,
portFile: Option[Path] = None,
applicationId: ApplicationId = ApplicationId("HTTP-JSON-API-Gateway"),
packageReloadInterval: FiniteDuration = HttpService.DefaultPackageReloadInterval,
maxInboundMessageSize: Int = HttpService.DefaultMaxInboundMessageSize,

View File

@ -37,6 +37,7 @@ import com.daml.ledger.client.configuration.{
import com.daml.ledger.client.services.pkg.PackageClient
import com.daml.ledger.service.LedgerReader
import com.daml.ledger.service.LedgerReader.PackageStore
import com.daml.ports.{Port, PortFiles}
import com.typesafe.scalalogging.StrictLogging
import io.grpc.netty.NettyChannelBuilder
import scalaz.Scalaz._
@ -61,6 +62,7 @@ object HttpService extends StrictLogging {
applicationId: ApplicationId,
address: String,
httpPort: Int,
portFile: Option[Path],
wsConfig: Option[WebsocketConfig],
accessTokenFile: Option[Path],
contractDao: Option[ContractDao] = None,
@ -167,6 +169,8 @@ object HttpService extends StrictLogging {
Http().bindAndHandleAsync(allEndpoints, address, httpPort, settings = settings),
)
_ <- either(portFile.cata(f => createPortFile(f, binding), \/-(()))): ET[Unit]
} yield binding
bindingEt.run: Future[Error \/ ServerBinding]
@ -284,4 +288,11 @@ object HttpService extends StrictLogging {
case NonFatal(e) =>
\/.left(Error(s"Cannot connect to the ledger server, error: ${e.description}"))
}
private def createPortFile(
file: Path,
binding: akka.http.scaladsl.Http.ServerBinding): Error \/ Unit = {
import util.ErrorOps._
PortFiles.write(file, Port(binding.localAddress.getPort)).liftErr(Error)
}
}

View File

@ -3,7 +3,8 @@
package com.daml.http
import java.nio.file.Paths
import java.io.File
import java.nio.file.{Path, Paths}
import akka.actor.ActorSystem
import akka.http.scaladsl.Http.ServerBinding
@ -44,6 +45,7 @@ object Main extends StrictLogging {
logger.info(
s"Config(ledgerHost=${config.ledgerHost: String}, ledgerPort=${config.ledgerPort: Int}" +
s", address=${config.address: String}, httpPort=${config.httpPort: Int}" +
s", portFile=${config.portFile: Option[Path]}" +
s", applicationId=${config.applicationId.unwrap: String}" +
s", packageReloadInterval=${config.packageReloadInterval.toString}" +
s", maxInboundMessageSize=${config.maxInboundMessageSize: Int}" +
@ -85,6 +87,7 @@ object Main extends StrictLogging {
applicationId = config.applicationId,
address = config.address,
httpPort = config.httpPort,
portFile = config.portFile,
wsConfig = config.wsConfig,
accessTokenFile = config.accessTokenFile,
contractDao = contractDao,
@ -155,7 +158,19 @@ object Main extends StrictLogging {
opt[Int]("http-port")
.action((x, c) => c.copy(httpPort = x))
.required()
.text("HTTP JSON API service port number")
.text(
"HTTP JSON API service port number. " +
"A port number of 0 will let the system pick an ephemeral port. " +
"Consider specifying `--port-file` option with port number 0.")
opt[File]("port-file")
.action((x, c) => c.copy(portFile = Some(x.toPath)))
.optional()
.text(
"Optional unique file name where to write the allocated HTTP port number. " +
"If process terminates gracefully, this file will be deleted automatically. " +
"Used to inform clients in CI about which port HTTP JSON API listens on. " +
"Defaults to none, that is, no file gets created.")
opt[String]("application-id")
.action((x, c) => c.copy(applicationId = ApplicationId(x)))

View File

@ -67,16 +67,18 @@ object HttpServiceTestFixture {
contractDao <- contractDaoF
httpService <- stripLeft(
HttpService.start(
"localhost",
ledgerPort.value,
applicationId,
"localhost",
0,
Some(Config.DefaultWsConfig),
None,
contractDao,
staticContentConfig,
doNotReloadPackages))
ledgerHost = "localhost",
ledgerPort = ledgerPort.value,
applicationId = applicationId,
address = "localhost",
httpPort = 0,
portFile = None,
wsConfig = Some(Config.DefaultWsConfig),
accessTokenFile = None,
contractDao = contractDao,
staticContentConfig = staticContentConfig,
packageReloadInterval = doNotReloadPackages
))
} yield httpService
val clientF: Future[LedgerClient] = for {

View File

@ -14,6 +14,9 @@ da_scala_library(
visibility = [
"//visibility:public",
],
deps = [
"@maven//:org_scalaz_scalaz_core_2_12",
],
)
da_scala_test(
@ -21,5 +24,6 @@ da_scala_test(
srcs = glob(["src/test/suite/scala/**/*.scala"]),
deps = [
":ports",
"@maven//:org_scalaz_scalaz_core_2_12",
],
)

View File

@ -0,0 +1,44 @@
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.ports
import java.nio.file.{Files, Path}
import scalaz.{Show, \/}
import scala.collection.JavaConverters._
object PortFiles {
sealed abstract class Error extends Serializable with Product
final case class FileAlreadyExists(path: Path) extends Error
final case class CannotWriteToFile(path: Path, reason: String) extends Error
object Error {
implicit val showInstance: Show[Error] = Show.shows {
case FileAlreadyExists(path) =>
s"Port file already exists: ${path.toAbsolutePath: Path}"
case CannotWriteToFile(path, reason) =>
s"Cannot write to port file: ${path.toAbsolutePath: Path}, reason: $reason"
}
}
/**
* Creates a port and requests that the created file be deleted when the virtual machine terminates.
* See [[java.io.File#deleteOnExit()]].
*/
def write(path: Path, port: Port): Error \/ Unit =
\/.fromTryCatchNonFatal {
writeUnsafe(path, port)
}.leftMap {
case _: java.nio.file.FileAlreadyExistsException => FileAlreadyExists(path)
case e => CannotWriteToFile(path, e.toString)
}
private def writeUnsafe(path: Path, port: Port): Unit = {
import java.nio.file.StandardOpenOption.CREATE_NEW
val lines: java.lang.Iterable[String] = List(port.value.toString).asJava
val created = Files.write(path, lines, CREATE_NEW)
created.toFile.deleteOnExit()
}
}

View File

@ -0,0 +1,39 @@
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package com.daml.ports
import java.nio.file.{Path, Paths}
import java.util.UUID
import com.daml.ports.PortFiles.FileAlreadyExists
import org.scalatest.{FreeSpec, Inside, Matchers}
import scalaz.{-\/, \/-}
@SuppressWarnings(Array("org.wartremover.warts.Any"))
class PortFilesSpec extends FreeSpec with Matchers with Inside {
"Can create a port file with a unique file name" in {
val path = uniquePath()
inside(PortFiles.write(path, Port(1024))) {
case \/-(()) =>
}
path.toFile.exists() shouldBe true
}
"Cannot create a port file with a nonunique file name" in {
val path = uniquePath()
inside(PortFiles.write(path, Port(1024))) {
case \/-(()) =>
}
inside(PortFiles.write(path, Port(1024))) {
case -\/(FileAlreadyExists(p)) =>
p shouldBe path
}
}
private def uniquePath(): Path = {
val fileName = s"${this.getClass.getSimpleName}-${UUID.randomUUID().toString}.dummy"
Paths.get(fileName)
}
}