Skip to content

Conversation

@aajtodd
Copy link
Contributor

@aajtodd aajtodd commented Sep 24, 2021

Issue #

N/A

Description of changes

  • Implements message encode/decode for the vnd.amazon.event-stream content-type.
  • Some churn from SdkBuffer -> SdkByteBuffer and related API changes

Scope

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

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

Copy link
Contributor

@ianbotsf ianbotsf left a comment

Choose a reason for hiding this comment

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

Looks most good, well done! The biggest question blocking my approval is the interpretation of length fields as signed integers.

* Read an encoded header from the [buffer]
*/
public fun decode(buffer: Buffer): Header {
check(buffer.readRemaining >= MIN_HEADER_LEN.toULong()) { "Invalid frame header; require at least 2 bytes" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Reuse the constant in the error message:

check(buffer.readRemaining >= MIN_HEADER_LEN.toULong()) { "Invalid frame header; require at least $MIN_HEADER_LEN bytes" }

Comment on lines 31 to 45
fun fromTypeId(value: Byte): HeaderType {
return when (value.toInt()) {
0 -> TRUE
1 -> FALSE
2 -> BYTE
3 -> INT16
4 -> INT32
5 -> INT64
6 -> BYTE_ARRAY
7 -> STRING
8 -> TIMESTAMP
9 -> UUID
else -> throw IllegalArgumentException("Unknown HeaderType: $value")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment: We can avoid duplication by using values instead:

fun fromTypeId(value: Byte): HeaderType =
    requireNotNull(values().find { it.value == value }) { "Unknown HeaderType: $value" }

Comment on lines 105 to 110
is ByteArray -> {
dest.writeHeader(HeaderType.BYTE_ARRAY)
check(value.size in Short.MIN_VALUE..Short.MAX_VALUE) { "HeaderValue ByteArray too long" }
dest.writeShort(value.size.toShort())
dest.writeFully(value)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Is any recovery possible at this point? If so, we should probably check the array size before writing the header type.

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'm not sure there is any recovery possible here but agree that the check belongs before starting the write.

}
is ByteArray -> {
dest.writeHeader(HeaderType.BYTE_ARRAY)
check(value.size in Short.MIN_VALUE..Short.MAX_VALUE) { "HeaderValue ByteArray too long" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: The lower bound here should probably be 0 instead of Short.MIN_VALUE.

Comment on lines 107 to 108
check(value.size in Short.MIN_VALUE..Short.MAX_VALUE) { "HeaderValue ByteArray too long" }
dest.writeShort(value.size.toShort())
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: It's not clear to me from reading the event streams description that the value byte length is meant to be a signed 16-bit integer. Do we have any other sources that may indicate whether 32KB is the max size for a blob/string? Or are 64K values possible?

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 agree. I think the header value lengths and overall lengths in the prelude ought to be unsigned. Good call out, will fix.

Comment on lines 166 to 167
@Suppress("NOTHING_TO_INLINE")
private inline fun MutableBuffer.writeHeader(headerType: HeaderType) = writeByte(headerType.value)
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 inline this function if the compiler thinks there's nothing to inline?

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'm smarter of course...will remove inline

Comment on lines +66 to +73
// read headers
var headerBytesConsumed = 0UL
while (headerBytesConsumed < prelude.headersLength.toULong()) {
val start = messageBuffer.readPosition
val header = Header.decode(messageBuffer)
headerBytesConsumed += messageBuffer.readPosition - start
message.addHeader(header)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: Should we verify when we're done parsing headers that headerBytesConsumed == prelude.headersLength.toULong()? It seems like that could provide an early warning that a message is malformed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems sensible, will update

* Encode a message to the [dest] buffer
*/
public fun encode(dest: MutableBuffer) {
val encodedHeaders = SdkByteBuffer(16u)
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 16u?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No reason, had to pick something. Any suggestion on what a sensible default would be?

Copy link
Contributor

Choose a reason for hiding this comment

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

In the absence of a reason to pick something else, I'd say 0u. In fact, upon further examination of smithy-kotlin#482, I'd suggest adding a default value of 0u to the secondary constructor.

Copy link
Contributor

@kggilmer kggilmer left a comment

Choose a reason for hiding this comment

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

Very clean, nice work.

*/

description = "Support for the vnd.amazon.event-stream content type"
extra["displayName"] = "AWS :: SDK :: Kotlin :: Event Stream"
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 like it would be more descriptive if the display name has "Protocols" after "Kotlin", also for json and xml.

is String -> {
dest.writeHeader(HeaderType.STRING)
val bytes = value.encodeToByteArray()
check(bytes.size in Short.MIN_VALUE..Short.MAX_VALUE) { "HeaderValue String too long" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Ian's point above applies here as well

Nit: The lower bound here should probably be 0 instead of Short.MIN_VALUE.

HeaderType.BYTE_ARRAY, HeaderType.STRING -> {
val len = buffer.readShort()
if (len < 0 || buffer.readRemaining < len.toULong()) {
throw IllegalStateException("Invalid HeaderValue; type=$type, len=$len")
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion

Seems like we may also want to know what buffer.readRemaining was at the time of check if this is thrown

* The decoder will consume the prelude when enough data is available. When it is invoked with enough
* data it will consume the remaining message bytes.
*/
public fun decodeFrame(buffer: Buffer): DecodedFrame {
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 gather that the intent here that if not enough bytes are in the buffer to decode the next frame that DecodedFrame.Incomplete is returned. Since there is only one valid type that can be returned Message and no state is captured by Incomplete would it not be simpler to represent this case with null instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ya we could probably do that, I'll take a look and see. I'm not even sure we'll keep FrameDecoder in it's current state depending on how it all gets wired up. I was sort of following along with both the Java and Rust implementations that have similar abstractions.

@aajtodd aajtodd merged commit cf91733 into main Oct 5, 2021
@aajtodd aajtodd deleted the feat-event-stream branch October 5, 2021 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants