jvmMain.aws.smithy.kotlin.runtime.http.engine.crt.SdkStreamResponseHandler.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of http-client-engine-crt-jvm Show documentation
Show all versions of http-client-engine-crt-jvm Show documentation
HTTP client engine backed by CRT
The newest version!
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package aws.smithy.kotlin.runtime.http.engine.crt
import aws.sdk.kotlin.crt.http.*
import aws.sdk.kotlin.crt.io.Buffer
import aws.smithy.kotlin.runtime.http.HeadersBuilder
import aws.smithy.kotlin.runtime.http.HttpBody
import aws.smithy.kotlin.runtime.http.HttpStatusCode
import aws.smithy.kotlin.runtime.http.isInformational
import aws.smithy.kotlin.runtime.http.response.HttpResponse
import aws.smithy.kotlin.runtime.io.SdkBuffer
import aws.smithy.kotlin.runtime.io.SdkByteChannel
import aws.smithy.kotlin.runtime.io.SdkByteReadChannel
import aws.smithy.kotlin.runtime.telemetry.logging.logger
import aws.smithy.kotlin.runtime.util.derivedName
import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlin.coroutines.CoroutineContext
/**
* Implements the CRT stream response interface which proxies the response from the CRT to the SDK
* @param conn The HTTP connection used to make the request. Will be closed when the response handler completes
*/
@OptIn(DelicateCoroutinesApi::class)
internal class SdkStreamResponseHandler(
private val conn: HttpClientConnection,
private val callContext: CoroutineContext,
) : HttpStreamResponseHandler {
// TODO - need to cancel the stream when the body is closed from the caller side early.
// There is no great way to do that currently without either (1) closing the connection or (2) throwing an
// exception from a callback such that AWS_OP_ERROR is returned. Wait for HttpStream to have explicit cancellation
private val logger = callContext.logger()
private val responseReady = Channel(1)
private val headers = HeadersBuilder()
// in practice only WINDOW_SIZE bytes will ever be in-flight
private val bodyChan = Channel(Channel.UNLIMITED)
private val lock = reentrantLock() // protects crtStream and cancelled state
private var crtStream: HttpStream? = null
// if the (coroutine) job is completed before the stream's onResponseComplete callback is
// invoked (for any reason) we consider the stream "cancelled"
private var cancelled = false
private val Int.isMainHeadersBlock: Boolean
get() = when (this) {
HttpHeaderBlock.MAIN.blockType -> true
else -> false
}
private var streamCompleted = false
/**
* Called by the response read channel as data is consumed
* @param size the number of bytes consumed
*/
private fun onDataConsumed(size: Int) {
lock.withLock {
crtStream?.incrementWindow(size)
}
}
override fun onResponseHeaders(
stream: HttpStream,
responseStatusCode: Int,
blockType: Int,
nextHeaders: List?,
) {
if (!blockType.isMainHeadersBlock) return
nextHeaders?.forEach {
headers.append(it.name, it.value)
}
}
private fun createHttpResponseBody(contentLength: Long?): HttpBody {
val ch = SdkByteChannel(true)
val writerContext = callContext + callContext.derivedName("response-body-writer")
val job = GlobalScope.launch(writerContext) {
val result = runCatching {
for (buffer in bodyChan) {
val wc = buffer.size.toInt()
ch.write(buffer)
// increment window
onDataConsumed(wc)
}
}
// immediately close when done to signal end of body stream
ch.close(result.exceptionOrNull())
}
job.invokeOnCompletion { cause ->
// close is idempotent, if not previously closed then close with cause
ch.close(cause)
}
return object : HttpBody.ChannelContent() {
override val contentLength: Long? = contentLength
override fun readFrom(): SdkByteReadChannel = ch
}
}
// signal response ready and engine can proceed (all that is required is headers, body is consumed asynchronously)
private fun signalResponse(stream: HttpStream) {
// already signalled
if (responseReady.isClosedForSend) return
val transferEncoding = headers["Transfer-Encoding"]?.lowercase()
val chunked = transferEncoding == "chunked"
val contentLength = headers["Content-Length"]?.toLong()
val status = HttpStatusCode.fromValue(stream.responseStatusCode)
val hasBody = ((contentLength != null && contentLength > 0) || chunked) &&
(status !in listOf(HttpStatusCode.NotModified, HttpStatusCode.NoContent)) &&
!status.isInformational()
val body = when (hasBody) {
false -> HttpBody.Empty
true -> createHttpResponseBody(contentLength)
}
val resp = HttpResponse(
status,
headers.build(),
body,
)
val result = responseReady.trySend(resp)
check(result.isSuccess) { "signalling response failed, result was: ${result.exceptionOrNull()}" }
responseReady.close()
}
override fun onResponseHeadersDone(stream: HttpStream, blockType: Int) {
if (!blockType.isMainHeadersBlock) return
signalResponse(stream)
}
override fun onResponseBody(stream: HttpStream, bodyBytesIn: Buffer): Int {
val isCancelled = lock.withLock {
crtStream = stream
cancelled
}
// short circuit, stop buffering data and discard remaining incoming bytes
if (isCancelled) {
crtStream?.close()
stream.close()
return bodyBytesIn.len
}
val buffer = SdkBuffer().apply {
val bytes = bodyBytesIn.readAll()
write(bytes)
}
bodyChan.trySend(buffer).getOrThrow()
// explicit window management is handled by `onDataConsumed` as data is read from the channel
return 0
}
override fun onResponseComplete(stream: HttpStream, errorCode: Int) {
// stream is only valid until the end of this callback, ensure any further data being read downstream
// doesn't call incrementWindow on a resource that has been free'd
lock.withLock {
crtStream?.close()
crtStream = null
streamCompleted = true
}
stream.close()
// close the body channel
if (errorCode != 0) {
val ex = crtException(errorCode)
responseReady.close(ex)
bodyChan.close(ex)
} else {
// closing the channel to indicate no more data will be sent
bodyChan.close()
// ensure a response was signalled (will close the channel on it's own if it wasn't already sent)
signalResponse(stream)
}
}
internal suspend fun waitForResponse(): HttpResponse =
responseReady.receive()
/**
* Invoked only after the consumer is finished with the response and it is safe to cleanup resources
*/
internal fun complete() {
// We have no way of cancelling the stream, we have to drive it to exhaustion OR close the connection.
// At this point we know it's safe to release resources so if the stream hasn't completed yet
// we forcefully shutdown the connection. This can happen when the stream's window is full and it's waiting
// on the window to be incremented to proceed (i.e. the user didn't consume the stream for whatever reason
// and more data is pending arrival). It can also happen if the coroutine for this request is cancelled
// before onResponseComplete fires.
lock.withLock {
val forceClose = !streamCompleted
if (forceClose) {
logger.debug { "stream did not complete before job, forcing connection shutdown! handler=$this; conn=$conn; conn.id=${conn.id}; stream=$crtStream" }
conn.shutdown()
cancelled = true
}
logger.trace { "Closing connection ${conn.id}" }
// return to pool
conn.close()
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy