mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
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:
parent
1ddcd3c096
commit
29e1931f17
@ -96,6 +96,7 @@ trait JsonApiFixture
|
||||
"localhost",
|
||||
0,
|
||||
None,
|
||||
None,
|
||||
None)(
|
||||
jsonApiActorSystem,
|
||||
jsonApiMaterializer,
|
||||
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user