mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
postgresql-testing: "Lock" ports when starting PostgreSQL. (#5310)
* http-json: Ask for a free port by specifying port 0. This will avoid race conditions. * bindings-akka-testing: Delete RandomPorts; it's unused. * ports: Fix the Bazel test glob. * ports: Move FreePort to postgresql-testing and add a test case. * postgresql-testing: Make `FreePort.find()` return a `Port`. * postgresql-testing: Lock free ports until the server starts. This uses a `FileLock`, which should work well on all our supported operating systems as long as everyone agrees to use it. CHANGELOG_BEGIN CHANGELOG_END * postgresql-testing: Try to find a free port 10 times, then give up. * postgresql-testing: Use a shared directory for the port lock. * postgresql-testing: Try an alternative way of getting `%LOCALAPPDATA%`.
This commit is contained in:
parent
819210827e
commit
52dbcf5d95
@ -20,13 +20,11 @@ da_scala_library(
|
||||
],
|
||||
deps = [
|
||||
"//ledger-api/rs-grpc-bridge",
|
||||
"//libs-scala/ports",
|
||||
"@maven//:com_typesafe_akka_akka_actor_2_12",
|
||||
"@maven//:com_typesafe_akka_akka_stream_2_12",
|
||||
"@maven//:com_typesafe_config",
|
||||
"@maven//:com_typesafe_scala_logging_scala_logging_2_12",
|
||||
"@maven//:org_scalactic_scalactic_2_12",
|
||||
"@maven//:org_scalatest_scalatest_2_12",
|
||||
"@maven//:org_slf4j_slf4j_api",
|
||||
],
|
||||
)
|
||||
|
@ -1,35 +0,0 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.ledger.client.testing
|
||||
|
||||
import com.digitalasset.ports.FreePort
|
||||
import com.typesafe.scalalogging.LazyLogging
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
trait RandomPorts extends LazyLogging { self: AkkaTest =>
|
||||
|
||||
def randomPort(lowerBound: Option[Int] = None, upperBound: Option[Int] = None): Int = {
|
||||
|
||||
@tailrec
|
||||
def tryNext(candidatePort: Int = 0): Int = {
|
||||
if (candidatePort != 0) {
|
||||
logger.info(s"Using port $candidatePort")
|
||||
candidatePort
|
||||
} else {
|
||||
val port = FreePort.find()
|
||||
logger.info(s"Checking port $port")
|
||||
val isHighEnough = lowerBound.fold(true)(port > _)
|
||||
val isLowEnough = upperBound.fold(true)(port < _)
|
||||
if (isHighEnough && isLowEnough) {
|
||||
tryNext(port)
|
||||
} else {
|
||||
tryNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tryNext()
|
||||
}
|
||||
}
|
@ -27,7 +27,7 @@ import com.digitalasset.platform.common.LedgerIdMode
|
||||
import com.digitalasset.platform.sandbox.SandboxServer
|
||||
import com.digitalasset.platform.sandbox.config.SandboxConfig
|
||||
import com.digitalasset.platform.services.time.TimeProviderType
|
||||
import com.digitalasset.ports.{FreePort, Port}
|
||||
import com.digitalasset.ports.Port
|
||||
import scalaz._
|
||||
import scalaz.std.option._
|
||||
import scalaz.std.scalaFuture._
|
||||
@ -62,23 +62,22 @@ object HttpServiceTestFixture {
|
||||
port <- ledger.portF
|
||||
} yield (ledger, port)
|
||||
|
||||
val httpServiceF: Future[(ServerBinding, Int)] = for {
|
||||
val httpServiceF: Future[ServerBinding] = for {
|
||||
(_, ledgerPort) <- ledgerF
|
||||
contractDao <- contractDaoF
|
||||
httpPort <- Future(FreePort.find())
|
||||
httpService <- stripLeft(
|
||||
HttpService.start(
|
||||
"localhost",
|
||||
ledgerPort.value,
|
||||
applicationId,
|
||||
"localhost",
|
||||
httpPort,
|
||||
0,
|
||||
Some(Config.DefaultWsConfig),
|
||||
None,
|
||||
contractDao,
|
||||
staticContentConfig,
|
||||
doNotReloadPackages))
|
||||
} yield (httpService, httpPort)
|
||||
} yield httpService
|
||||
|
||||
val clientF: Future[LedgerClient] = for {
|
||||
(_, ledgerPort) <- ledgerF
|
||||
@ -91,10 +90,11 @@ object HttpServiceTestFixture {
|
||||
} yield codecs
|
||||
|
||||
val fa: Future[A] = for {
|
||||
(_, httpPort) <- httpServiceF
|
||||
httpService <- httpServiceF
|
||||
address = httpService.localAddress
|
||||
uri = Uri.from(scheme = "http", host = address.getHostName, port = address.getPort)
|
||||
(encoder, decoder) <- codecsF
|
||||
client <- clientF
|
||||
uri = Uri.from(scheme = "http", host = "localhost", port = httpPort)
|
||||
a <- testFn(uri, encoder, decoder, client)
|
||||
} yield a
|
||||
|
||||
@ -103,7 +103,7 @@ object HttpServiceTestFixture {
|
||||
.sequence(
|
||||
Seq(
|
||||
ledgerF.map(_._1.close()),
|
||||
httpServiceF.flatMap(_._1.unbind()),
|
||||
httpServiceF.flatMap(_.unbind()),
|
||||
) map (_ fallbackTo Future.successful(())))
|
||||
.transform(_ => ta)
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ da_scala_library(
|
||||
|
||||
da_scala_test(
|
||||
name = "ports-tests",
|
||||
srcs = glob(["src/test/suite/**/*.scala"]),
|
||||
srcs = glob(["src/test/suite/scala/**/*.scala"]),
|
||||
deps = [
|
||||
":ports",
|
||||
],
|
||||
|
@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.ports
|
||||
|
||||
import java.net.{InetAddress, ServerSocket}
|
||||
|
||||
object FreePort {
|
||||
def find(): Int = {
|
||||
val socket = new ServerSocket(0, 0, InetAddress.getLoopbackAddress)
|
||||
try {
|
||||
socket.getLocalPort
|
||||
} finally {
|
||||
// We have to release the port so that it can be used. Note that there is a small race window,
|
||||
// as releasing the port then handing it to the server is not atomic. If this turns out to be
|
||||
// an issue, we need to find an atomic way of doing that.
|
||||
socket.close()
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@
|
||||
load(
|
||||
"//bazel_tools:scala.bzl",
|
||||
"da_scala_library",
|
||||
"da_scala_test",
|
||||
)
|
||||
|
||||
da_scala_library(
|
||||
@ -25,3 +26,12 @@ da_scala_library(
|
||||
"@maven//:org_scalatest_scalatest_2_12",
|
||||
],
|
||||
)
|
||||
|
||||
da_scala_test(
|
||||
name = "postgresql-testing-tests",
|
||||
srcs = glob(["src/test/suite/scala/**/*.scala"]),
|
||||
deps = [
|
||||
":postgresql-testing",
|
||||
"//libs-scala/ports",
|
||||
],
|
||||
)
|
||||
|
@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.testing.postgresql
|
||||
|
||||
import java.net.{InetAddress, ServerSocket}
|
||||
|
||||
import com.digitalasset.ports.Port
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
private[postgresql] object FreePort {
|
||||
|
||||
@tailrec
|
||||
def find(tries: Int = 10): PortLock.Locked = {
|
||||
val socket = new ServerSocket(0, 0, InetAddress.getLoopbackAddress)
|
||||
val portLock = try {
|
||||
val port = Port(socket.getLocalPort)
|
||||
PortLock.lock(port)
|
||||
} finally {
|
||||
socket.close()
|
||||
}
|
||||
portLock match {
|
||||
case Right(locked) =>
|
||||
socket.close()
|
||||
locked
|
||||
case Left(failure) =>
|
||||
socket.close()
|
||||
if (tries <= 1) {
|
||||
throw failure
|
||||
} else {
|
||||
find(tries - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.testing.postgresql
|
||||
|
||||
import java.io.RandomAccessFile
|
||||
import java.nio.channels.{
|
||||
ClosedChannelException,
|
||||
FileChannel,
|
||||
FileLock,
|
||||
OverlappingFileLockException
|
||||
}
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
|
||||
import com.digitalasset.ports.Port
|
||||
|
||||
private[postgresql] object PortLock {
|
||||
|
||||
// We can't use `sys.props("java.io.tmpdir")` because Bazel changes this for each test run.
|
||||
// For this to be useful, it needs to be shared across concurrent runs.
|
||||
private val portLockDirectory: Path = {
|
||||
val tempDirectory =
|
||||
if (sys.props("os.name").startsWith("Windows")) {
|
||||
Paths.get(sys.props("user.home"), "AppData", "Local", "Temp")
|
||||
} else {
|
||||
Paths.get("/tmp")
|
||||
}
|
||||
tempDirectory.resolve(Paths.get("daml", "build", "postgresql-testing", "ports"))
|
||||
}
|
||||
|
||||
def lock(port: Port): Either[FailedToLock, Locked] = {
|
||||
Files.createDirectories(portLockDirectory)
|
||||
val portLockFile = portLockDirectory.resolve(port.toString)
|
||||
val file = new RandomAccessFile(portLockFile.toFile, "rw")
|
||||
val channel = file.getChannel
|
||||
try {
|
||||
val lock = channel.tryLock()
|
||||
val locked = new Locked(port, lock, channel, file)
|
||||
if (lock != null) {
|
||||
Right(locked)
|
||||
} else {
|
||||
locked.unlock()
|
||||
Left(FailedToLock(port))
|
||||
}
|
||||
} catch {
|
||||
case _: OverlappingFileLockException =>
|
||||
channel.close()
|
||||
file.close()
|
||||
Left(FailedToLock(port))
|
||||
}
|
||||
}
|
||||
|
||||
final class Locked(val port: Port, lock: FileLock, channel: FileChannel, file: RandomAccessFile) {
|
||||
def unlock(): Unit = {
|
||||
try {
|
||||
lock.release()
|
||||
} catch {
|
||||
// ignore
|
||||
case _: ClosedChannelException =>
|
||||
}
|
||||
channel.close()
|
||||
file.close()
|
||||
}
|
||||
}
|
||||
|
||||
case class FailedToLock(port: Port) extends RuntimeException(s"Failed to lock port $port.")
|
||||
|
||||
}
|
@ -9,7 +9,6 @@ import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.{Files, Path, Paths}
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
import com.digitalasset.ports.FreePort
|
||||
import com.digitalasset.testing.postgresql.PostgresAround._
|
||||
import org.apache.commons.io.{FileUtils, IOUtils}
|
||||
import org.slf4j.LoggerFactory
|
||||
@ -28,7 +27,8 @@ trait PostgresAround {
|
||||
val tempDir = Files.createTempDirectory("postgres_test")
|
||||
val dataDir = tempDir.resolve("data")
|
||||
val confFile = Paths.get(dataDir.toString, "postgresql.conf")
|
||||
val port = FreePort.find()
|
||||
val lockedPort = FreePort.find()
|
||||
val port = lockedPort.port
|
||||
val jdbcUrl = s"jdbc:postgresql://$hostName:$port/$databaseName?user=$userName"
|
||||
val logFile = Files.createFile(tempDir.resolve("postgresql.log"))
|
||||
postgresFixture = PostgresFixture(jdbcUrl, port, tempDir, dataDir, confFile, logFile)
|
||||
@ -37,10 +37,12 @@ trait PostgresAround {
|
||||
initializeDatabase()
|
||||
createConfigFile()
|
||||
startPostgres()
|
||||
lockedPort.unlock()
|
||||
createTestDatabase(databaseName)
|
||||
logger.info(s"PostgreSQL has started on port $port.")
|
||||
} catch {
|
||||
case NonFatal(e) =>
|
||||
lockedPort.unlock()
|
||||
stopPostgres()
|
||||
deleteRecursively(tempDir)
|
||||
postgresFixture = null
|
||||
|
@ -5,9 +5,11 @@ package com.digitalasset.testing.postgresql
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import com.digitalasset.ports.Port
|
||||
|
||||
case class PostgresFixture(
|
||||
jdbcUrl: String,
|
||||
port: Int,
|
||||
port: Port,
|
||||
tempDir: Path,
|
||||
dataDir: Path,
|
||||
confFile: Path,
|
||||
|
@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package com.digitalasset.testing.postgresql
|
||||
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
|
||||
class FreePortSpec extends WordSpec with Matchers {
|
||||
"a free port" should {
|
||||
"always be available" in {
|
||||
val lockedPort = FreePort.find()
|
||||
try {
|
||||
lockedPort.port.value should (be >= 1024 and be < 65536)
|
||||
} finally {
|
||||
lockedPort.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
"lock, to prevent race conditions" in {
|
||||
val lockedPort = FreePort.find()
|
||||
try {
|
||||
PortLock.lock(lockedPort.port) should be(Left(PortLock.FailedToLock(lockedPort.port)))
|
||||
} finally {
|
||||
lockedPort.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
"unlock when the server's started" in {
|
||||
val lockedPort = FreePort.find()
|
||||
lockedPort.unlock()
|
||||
|
||||
val locked = PortLock
|
||||
.lock(lockedPort.port)
|
||||
.fold(failure => throw failure, identity)
|
||||
locked.unlock()
|
||||
succeed
|
||||
}
|
||||
|
||||
"can be unlocked twice" in {
|
||||
val lockedPort = FreePort.find()
|
||||
lockedPort.unlock()
|
||||
lockedPort.unlock()
|
||||
succeed
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user