Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: correctly handle async cancellation of call context in OkHttp engine #1063

Merged
merged 5 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changes/289fe06b-6d65-47da-a007-360398c39244.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"id": "289fe06b-6d65-47da-a007-360398c39244",
"type": "bugfix",
"description": "Correctly handle async cancellation of call context in OkHttp engine",
"issues": [
"awslabs/smithy-kotlin#1061"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,16 @@ public class OkHttpEngine(
val engineCall = client.newCall(engineRequest)
val engineResponse = mapOkHttpExceptions { engineCall.executeAsync() }

callContext.job.invokeOnCompletion {
engineResponse.body.close()
}

val response = engineResponse.toSdkResponse()
val requestTime = Instant.fromEpochMilliseconds(engineResponse.sentRequestAtMillis)
val responseTime = Instant.fromEpochMilliseconds(engineResponse.receivedResponseAtMillis)

return OkHttpCall(request, response, requestTime, responseTime, callContext, engineCall)
return OkHttpCall(request, response, requestTime, responseTime, callContext, engineCall).also { call ->
callContext.job.invokeOnCompletion { error ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(naming): error -> cause to match documentation?

if (error != null) call.cancelInFlight()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correctness: you can receive a non-null CancellationException which is not meant to be treated as an error. do we still want to cancel the call in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we definitely want to cancel when we get a CancellationException. We're bridging two contexts here so we want to ensure resources belonging to the engine are cleaned up when the call context is cancelled. I'll leave a comment to that effect for future readers.

engineResponse.body.close()
}
}
}

override fun shutdown() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,45 @@ class AsyncStressTest : AbstractEngineTest() {
assertEquals(engineJobsBefore.size, engineJobsAfter.size, message)
}
}

@Test
fun testJobCancellation() = testEngines {
// https://github.com/smithy-lang/smithy-kotlin/issues/1061

test { _, client ->
val req = HttpRequest {
testSetup()
url.path.decoded = "slow"
}

// Expect CancellationException because we're cancelling
assertFailsWith<CancellationException> {
coroutineScope {
val parentScope = this

println("Invoking call on ctx $coroutineContext")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove println

val call = client.call(req)

val bytes = async {
delay(100.milliseconds)
println("Body of type ${call.response.body} on ctx $coroutineContext")
try {
call.response.body.readAll()
} catch (e: Throwable) {
// IllegalStateException: "Unbalanced enter/exit" will be thrown if body closed improperly
assertIsNot<IllegalStateException>(e)
null
}
}

val cancellation = async {
delay(400.milliseconds)
parentScope.cancel("Cancelling!")
}

awaitAll(bytes, cancellation)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package aws.smithy.kotlin.runtime.http.test.suite
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.milliseconds

internal fun Application.concurrentTests() {
routing {
Expand All @@ -18,5 +20,17 @@ internal fun Application.concurrentTests() {
call.respondText(text.repeat(respSize / text.length))
}
}

route("slow") {
get {
val chunk = ByteArray(256) { it.toByte() }
call.respondOutputStream {
repeat(10) {
delay(200.milliseconds)
write(chunk)
}
}
}
}
}
}
Loading