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

feat: support default checksums #1191

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open

feat: support default checksums #1191

wants to merge 29 commits into from

Conversation

0marperez
Copy link
Contributor

Issue #

Description of changes

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@0marperez 0marperez added the no-changelog Indicates that a changelog entry isn't required for a pull request. Use sparingly. label Nov 27, 2024

This comment has been minimized.

1 similar comment

This comment has been minimized.

@0marperez
Copy link
Contributor Author

Note to reviewers: I'll address the breaking API changes and failing protocol issues soon. In the meantime, I’d appreciate a review of the checksum-related changes.

Comment on lines 29 to 31
* Calculates a request's checksum.
*
* If the checksum will be sent as a header, calculate the checksum.
* If a user supplies a checksum via an HTTP header no calculation will be done. The exception is MD5, if a user
Copy link
Contributor

Choose a reason for hiding this comment

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

correctness/clarification: "Calculates a request's checksum" and "If a user supplies a checksum via an HTTP header no calculation will be done." conflict with each other

*
* @param requestChecksumRequired Model sourced flag indicating if checksum calculation is mandatory.
* @param requestChecksumCalculation Configuration option that determines when checksum calculation should be done.
* @param userSelectedChecksumAlgorithm The checksum algorithm that the user selected for the request, may be null.
Copy link
Contributor

Choose a reason for hiding this comment

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

naming: requestChecksumAlgorithm

private val forcedToCalculateChecksum = requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED
private val checksumHeader = StringBuilder("x-amz-checksum-")
private val defaultChecksumAlgorithm = lazy { Crc32() }
private val defaultChecksumAlgorithmHeaderPostfix = "crc32"
Copy link
Contributor

Choose a reason for hiding this comment

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

Postfix -> Suffix

