From 38c09133f8318e1134510844406912f192276e53 Mon Sep 17 00:00:00 2001 From: Mehdi Mollaverdi Date: Fri, 31 May 2024 01:43:11 +1000 Subject: [PATCH] Hold onto randomly allocated ports to avoid port conflicts in concurrent tests (#182) * Hold onto randomly allocated ports to avoid port conflicts in concurrent tests * Rename releasePort to onBeforeStartup as per PR feedback --- .../tempest/testing/DockerDynamoDbServer.kt | 6 ++++-- .../testing/internal/TestDynamoDbService.kt | 18 +++++++++++++----- .../cash/tempest/testing/internal/TestUtils.kt | 8 ++++++-- .../cash/tempest/testing/JvmDynamoDbServer.kt | 6 ++++-- .../cash/tempest/testing/TestDynamoDbServer.kt | 3 ++- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/tempest-testing-docker/src/main/kotlin/app/cash/tempest/testing/DockerDynamoDbServer.kt b/tempest-testing-docker/src/main/kotlin/app/cash/tempest/testing/DockerDynamoDbServer.kt index a1240a7c7..434a839cb 100644 --- a/tempest-testing-docker/src/main/kotlin/app/cash/tempest/testing/DockerDynamoDbServer.kt +++ b/tempest-testing-docker/src/main/kotlin/app/cash/tempest/testing/DockerDynamoDbServer.kt @@ -24,12 +24,14 @@ import com.github.dockerjava.api.model.Ports import com.google.common.util.concurrent.AbstractIdleService class DockerDynamoDbServer private constructor( - override val port: Int + override val port: Int, + private val onBeforeStartup: () -> Unit ) : AbstractIdleService(), TestDynamoDbServer { override val id = "tempest-docker-dynamodb-local-$port" override fun startUp() { + onBeforeStartup() composer.start() // Temporary client to block until the container is running @@ -68,6 +70,6 @@ class DockerDynamoDbServer private constructor( ) object Factory : TestDynamoDbServer.Factory { - override fun create(port: Int) = DockerDynamoDbServer(port) + override fun create(port: Int, onBeforeStartup: () -> Unit) = DockerDynamoDbServer(port, onBeforeStartup) } } diff --git a/tempest-testing-internal/src/main/kotlin/app/cash/tempest/testing/internal/TestDynamoDbService.kt b/tempest-testing-internal/src/main/kotlin/app/cash/tempest/testing/internal/TestDynamoDbService.kt index 1f638ffc0..3f4bd4d93 100644 --- a/tempest-testing-internal/src/main/kotlin/app/cash/tempest/testing/internal/TestDynamoDbService.kt +++ b/tempest-testing-internal/src/main/kotlin/app/cash/tempest/testing/internal/TestDynamoDbService.kt @@ -46,9 +46,15 @@ class TestDynamoDbService private constructor( companion object { private val defaultPorts = ConcurrentHashMap() - private fun defaultPort(key: String): Int { + private fun defaultPort(key: String): PortHolder { + var releasePort: () -> Unit = {} // Only pick random port once to share one test server with multiple tests. - return defaultPorts.getOrPut(key, ::pickRandomPort) + val port = defaultPorts.getOrPut(key) { + val socket = allocateRandomPort() + releasePort = { socket.close() } + socket.localPort + } + return PortHolder(port, releasePort) } private val runningServers = ConcurrentHashMap.newKeySet() @@ -56,11 +62,13 @@ class TestDynamoDbService private constructor( @JvmStatic fun create(serverFactory: TestDynamoDbServer.Factory<*>, tables: List, port: Int? = null): TestDynamoDbService { - val port = port ?: defaultPort(serverFactory.toString()) + val portHolder = port?.let { PortHolder(it) } ?: defaultPort(serverFactory.toString()) return TestDynamoDbService( - DefaultTestDynamoDbClient(tables, port), - serverFactory.create(port) + DefaultTestDynamoDbClient(tables, portHolder.value), + serverFactory.create(portHolder.value, portHolder.releasePort) ) } } + + private data class PortHolder(val value: Int, val releasePort: () -> Unit = {}) } diff --git a/tempest-testing-internal/src/main/kotlin/app/cash/tempest/testing/internal/TestUtils.kt b/tempest-testing-internal/src/main/kotlin/app/cash/tempest/testing/internal/TestUtils.kt index 6113acc94..597ee1a70 100644 --- a/tempest-testing-internal/src/main/kotlin/app/cash/tempest/testing/internal/TestUtils.kt +++ b/tempest-testing-internal/src/main/kotlin/app/cash/tempest/testing/internal/TestUtils.kt @@ -32,8 +32,12 @@ import java.net.InetSocketAddress import java.net.ServerSocket import java.net.Socket -fun pickRandomPort(): Int { - ServerSocket(0).use { socket -> return socket.localPort } +fun allocateRandomPort(): ServerSocket { + val socket = ServerSocket(0) //use { socket -> return socket.localPort } + Runtime.getRuntime().addShutdownHook( + Thread { socket.close() } + ) + return socket } private const val CONNECT_TIMEOUT_MILLIS = 1_000 diff --git a/tempest-testing-jvm/src/main/kotlin/app/cash/tempest/testing/JvmDynamoDbServer.kt b/tempest-testing-jvm/src/main/kotlin/app/cash/tempest/testing/JvmDynamoDbServer.kt index 528d5e9a0..d29e27508 100644 --- a/tempest-testing-jvm/src/main/kotlin/app/cash/tempest/testing/JvmDynamoDbServer.kt +++ b/tempest-testing-jvm/src/main/kotlin/app/cash/tempest/testing/JvmDynamoDbServer.kt @@ -22,7 +22,8 @@ import com.google.common.util.concurrent.AbstractIdleService import java.io.File class JvmDynamoDbServer private constructor( - override val port: Int + override val port: Int, + private val onBeforeStartup: () -> Unit ) : AbstractIdleService(), TestDynamoDbServer { override val id = "tempest-jvm-dynamodb-local-$port" @@ -33,6 +34,7 @@ class JvmDynamoDbServer private constructor( val libraryFile = libsqlite4javaNativeLibrary() System.setProperty("sqlite4java.library.path", libraryFile.parent) + onBeforeStartup() server = ServerRunner.createServerFromCommandLineArgs( arrayOf("-inMemory", "-port", port.toString()) ) @@ -87,6 +89,6 @@ class JvmDynamoDbServer private constructor( } object Factory : TestDynamoDbServer.Factory { - override fun create(port: Int) = JvmDynamoDbServer(port) + override fun create(port: Int, onBeforeStartup: () -> Unit) = JvmDynamoDbServer(port, onBeforeStartup) } } diff --git a/tempest-testing/src/main/kotlin/app/cash/tempest/testing/TestDynamoDbServer.kt b/tempest-testing/src/main/kotlin/app/cash/tempest/testing/TestDynamoDbServer.kt index 45d39f321..b9bd828c4 100644 --- a/tempest-testing/src/main/kotlin/app/cash/tempest/testing/TestDynamoDbServer.kt +++ b/tempest-testing/src/main/kotlin/app/cash/tempest/testing/TestDynamoDbServer.kt @@ -26,6 +26,7 @@ interface TestDynamoDbServer : Service { val port: Int interface Factory { - fun create(port: Int): T + fun create(port: Int): T = create(port) {} + fun create(port: Int, onBeforeStartup: () -> Unit): T } }