Skip to content

Commit

Permalink
KTOR-7359 Implement a suspending version of EmbeddedServer.start and …
Browse files Browse the repository at this point in the history
…EmbeddedServer.stop (#4481)

* Add startSuspend/stopSuspend in EmbeddedServer
* Make EngineTestBase work on js/wasmJs
  • Loading branch information
whyoleg authored Nov 19, 2024
1 parent 65816e9 commit ba4d001
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 8 deletions.
4 changes: 4 additions & 0 deletions ktor-server/ktor-server-core/api/ktor-server-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -590,9 +590,13 @@ public final class io/ktor/server/engine/EmbeddedServer {
public final fun reload ()V
public final fun start (Z)Lio/ktor/server/engine/EmbeddedServer;
public static synthetic fun start$default (Lio/ktor/server/engine/EmbeddedServer;ZILjava/lang/Object;)Lio/ktor/server/engine/EmbeddedServer;
public final fun startSuspend (ZLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun startSuspend$default (Lio/ktor/server/engine/EmbeddedServer;ZLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
public final fun stop (JJ)V
public final fun stop (JJLjava/util/concurrent/TimeUnit;)V
public static synthetic fun stop$default (Lio/ktor/server/engine/EmbeddedServer;JJILjava/lang/Object;)V
public final fun stopSuspend (JJLkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static synthetic fun stopSuspend$default (Lio/ktor/server/engine/EmbeddedServer;JJLkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
}

public final class io/ktor/server/engine/EmbeddedServerKt {
Expand Down
2 changes: 2 additions & 0 deletions ktor-server/ktor-server-core/api/ktor-server-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,8 @@ final class <#A: io.ktor.server.engine/ApplicationEngine, #B: io.ktor.server.eng

final fun start(kotlin/Boolean = ...): io.ktor.server.engine/EmbeddedServer<#A, #B> // io.ktor.server.engine/EmbeddedServer.start|start(kotlin.Boolean){}[0]
final fun stop(kotlin/Long = ..., kotlin/Long = ...) // io.ktor.server.engine/EmbeddedServer.stop|stop(kotlin.Long;kotlin.Long){}[0]
final suspend fun startSuspend(kotlin/Boolean = ...): io.ktor.server.engine/EmbeddedServer<#A, #B> // io.ktor.server.engine/EmbeddedServer.startSuspend|startSuspend(kotlin.Boolean){}[0]
final suspend fun stopSuspend(kotlin/Long = ..., kotlin/Long = ...) // io.ktor.server.engine/EmbeddedServer.stopSuspend|stopSuspend(kotlin.Long;kotlin.Long){}[0]
}

final class <#A: kotlin/Any, #B: io.ktor.events/EventDefinition<#A>> io.ktor.server.application.hooks/MonitoringEvent : io.ktor.server.application/Hook<kotlin/Function1<#A, kotlin/Unit>> { // io.ktor.server.application.hooks/MonitoringEvent|null[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package io.ktor.server.engine

import io.ktor.events.*
import io.ktor.server.application.*
import io.ktor.server.engine.internal.*
import io.ktor.util.logging.*
import kotlinx.coroutines.*
import kotlin.coroutines.*
Expand Down Expand Up @@ -34,10 +35,17 @@ public expect class EmbeddedServer<TEngine : ApplicationEngine, TConfiguration :

public fun start(wait: Boolean = false): EmbeddedServer<TEngine, TConfiguration>

public suspend fun startSuspend(wait: Boolean = false): EmbeddedServer<TEngine, TConfiguration>

public fun stop(
gracePeriodMillis: Long = engineConfig.shutdownGracePeriod,
timeoutMillis: Long = engineConfig.shutdownGracePeriod
)

public suspend fun stopSuspend(
gracePeriodMillis: Long = engineConfig.shutdownGracePeriod,
timeoutMillis: Long = engineConfig.shutdownGracePeriod
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ actual constructor(

private val modules = rootConfig.modules

public actual fun start(wait: Boolean): EmbeddedServer<TEngine, TConfiguration> {
addShutdownHook { stop() }

private fun prepareToStart() {
safeRaiseEvent(ApplicationStarting, application)
try {
modules.forEach { application.it() }
Expand All @@ -65,9 +63,20 @@ actual constructor(
)
}
}
}

public actual fun start(wait: Boolean): EmbeddedServer<TEngine, TConfiguration> {
addShutdownHook { stop() }
prepareToStart()
engine.start(wait)
return this
}

@OptIn(DelicateCoroutinesApi::class)
public actual suspend fun startSuspend(wait: Boolean): EmbeddedServer<TEngine, TConfiguration> {
addShutdownHook { GlobalScope.launch { stopSuspend() } }
prepareToStart()
engine.startSuspend(wait)
return this
}

Expand All @@ -76,6 +85,11 @@ actual constructor(
destroy(application)
}

public actual suspend fun stopSuspend(gracePeriodMillis: Long, timeoutMillis: Long) {
engine.stopSuspend(gracePeriodMillis, timeoutMillis)
destroy(application)
}

private fun destroy(application: Application) {
safeRaiseEvent(ApplicationStopping, application)
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ actual constructor(
return this
}

public actual suspend fun startSuspend(wait: Boolean): EmbeddedServer<TEngine, TConfiguration> {
return withContext(Dispatchers.IOBridge) { start(wait) }
}

public fun stop(shutdownGracePeriod: Long, shutdownTimeout: Long, timeUnit: TimeUnit) {
try {
engine.stop(timeUnit.toMillis(shutdownGracePeriod), timeUnit.toMillis(shutdownTimeout))
Expand All @@ -312,6 +316,10 @@ actual constructor(
stop(gracePeriodMillis, timeoutMillis, TimeUnit.MILLISECONDS)
}

public actual suspend fun stopSuspend(gracePeriodMillis: Long, timeoutMillis: Long) {
withContext(Dispatchers.IOBridge) { stop(gracePeriodMillis, timeoutMillis) }
}

private fun instantiateAndConfigureApplication(currentClassLoader: ClassLoader): Application {
val newInstance = if (recreateInstance || _applicationInstance == null) {
Application(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,19 @@ actual constructor(
return this
}

public actual suspend fun startSuspend(wait: Boolean): EmbeddedServer<TEngine, TConfiguration> {
return withContext(Dispatchers.IOBridge) { start(wait) }
}

public actual fun stop(gracePeriodMillis: Long, timeoutMillis: Long) {
engine.stop(gracePeriodMillis, timeoutMillis)
destroy(application)
}

public actual suspend fun stopSuspend(gracePeriodMillis: Long, timeoutMillis: Long) {
withContext(Dispatchers.IOBridge) { stop(gracePeriodMillis, timeoutMillis) }
}

private fun destroy(application: Application) {
safeRaiseEvent(ApplicationStopping, application)
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@ import io.ktor.server.testing.*
import io.ktor.util.logging.*
import kotlinx.coroutines.*
import kotlin.coroutines.*
import kotlin.test.*
import kotlin.time.Duration.Companion.seconds

private const val UNINITIALIZED_PORT = -1
private const val DEFAULT_PORT = 0

actual abstract class EngineTestBase<
TEngine : ApplicationEngine,
TConfiguration : ApplicationEngine.Configuration
Expand All @@ -32,14 +36,35 @@ actual constructor(
@Retention
protected actual annotation class Http2Only actual constructor()

protected actual var port: Int = 0
/**
* It's not possible to find a free port during test setup,
* as on JS (Node.js) all APIs are non-blocking (suspend).
* That's why we assign port after the server is started in [startServer]
* Note: this means, that [port] can be used only after calling [createAndStartServer] or [startServer].
*/
private var _port: Int = UNINITIALIZED_PORT
protected actual var port: Int
get() {
check(_port != UNINITIALIZED_PORT) { "Port is not initialized" }
return _port
}
set(_) {
error("Can't reassign port.")
}

protected actual var sslPort: Int = 0
protected actual var server: EmbeddedServer<TEngine, TConfiguration>? = null

protected actual var enableHttp2: Boolean = false
protected actual var enableSsl: Boolean = false
protected actual var enableCertVerify: Boolean = false

@OptIn(DelicateCoroutinesApi::class)
@AfterTest
fun tearDownBase() {
GlobalScope.launch { server?.stopSuspend(gracePeriodMillis = 0, timeoutMillis = 500) }
}

protected actual suspend fun createAndStartServer(
log: Logger?,
parent: CoroutineContext,
Expand All @@ -56,7 +81,7 @@ actual constructor(
return server
}

server.stop(1L, 1L)
server.stopSuspend(1L, 1L)
}

error(lastFailures)
Expand All @@ -67,7 +92,6 @@ actual constructor(
parent: CoroutineContext = EmptyCoroutineContext,
module: Application.() -> Unit
): EmbeddedServer<TEngine, TConfiguration> {
val _port = this.port
val environment = applicationEnvironment {
val delegate = KtorSimpleLogger("io.ktor.test")
this.log = log ?: object : Logger by delegate {
Expand All @@ -88,7 +112,10 @@ actual constructor(
}

return embeddedServer(applicationEngineFactory, properties) {
connector { port = _port }
connector {
// the default port is zero, so that it will be automatically assigned when the server is started.
port = DEFAULT_PORT
}
shutdownGracePeriod = 1000
shutdownTimeout = 1000
}
Expand All @@ -101,7 +128,8 @@ actual constructor(
// we start it on the global scope because we don't want it to fail the whole test
// as far as we have retry loop on call side
val starting = GlobalScope.async {
server.start(wait = false)
server.startSuspend(wait = false)
_port = server.engine.resolvedConnectors().first().port
delay(500)
}

Expand Down

0 comments on commit ba4d001

Please sign in to comment.