* The header must start with "x-amz-checksum-" followed by the checksum algorithm's name.
* MD5 is not considered a valid checksum algorithm.
*/
private fun userProviderChecksumHeader(request: HttpRequest, logger: Logger): String? {
Copy link
Contributor

Choose a reason for hiding this comment

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

style: can be restructured as an extension function HttpRequest.checksumHeader(logger: Logger): String?

and naming: userProvidedChecksumHeader

* Users can check which checksum was validated by referencing the `ResponseChecksumValidated` execution context variable.
*
* @param shouldValidateResponseChecksumInitializer A function which uses the input [I] to return whether response checksum validation should occur
* @param responseValidationRequired Flag indicating if the checksum validation is mandatory.
Copy link
Contributor

Choose a reason for hiding this comment

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

docs: "Model sourced flag"

.removePrefix("x-amz-checksum-")
.toHashFunction() ?: throw ClientException("Could not parse checksum algorithm from header $checksumHeader")

if (context.protocolResponse.body is HttpBody.Bytes) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this branch added? The spec says we should delay checksum calculation until the body is consumed:

Where possible, SDKs MUST defer this calculation and validation until the payload is actually consumed by the user i.e. a payload must not be read twice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

toHashingBody doesn't support Bytes bodies. I decided to calculate the checksum in memory instead of adding support for Bytes to toHashingBody. It should be safe if the request is not being streamed but I think you're right that we're not following the spec

Copy link
Contributor

@lauzadis lauzadis Nov 27, 2024

Choose a reason for hiding this comment

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

toHashingBody doesn't support Bytes bodies

Does it need to? The previous implementation never seemed to need it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it does, the spec has some unit tests with non-streaming bodies

Copy link
Contributor Author

@0marperez 0marperez Dec 2, 2024

Choose a reason for hiding this comment

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

Discussed offline and we'll be removing support for HttpBody.Bytes from FlexibleChecksumsResponseInterceptor because HTTP bodies are never bytes. Source

Copy link
Contributor

Choose a reason for hiding this comment

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

I think removing support for HttpBody.Bytes is a bad idea. Our current implementations of OkHttp and CRT engines do not return bytes, that's true, but future implementations or other engines wrapped by users may. In addition, interceptors may be used to rewrite response bodies by streaming them into memory, processing them, and then rewriting the body in a new form (possibly HttpBody.Bytes). I think we need to ensure each type of HttpBody is handled correctly.

@@ -126,23 +133,6 @@ class FlexibleChecksumsRequestInterceptorTest {
assertEquals(0, call.request.headers.getNumChecksumHeaders())
}

@Test
fun itSetsChecksumHeaderViaExecutionContext() = runTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this test removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're no longer setting the checksum header via execution context

@@ -163,29 +168,4 @@ class FlexibleChecksumsResponseInterceptorTest {

op.roundTrip(client, TestInput("input"))
}

@Test
fun testSkipsValidationWhenDisabled() = runTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why was this test removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be covered under ChecksumConfigTest > ResponseChecksumValidation but I added it back


public enum class HttpChecksumConfigOption {
/**
* SDK will create/validate checksum if the service marks it as required or if this is set.
Copy link
Contributor

Choose a reason for hiding this comment

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

docs: create -> calculate

Copy link
Contributor

Choose a reason for hiding this comment

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

correctness: missing tests for when requestChecksumRequired = false and requestChecksumCalculation = HttpChecksumConfigOption.WHEN_REQUIRED. Same for response validation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The request test cases are already here. I think the response validation unit tests need one more to be exhaustive.

Copy link
Contributor

Choose a reason for hiding this comment

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

Those tests in ChecksumConfigTest.kt aren't validating interceptor behavior like the tests in this file do.

* @param checksumAlgorithmNameInitializer an optional function which parses the input [I] to return the checksum algorithm name.
* if not set, then the [HttpOperationContext.ChecksumAlgorithm] execution context attribute will be used.
* If the request will be streamed:
* - The checksum calculation is done asynchronously using a hashing & completing body.
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "asynchronously" → "during transmission"

* - The checksum will be sent in a trailing header, once the request is consumed.
*
* If the request will not be streamed:
* - The checksum calculation is done synchronously
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "synchronously" → "before transmission"

Comment on lines 60 to 75
private val defaultChecksumAlgorithm = lazy { Crc32() }
private val defaultChecksumAlgorithmHeaderPostfix = "crc32"

private val checksumAlgorithm = userSelectedChecksumAlgorithm?.let {
val hashFunction = userSelectedChecksumAlgorithm.toHashFunction()
if (hashFunction == null || !hashFunction.isSupported) {
throw ClientException("Checksum algorithm '$userSelectedChecksumAlgorithm' is not supported for flexible checksums")
}
checksumHeader.append(userSelectedChecksumAlgorithm.lowercase())
hashFunction
} ?: if (forcedToCalculateChecksum) {
checksumHeader.append(defaultChecksumAlgorithmHeaderPostfix)
defaultChecksumAlgorithm.value
} else {
null
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Seems unnecessary to declare forcedToCalculateChecksum, defaultChecksumAlgorithm, and defaultChecksumAlgorithmHeaderPostfix as fields only to use them in one if branch. Could this just be:

?: if (requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED) {
    checksumHeader.append("crc32")
    Crc32()
} else

Comment on lines 80 to 81
userProviderChecksumHeader(context.protocolRequest, logger)?.let {
logger.debug { "User supplied a checksum via header, skipping checksum calculation" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Style: These log messages which talk about the user feel awkward. Log messages are for users so it seems strange to mention the user in the third person. I'd suggest rewording these to be more about data/actions and less about the actors involved (e.g., "Found a provided checksum in the request, skipping checksum calculation").


deferredChecksum.complete(checksum)
} else {
logger.debug { "Calculating checksum asynchronously" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: We've lost the log message that we're calculating a checksum asynchronously during transmission. I'd suggest moving your "Calculating checksum using '$checksumAlgorithm'" message into the if/else clauses, tweaking each to mention whether the checksum is being calculated before or during transmission.

Comment on lines 159 to 163
private fun String.isCompositeChecksum(): Boolean {
// Ends with "-#" where "#" is a number between 1-1000
val regex = Regex("-([1-9][0-9]{0,2}|1000)$")
return regex.containsMatchIn(this)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: When this logic is made S3 specific, we shouldn't assert on the part number fitting inside a certain range. Just "-(\d)+$" should suffice.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Please add tests for HttpChecksumConfigOption.WHEN_REQUIRED. Applies to response interceptors as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The request test cases are already here. I think the response validation unit tests need one more to be exhaustive.

Comment on lines -129 to -144
@Test
fun itSetsChecksumHeaderViaExecutionContext() = runTest {
checksums.forEach { (checksumAlgorithmName, expectedChecksumValue) ->
val req = HttpRequestBuilder().apply {
body = HttpBody.fromBytes("<Foo>bar</Foo>".encodeToByteArray())
}

val op = newTestOperation<Unit, Unit>(req, Unit)
op.context[HttpOperationContext.ChecksumAlgorithm] = checksumAlgorithmName
op.interceptors.add(FlexibleChecksumsRequestInterceptor<Unit>())

op.roundTrip(client, Unit)
val call = op.context.attributes[HttpOperationContext.HttpCallList].first()
assertEquals(expectedChecksumValue, call.request.headers["x-amz-checksum-$checksumAlgorithmName"])
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Do we still use HttpOperationContext.ChecksumAlgorithm or can it be deprecated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it's still used for MD5 checksums in the httpChecksumRequiredTrait

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Update on this, it's no longer used anywhere so we can deprecate/remove it

Comment on lines 167 to 190
@Test
fun testSkipsValidationWhenDisabled() = runTest {
val req = HttpRequestBuilder()
val op = newTestOperation<TestInput>(req)

op.interceptors.add(
FlexibleChecksumsResponseInterceptor<TestInput> {
false
},
)

val responseChecksumHeaderName = "x-amz-checksum-crc32"

val responseHeaders = Headers {
append(responseChecksumHeaderName, "incorrect-checksum-would-throw-if-validated")
}

val client = getMockClient(response, responseHeaders)

val output = op.roundTrip(client, TestInput("input"))
output.body.readAll()

assertNull(op.context.getOrNull(ChecksumHeaderValidated))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Why did we delete this test?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be covered under ChecksumConfigTest > ResponseChecksumValidation but I added it back

Comment on lines 31 to 34
/**
* SDK will create/validate checksum if the service marks it as required or if this is set.
*/
WHEN_SUPPORTED,
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: "or if this is set" → "or if the service offers optional checksums"

This comment has been minimized.

This comment has been minimized.

This comment has been minimized.

@0marperez 0marperez marked this pull request as ready for review December 4, 2024 20:53
@0marperez 0marperez requested a review from a team as a code owner December 4, 2024 20:53

This comment has been minimized.

This comment has been minimized.

Comment on lines 80 to 81
}
private val checksumAlgorithm = requestChecksumAlgorithm?.let {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Missing vertical whitespace

Comment on lines 81 to 91
private val checksumAlgorithm = requestChecksumAlgorithm?.let {
val hashFunction = requestChecksumAlgorithm.toHashFunction()
if (hashFunction == null || !hashFunction.isSupported) {
throw ClientException("Checksum algorithm '$requestChecksumAlgorithm' is not supported for flexible checksums")
}
hashFunction
} ?: if (requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED) {
Crc32()
} else {
null
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Could be simplified a bit:

private val checksumAlgorithm = requestChecksumAlgorithm
    ?.let { it.toHashFunction() ?: throw ClientException("Checksum algorithm '$it' is not supported for flexible checksums") }
    ?.takeIf { it.isSupported }
    ?: (Crc32().takeIf { requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED })

Or even more by extracting the throwing logic to a helper function:

// in HashFunction.kt
public fun String.toHashFunctionOrThrow(): String =
    toHashFunction() ?: throw ClientException("Checksum algorithm '$this' is not supported for flexible checksums")

// in FlexibleChecksumsRequestInterceptor.kt
private val checksumAlgorithm = requestChecksumAlgorithm
    ?.toHashFunctionOrThrow()
    ?.takeIf { it.isSupported }
    ?: (Crc32().takeIf { requestChecksumRequired || requestChecksumCalculation == HttpChecksumConfigOption.WHEN_SUPPORTED })

Comment on lines 120 to 127
request.body = request.body
.toHashingBody(
checksumAlgorithm,
request.body.contentLength,
)
.toCompletingBody(
deferredChecksum,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Unnecessary wrapping

.toHashingBody(checksumAlgorithm, req.body.contentLength)
.toCompletingBody(deferredChecksum)
}
context.executionContext.emitBusinessMetric(checksumAlgorithm.toBusinessMetric())
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: I see here we're emitting the metric for the specific algorithm (e.g., FLEXIBLE_CHECKSUMS_REQ_CRC32C). Where are we emitting the metric for request/response enablement mode (e.g., FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We're emitting those metrics in the SDK side. Here and here.

Comment on lines 261 to 275
private fun HttpRequest.userProvidedChecksumHeader(logger: Logger): String? {
this.headers.entries().forEach { header ->
val headerName = header.key.lowercase()
if (headerName.startsWith("x-amz-checksum-")) {
if (headerName == "x-amz-checksum-md5") {
logger.debug {
"MD5 checksum was supplied via header, MD5 is not a supported algorithm, ignoring header"
}
} else {
return headerName
}
}
}
return null
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Could be simplified:

private fun HttpRequest.userProvidedChecksumHeader(logger: Logger) = headers
    .names()
    .filter { it.startsWith("x-amz-checksum-", ignoreCase = true) }
    .filterNot { key -> key
        .equals("x-amz-checksum-md5", ignoreCase = true)
        .also { if (it) logger.debug { "MD5 checksum was supplied via header, MD5 is not a supported algorithm, ignoring header" } }
    }
    .firstOrNull()
}

Comment on lines 151 to 160

/**
* Verifies if a checksum is composite.
* Composite checksums are used for multipart uploads.
*/
private fun String.isCompositeChecksum(): Boolean {
// Ends with "-#" where "#" is a number
val regex = Regex("-(\\d)+$")
return regex.containsMatchIn(this)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Correctness: This customization should be specific to S3 only. Any other service which returns a checksum with -# in it should be handled as normal (likely by failing validation). We should move this logic/check to aws-sdk-kotlin in a customization that only applies to S3.

Copy link
Contributor

Choose a reason for hiding this comment

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

After reading more of the aws-sdk-kotlin PR and re-reading this interceptor, I can see now that this composite checksum usage is gated by a constructor flag. This works but it still seems too specific to a non-smithy-kotlin concern to me. I'd be interested in seeing if this logic can be completely removed from smithy-kotlin in favor of something more additive in aws-sdk-kotlin.

Copy link
Contributor

Choose a reason for hiding this comment

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

Those tests in ChecksumConfigTest.kt aren't validating interceptor behavior like the tests in this file do.

@@ -26,7 +26,7 @@ import kotlinx.coroutines.job
import kotlin.coroutines.coroutineContext

/**
* Calculates a request's checksum.
* Handles request checksums.
Copy link
Contributor

Choose a reason for hiding this comment

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

clarification: "Handles request checksums for operations with the [HttpChecksumTrait] applied"


private val checksumAlgorithm = userSelectedChecksumAlgorithm?.let {
val hashFunction = userSelectedChecksumAlgorithm.toHashFunction()
// FIXME: Remove in next minor version bump
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add these to our internal ticket SDK-KT-385

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, SDK-KT-385 seems to be more for SDK changes. I can create a ticket for the smith kotlin version bump since it seems like we'll do a minor version bump for the SDK soon and one for smithy kotlin later at a different time

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm pretty sure we will bump both at the same time

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, in that case I'll just fix the FIXMEs since this should go into a v1.4 branch. I have to update the PR.

private val responseValidationRequired: Boolean,
private val responseChecksumValidation: HttpChecksumConfigOption?,
private val serviceUsesCompositeChecksums: Boolean,
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing KDocs

correctness (naming): operationUsesCompositeChecksums

private val responseValidationRequired: Boolean,
private val responseChecksumValidation: HttpChecksumConfigOption?,
private val serviceUsesCompositeChecksums: Boolean,
Copy link
Contributor

Choose a reason for hiding this comment

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

question: Why do we need this extra flag to tell if the operation might send back a composite checksum? Why can't we just rely on serviceChecksumValue.isCompositeChecksum()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ian left a comment above, composite checksums are SDK specific so they should be handled downstream. I was using a flag to check for them but my most recent (local) changes remove this option and handle composite checksums completely in the SDK.

This comment has been minimized.

@@ -60,7 +64,7 @@ structure TestStruct {
@required
nestedListValue: NestedList

@required
// @required
Copy link
Contributor

Choose a reason for hiding this comment

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

Were these @required removed to fix some protocol tests? I'd rather leave them disabled than change our test models

Copy link
Contributor Author

@0marperez 0marperez Dec 13, 2024

Choose a reason for hiding this comment

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

The required trait isn't supposed to be there, a smithy validator will throw an error if it is, even if I disable the protocol test

Comment on lines 338 to 341
/**
* Render optionally installing Md5ChecksumMiddleware.
* The Md5 middleware will only be installed if the operation requires a checksum and the user has not opted-in to flexible checksums.
* The Md5 middleware will only be installed if the operation requires a checksum.
*/
Copy link
Contributor

@lauzadis lauzadis Dec 13, 2024

Choose a reason for hiding this comment

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

question: Why is this installed for every operation now without consideration of flexible checksums? If we enable CRC32 by default, doesn't this also calculate the MD5 unnecessarily?


private val checksumHeader = buildString {
append("x-amz-checksum-")
append(requestChecksumAlgorithm?.lowercase() ?: "crc32")
Copy link
Contributor

Choose a reason for hiding this comment

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

style: "crc32" could be lifted to a const val SDK_DEFAULT_CHECKSUM_ALGORITHM for easier understanding

check(context.protocolRequest.body !is HttpBody.Empty) {
"Can't calculate the checksum of an empty body"
if (checksumAlgorithm == null) {
logger.debug { "A checksum algorithm isn't selected and checksum calculation isn't required, skipping checksum calculation" }
Copy link
Contributor

Choose a reason for hiding this comment

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

We could also fall into this case when a user does select a checksum algorithm but they also configure WHEN_REQUIRED.

I think a better log would be:
"A checksum algorithm isn't selected or checksum calculation isn't required, skipping checksum calculation"

)
val checksumAlgorithm = checksumHeader
.removePrefix("x-amz-checksum-")
.toHashFunction() ?: throw ClientException("Could not parse checksum algorithm from header $checksumHeader")
Copy link
Contributor

Choose a reason for hiding this comment

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

Should use toHashFunctionOrThrow here

.toChecksumValidatingBody(serviceChecksumValue),
)
}
else -> throw IllegalStateException("Http body type '$bodyType' is not supported for flexible checksums.")
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: capitalize HTTP or make it HttpBody

Comment on lines 178 to 183

/**
* Runtime accessible version of the [IllegalStateException]
*/
@InternalApi
public class IllegalStateException(message: String) : IllegalStateException(message)
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the purpose of this, why is not accessible from runtime already?

@@ -6,9 +6,13 @@
package aws.smithy.kotlin.runtime.text

import aws.smithy.kotlin.runtime.InternalApi
import java.util.*
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't use java imports in a common source set

This comment has been minimized.

This comment has been minimized.

Copy link

Affected Artifacts

Changed in size
Artifact Pull Request (bytes) Latest Release (bytes) Delta (bytes) Delta (percentage)
http-jvm.jar 108,391 103,477 4,914 4.75%
smithy-client-jvm.jar 65,382 62,681 2,701 4.31%
runtime-core-jvm.jar 816,527 812,469 4,058 0.50%
aws-signing-common-jvm.jar 66,779 66,491 288 0.43%
aws-signing-default-jvm.jar 52,520 52,433 87 0.17%
http-client-jvm.jar 314,579 314,811 -232 -0.07%

Comment on lines +28 to +29
// FIXME: Re-enable. This test is broken after a smithy update: https://github.com/smithy-lang/smithy/pull/2467
// ProtocolTest("aws-json-10", "aws.protocoltests.json10#JsonRpc10"),
Copy link
Contributor

Choose a reason for hiding this comment

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

FYI I know we talked about disabling some tests that are failing in Smithy 1.53.0, but these are disabling the entire test suite, which we don't want to do

@@ -32,7 +33,7 @@ import software.amazon.smithy.rulesengine.traits.EndpointTestsTrait
* shape's closure for example)
*/
@Suppress("EXTENSION_SHADOWED_BY_MEMBER")
inline fun <reified T : Shape> Model.shapes(): List<T> = shapes(T::class.java).toList()
inline fun <reified T : Shape> Model.shapes(): List<T> = shapes(T::class.java).kotlinToList()
Copy link
Contributor

Choose a reason for hiding this comment

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

This has never been a problem before, what changed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There was a refactor in AWS SDK Kotlin to make the codegen tests KMP, which led to the JVM CI checks failing because we've been using JVM 17. I set the JVM version in the tests to 1.8 and the toList function used above is only available since JVM 16

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok that makes sense. You should be able to use shapes(T::class.java).collect(Collectors.toList()) instead of hacking around with the Kotlin toList

import software.amazon.smithy.model.traits.HttpChecksumRequiredTrait

/**
* Handles the `httpChecksumRequired` trait.
Copy link
Contributor

Choose a reason for hiding this comment

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

nit/docs: Explain more about how it "handles" the trait

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The "meat" of the integration is the middleware and both of them have their own Kdocs. It feels like docs overkill to also add a summary of what each middleware is doing here

writer.write(
"op.context[#T.DefaultChecksumAlgorithm] = #S",
RuntimeTypes.HttpClient.Operation.HttpOperationContext,
"MD5",
Copy link
Contributor

Choose a reason for hiding this comment

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

question/correctness: Can/should we store this as a HashFunction rather than a String?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, I think we would have to initialize the HashFunction in the context and its seems slightly worse than what we have right now. Thoughts?

Comment on lines +34 to +39

/**
* @return The default checksum algorithm name, null if default checksums are disabled.
*/
internal fun defaultChecksumAlgorithmName(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): String? =
context.executionContext.getOrNull(HttpOperationContext.DefaultChecksumAlgorithm)
Copy link
Contributor

Choose a reason for hiding this comment

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

correctness: This seems like the wrong place to define this function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where would be better?

Copy link
Contributor

Choose a reason for hiding this comment

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

I would have said HttpChecksumRequiredInterceptor but I see it's also used by FlexibleChecksumsRequestInterceptor. I think it's fine here.

style: make it an extension val

internal val ProtocolRequestInterceptorContext<Any, HttpRequest>.defaultChecksumAlgorithmName: String?
    get() = executionContext.getOrNull(HttpOperationContext.DefaultChecksumAlgorithm)

Comment on lines +196 to +201
/**
* Convert an [HttpBody] with an underlying [HashingSource] or [HashingByteReadChannel]
* to a [CompletingSource] or [CompletingByteReadChannel], respectively.
*/
@InternalApi
public fun HttpBody.toCompletingBody(deferred: CompletableDeferred<String>): HttpBody = when (this) {
Copy link
Contributor

Choose a reason for hiding this comment

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

These used to be private, now they are @InternalApi public, meaning they are included in our API dumps and part of backwards compatibility. Was this move necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, I moved this here because I was going to add support for streaming checksums in the HttpCheksumsRequired interceptor

*/
@InternalApi
public val HashFunction.isSupportedForFlexibleChecksums: Boolean get() =
algorithmsSupportedForFlexibleChecksums.contains(this::class.simpleName)
Copy link
Contributor

Choose a reason for hiding this comment

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

correctness: Relying on this::class.simpleName seems unsafe.

Better solution:

@InternalApi
public val HashFunction.isSupportedForFlexibleChecksums: Boolean 
    get() = when (this) {
        is Crc32, is Crc32c, is Sha1, is Sha256 -> true
        else -> false
    }

Copy link
Contributor Author

@0marperez 0marperez Dec 19, 2024

Choose a reason for hiding this comment

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

Yeah agree, that's what we had before but we would have to keep track of what checksum algorithms are supported for flexible checksums in two places. In HashFunction.isSupportedForFlexibleChecksums & algorithmsSupportedForFlexibleChecksums.

I made a tradeoff here, it seems more risky to possibly miss updating what algorithms we support in both locations than relying on the class name. Plus we have unit tests that should lessen some of our concerns here

Copy link
Contributor

Choose a reason for hiding this comment

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

The only other use of algorithmsSupportedForFlexibleChecksums is in a log message. I'd rather remove the log message than keep this class reflection

Comment on lines +6 to +11
@InternalApi
public fun runBlocking(block: suspend () -> Unit) {
runBlocking {
block()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

question: What is this used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's so we can have runBlocking as a runtime type, it's used in the presigner generator

Copy link
Contributor

Choose a reason for hiding this comment

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

You can just add a new runBlocking type under the existing KotlinxCoroutines runtime type

@@ -0,0 +1,40 @@
package aws.smithy.kotlin.runtime.client.config
Copy link
Contributor

Choose a reason for hiding this comment

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

correctness: missing license at the top. same applies to Concurrency.kt

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I think we need to adjust our Ktlint for be more stringent when it comes to license headers

}
}

public enum class HttpChecksumConfigOption {
Copy link
Contributor

Choose a reason for hiding this comment

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

docs: missing KDocs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we can add a new rule to Ktlint about Kdocs? Not sure it that's possible but I think I'd be helpful

}
}

public enum class HttpChecksumConfigOption {
Copy link
Contributor

Choose a reason for hiding this comment

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

correctness: Right now request and response checksum configs both use this enum. In the future, their config options might diverge, which will force us to make an API break. I'd recommend splitting this into HttpChecksumRequestConfigOption and HttpChecksumResponseConfigOption now to get ahead of that.

Copy link
Contributor Author

@0marperez 0marperez Dec 19, 2024

Choose a reason for hiding this comment

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

It seems like just speculation for now, I say we only separate them if that time comes. Changing the type of this config option would be a breaking change for all SDKs. I think it's more likely that we would make additive changes

Copy link
Contributor

@lauzadis lauzadis Dec 19, 2024

Choose a reason for hiding this comment

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

It's not speculative, there was some discussion during the spec review about adding more options to either request or response but not both.

The spec calls out RequestChecksumCalculation and ResponseChecksumValidation as separate config types, so adding a new config option to either of those wouldn't be a breaking change for SDKs which implement it as the spec says.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no-changelog Indicates that a changelog entry isn't required for a pull request. Use sparingly.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants