Skip to content

Commit

Permalink
fixup! KTOR-6734 Fixes for Jetty 12 upgrade
Browse files Browse the repository at this point in the history
  • Loading branch information
bjhham committed Feb 28, 2025
1 parent 03668de commit d014b91
Showing 24 changed files with 489 additions and 258 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.jetty.jakarta

import kotlinx.io.IOException
import org.eclipse.jetty.util.Callback
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
* Jetty works with a `Callback` type which succeeds or fails similar to a coroutine continuation.
*/
internal fun Continuation<Unit>.asCallback(): Callback = object : Callback {
override fun failed(x: Throwable?) {
resumeWithException(x ?: IOException("Failed with no exception"))
}
override fun succeeded() {
resume(Unit)
}
}
Original file line number Diff line number Diff line change
@@ -9,20 +9,24 @@ import io.ktor.server.engine.*
import io.ktor.utils.io.*
import org.eclipse.jetty.server.Request
import org.eclipse.jetty.server.Response
import java.util.concurrent.Executor
import kotlin.coroutines.CoroutineContext

@InternalAPI
public class JettyApplicationCall(
application: Application,
request: Request,
response: Response,
engineExecutor: Executor,
engineDispatcher: CoroutineContext,
appDispatcher: CoroutineContext,
override val coroutineContext: CoroutineContext
) : BaseApplicationCall(application) {

override val request: JettyApplicationRequest =
JettyApplicationRequest(this, request)
override val response: JettyApplicationResponse =
JettyApplicationResponse(this, request, response, coroutineContext)
JettyApplicationResponse(this, request, response, engineExecutor, engineDispatcher, appDispatcher)

init {
putResponseAttribute()
Original file line number Diff line number Diff line change
@@ -71,7 +71,8 @@ public open class JettyApplicationEngineBase(
configuration.shutdownTimeout
)

val connectors = server.connectors.zip(configuration.connectors)
val connectors = server.connectors
.zip(configuration.connectors)
.map { it.second.withPort((it.first as ServerConnector).localPort) }
resolvedConnectorsDeferred.complete(connectors)

Original file line number Diff line number Diff line change
@@ -9,21 +9,68 @@ import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.request.*
import io.ktor.utils.io.*
import io.ktor.utils.io.jvm.javaio.*
import org.eclipse.jetty.io.*
import org.eclipse.jetty.server.*
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.io.EOFException
import kotlinx.io.IOException
import org.eclipse.jetty.server.Request
import kotlin.coroutines.resume

@InternalAPI
public class JettyApplicationRequest(
call: PipelineCall,
request: Request
) : BaseApplicationRequest(call) {

// See https://jetty.org/docs/jetty/12/programming-guide/arch/io.html#content-source
private val requestBodyJob: WriterJob =
call.writer(Dispatchers.IO + CoroutineName("request-reader")) {
val contentLength = if (request.headers.contains(HttpHeaders.ContentLength)) {
request.headers.get(HttpHeaders.ContentLength)?.toLong()
} else null

var bytesRead = 0L
while (true) {
when(val chunk = request.read()) {
// nothing available, suspend for more content
null -> {
suspendCancellableCoroutine { continuation ->
request.demand { continuation.resume(Unit) }
}
}
// read the chunk, exit and close channel if last chunk or failure
else -> {
with(chunk) {
if (failure != null) {
if (isLast)
throw failure
call.application.log.warn("Recoverable error reading request body; continuing", failure)
} else {
bytesRead += byteBuffer.remaining()
channel.writeFully(byteBuffer)
release()
if (contentLength != null && bytesRead > contentLength) {
channel.cancel(IOException("Request body exceeded content length limit"))
}
if (isLast) {
if (contentLength != null && bytesRead < contentLength) {
channel.cancel(EOFException("Expected $contentLength bytes, received only $bytesRead"))
}
return@writer
}
}
}
}
}
}
}

override val cookies: RequestCookies = JettyRequestCookies(this, request)

override val engineHeaders: Headers = JettyHeaders(request)

override val engineReceiveChannel: ByteReadChannel = Content.Source.asInputStream(request).toByteReadChannel()
override val engineReceiveChannel: ByteReadChannel by lazy { requestBodyJob.channel }

override val local: RequestConnectionPoint = JettyConnectionPoint(request)

Original file line number Diff line number Diff line change
@@ -11,26 +11,23 @@ import io.ktor.server.engine.*
import io.ktor.server.response.*
import io.ktor.utils.io.*
import io.ktor.utils.io.pool.*
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.*
import kotlinx.io.InternalIoApi
import org.eclipse.jetty.server.Request
import org.eclipse.jetty.server.Response
import org.eclipse.jetty.util.Callback
import java.nio.ByteBuffer
import java.nio.ByteBuffer.allocate
import java.util.concurrent.Executor
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

@InternalAPI
public class JettyApplicationResponse(
call: PipelineCall,
private val request: Request,
private val response: Response,
override val coroutineContext: CoroutineContext
private val executor: Executor,
override val coroutineContext: CoroutineContext,
private val userContext: CoroutineContext,
) : BaseApplicationResponse(call), CoroutineScope {
private companion object {
private val bufferPool = ByteBufferPool(bufferSize = 8192)
@@ -48,42 +45,33 @@ public class JettyApplicationResponse(
}
}

@OptIn(InternalCoroutinesApi::class)
@OptIn(InternalCoroutinesApi::class, InternalIoApi::class)
private val responseJob: Lazy<ReaderJob> = lazy {
reader(Dispatchers.IO + coroutineContext + CoroutineName("response-writer")) {
var count = 0
val buffer = bufferPool.borrow()
try {
while(true) {
when(val current = channel.readAvailable(buffer)) {
while (true) {
when (val current = channel.readAvailable(buffer)) {
-1 -> break
0 -> {
channel.awaitContent()
continue
}
0 -> continue
else -> count += current
}

suspendCoroutine<Unit> { continuation ->
try {
response.write(
channel.isClosedForRead,
buffer.flip(),
Callback.from {
buffer.flip()
continuation.resume(Unit)
}
)
} catch (cause: Throwable) {
continuation.resumeWithException(cause)
}
suspendCancellableCoroutine { continuation ->
response.write(
channel.isClosedForRead,
buffer.flip(),
continuation.asCallback()
)
}
buffer.compact()
}
} finally {
bufferPool.recycle(buffer)
runCatching {
if (!response.isCommitted)
response.write(true, allocate(0), Callback.NOOP)
response.write(true, emptyBuffer, Callback.NOOP)
}
}
}
@@ -98,23 +86,37 @@ public class JettyApplicationResponse(
override fun getEngineHeaderValues(name: String): List<String> = response.headers.getValuesList(name)
}

// TODO set idle timeout from websocket config on endpoint
override suspend fun respondUpgrade(upgrade: OutgoingContent.ProtocolUpgrade) {
val connection = request.connectionMetaData.connection
val endpoint = connection.endPoint
if (responseJob.isInitialized())
responseJob.value.cancel()

// use the underlying endpoint instance for two-way connection
val endpoint = request.connectionMetaData.connection.endPoint
endpoint.idleTimeout = 6000 * 1000

val websocketConnection = JettyWebsocketConnection(endpoint, coroutineContext)
response.write(true, allocate(0), Callback.from { endpoint.upgrade(websocketConnection) })
val websocketConnection = JettyWebsocketConnection2(
endpoint,
executor,
bufferPool,
coroutineContext
)

suspendCancellableCoroutine { continuation ->
response.write(true, emptyBuffer, continuation.asCallback())
}

endpoint.upgrade(websocketConnection)

val upgradeJob = upgrade.upgrade(
websocketConnection.inputChannel,
websocketConnection.outputChannel,
coroutineContext,
coroutineContext,
userContext,
)

upgradeJob.invokeOnCompletion {
websocketConnection.inputChannel.close()
websocketConnection.inputChannel.cancel()
websocketConnection.outputChannel.close()
}

@@ -126,7 +128,7 @@ public class JettyApplicationResponse(
}

override suspend fun respondNoContent(content: OutgoingContent.NoContent) {
response.write(true, allocate(0), Callback.NOOP)
response.write(true, emptyBuffer, Callback.NOOP)
}

override suspend fun responseChannel(): ByteWriteChannel =
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.server.jetty.jakarta

import io.ktor.utils.io.*
import io.ktor.utils.io.pool.*
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import org.eclipse.jetty.io.EndPoint
import kotlin.coroutines.CoroutineContext

public fun CoroutineScope.connect(
endpoint: EndPoint,
coroutineNamePrefix: String,
bufferPool: ByteBufferPool,
coroutineContext: CoroutineContext
): Pair<WriterJob, ReaderJob> {
val inputJob: WriterJob =
writer(Dispatchers.IO + coroutineContext + CoroutineName("$coroutineNamePrefix-input")) {
val buffer = bufferPool.borrow()
try {
while (true) {
suspendCancellableCoroutine { continuation ->
endpoint.tryFillInterested(continuation.asCallback())
}
when (endpoint.fill(buffer)) {
-1 -> break
0 -> continue
else -> {}
}
channel.writeFully(buffer.flip())
buffer.compact()
}
} finally {
bufferPool.recycle(buffer)
}
}

val outputJob: ReaderJob =
reader(Dispatchers.IO + coroutineContext + CoroutineName("$coroutineNamePrefix-output")) {
val buffer = bufferPool.borrow()
try {
while (true) {
when (channel.readAvailable(buffer)) {
-1 -> break
0 -> continue
else -> {}
}
suspendCancellableCoroutine<Unit> { continuation ->
endpoint.write(continuation.asCallback(), buffer.flip())
}
buffer.compact()
}
} finally {
bufferPool.recycle(buffer)
}
}

return inputJob to outputJob
}
Loading
Oops, something went wrong.

0 comments on commit d014b91

Please sign in to comment.