-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(CargoMessageSet): Implement encapsulation batching (#74)
- v1.69.9
- v1.69.8
- v1.69.7
- v1.69.6
- v1.69.5
- v1.69.4
- v1.69.3
- v1.69.2
- v1.69.1
- v1.69.0
- v1.68.8
- v1.68.7
- v1.68.6
- v1.68.5
- v1.68.4
- v1.68.3
- v1.68.2
- v1.68.1
- v1.68.0
- v1.67.13
- v1.67.12
- v1.67.11
- v1.67.10
- v1.67.9
- v1.67.8
- v1.67.7
- v1.67.6
- v1.67.5
- v1.67.4
- v1.67.3
- v1.67.2
- v1.67.1
- v1.67.0
- v1.66.13
- v1.66.12
- v1.66.11
- v1.66.10
- v1.66.9
- v1.66.8
- v1.66.7
- v1.66.6
- v1.66.5
- v1.66.4
- v1.66.3
- v1.66.2
- v1.66.1
- v1.66.0
- v1.65.5
- v1.65.4
- v1.65.3
- v1.65.2
- v1.65.1
- v1.65.0
- v1.64.0
- v1.63.1
- v1.63.0
- v1.62.1
- v1.62.0
- v1.61.0
- v1.60.1
- v1.60.0
- v1.59.1
- v1.59.0
- v1.58.0
- v1.57.0
- v1.56.0
- v1.55.3
- v1.55.2
- v1.55.1
- v1.55.0
- v1.54.2
- v1.54.1
- v1.54.0
- v1.53.2
- v1.53.1
- v1.53.0
- v1.52.0
- v1.51.2
- v1.51.1
- v1.51.0
- v1.50.0
- v1.49.1
- v1.49.0
- v1.48.13
- v1.48.12
- v1.48.11
- v1.48.10
- v1.48.9
- v1.48.8
- v1.48.7
- v1.48.6
- v1.48.5
- v1.48.4
- v1.48.3
- v1.48.2
- v1.48.1
- v1.48.0
- v1.47.1
- v1.47.0
- v1.46.2
- v1.46.1
- v1.46.0
- v1.45.0
- v1.44.0
- v1.43.0
- v1.42.1
- v1.42.0
- v1.41.0
- v1.40.0
- v1.39.1
- v1.39.0
- v1.38.4
- v1.38.3
- v1.38.2
- v1.38.1
- v1.38.0
- v1.37.0
- v1.36.5
- v1.36.4
- v1.36.3
- v1.36.2
- v1.36.1
- v1.36.0
- v1.35.0
- v1.34.1
- v1.34.0
- v1.33.0
- v1.32.0
- v1.31.4
- v1.31.3
- v1.31.2
- v1.31.1
- v1.31.0
- v1.30.0
- v1.29.0
- v1.28.0
- v1.27.1
- v1.27.0
- v1.26.0
- v1.25.2
- v1.25.1
- v1.25.0
- v1.24.1
- v1.24.0
- v1.23.2
- v1.23.1
- v1.23.0
Showing
7 changed files
with
252 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
81 changes: 81 additions & 0 deletions
81
src/main/kotlin/tech/relaycorp/relaynet/messages/payloads/CargoBatching.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
@file:JvmName("CargoMessageBatching") | ||
|
||
package tech.relaycorp.relaynet.messages.payloads | ||
|
||
import tech.relaycorp.relaynet.messages.InvalidMessageException | ||
import tech.relaycorp.relaynet.ramf.EncryptedRAMFMessage | ||
import java.time.ZonedDateTime | ||
import java.util.Collections | ||
|
||
private const val MAX_BATCH_LENGTH = | ||
EncryptedRAMFMessage.MAX_PAYLOAD_PLAINTEXT_LENGTH - CargoMessage.DER_TL_OVERHEAD_OCTETS | ||
|
||
/** | ||
* Serialization and expiry date of a message to be encapsulated in a cargo message set. | ||
* | ||
* @throws InvalidMessageException if `cargoMessageSerialized` is longer than | ||
* [CargoMessage.MAX_LENGTH] | ||
*/ | ||
@Suppress("ArrayInDataClass") | ||
data class CargoMessageWithExpiry( | ||
val cargoMessageSerialized: ByteArray, | ||
val expiryDate: ZonedDateTime | ||
) { | ||
init { | ||
if (CargoMessage.MAX_LENGTH < cargoMessageSerialized.size) { | ||
throw InvalidMessageException( | ||
"Message must not be longer than ${CargoMessage.MAX_LENGTH} octets " + | ||
"(got ${cargoMessageSerialized.size})" | ||
) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Serialization and expiry date of a cargo message set. | ||
*/ | ||
data class CargoMessageSetWithExpiry( | ||
val cargoMessageSet: CargoMessageSet, | ||
val latestMessageExpiryDate: ZonedDateTime | ||
) | ||
|
||
/** | ||
* Batch as many messages together as possible without exceeding the payload length limit on | ||
* individual cargoes. | ||
* | ||
* If all messages can be encapsulated in the same cargo message set, they will be. Otherwise, | ||
* multiple cargo message sets will be generated. The output will be empty if the input is | ||
* empty too. | ||
*/ | ||
suspend fun Sequence<CargoMessageWithExpiry>.batch(): Sequence<CargoMessageSetWithExpiry> = | ||
sequence { | ||
val currentBatch = mutableListOf<ByteArray>() | ||
var currentBatchExpiry: ZonedDateTime? = null | ||
var currentBatchAvailableOctets = MAX_BATCH_LENGTH | ||
|
||
this@batch.forEach { messageWithExpiry -> | ||
val messageTlvLength = | ||
CargoMessage.DER_TL_OVERHEAD_OCTETS + messageWithExpiry.cargoMessageSerialized.size | ||
val messageFitsInCurrentBatch = messageTlvLength <= currentBatchAvailableOctets | ||
if (!messageFitsInCurrentBatch) { | ||
val cargoMessageSet = CargoMessageSet(currentBatch.toTypedArray()) | ||
yield(CargoMessageSetWithExpiry(cargoMessageSet, currentBatchExpiry!!)) | ||
|
||
currentBatch.clear() | ||
currentBatchExpiry = null | ||
currentBatchAvailableOctets = MAX_BATCH_LENGTH | ||
} | ||
|
||
currentBatch.add(messageWithExpiry.cargoMessageSerialized) | ||
currentBatchAvailableOctets -= messageTlvLength | ||
|
||
currentBatchExpiry = currentBatchExpiry ?: messageWithExpiry.expiryDate | ||
currentBatchExpiry = | ||
Collections.max(listOf(currentBatchExpiry, messageWithExpiry.expiryDate)) | ||
} | ||
|
||
if (currentBatch.isNotEmpty()) { | ||
val cargoMessageSet = CargoMessageSet(currentBatch.toTypedArray()) | ||
yield(CargoMessageSetWithExpiry(cargoMessageSet, currentBatchExpiry as ZonedDateTime)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
src/test/kotlin/tech/relaycorp/relaynet/messages/payloads/CargoBatchingTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
package tech.relaycorp.relaynet.messages.payloads | ||
|
||
import kotlinx.coroutines.ExperimentalCoroutinesApi | ||
import kotlinx.coroutines.test.runBlockingTest | ||
import org.junit.jupiter.api.assertThrows | ||
import tech.relaycorp.relaynet.messages.InvalidMessageException | ||
import java.time.ZonedDateTime | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
|
||
private val expiryDate = ZonedDateTime.now().plusDays(1) | ||
|
||
@ExperimentalCoroutinesApi | ||
class BatchTest { | ||
private val messageSerialized = "I'm a parcel. Pinky promise.".toByteArray() | ||
|
||
@Test | ||
fun `Zero messages should result in zero batches`() = runBlockingTest { | ||
val batches = emptySequence<CargoMessageWithExpiry>().batch() | ||
|
||
assertEquals(0, batches.count()) | ||
} | ||
|
||
@Test | ||
fun `A single message should result in one batch`() = runBlockingTest { | ||
val batches = sequenceOf(CargoMessageWithExpiry(messageSerialized, expiryDate)).batch() | ||
|
||
assertEquals(1, batches.count()) | ||
val cargoMessageSet = batches.first().cargoMessageSet | ||
assertEquals(1, cargoMessageSet.messages.size) | ||
assertEquals(messageSerialized.asList(), cargoMessageSet.messages.first().asList()) | ||
} | ||
|
||
@Test | ||
fun `Multiple small messages should be put in the same batch`() = runBlockingTest { | ||
val message2Serialized = "I'm a PCA. *wink wink*".toByteArray() | ||
|
||
val batches = sequenceOf( | ||
CargoMessageWithExpiry(messageSerialized, expiryDate), | ||
CargoMessageWithExpiry(message2Serialized, expiryDate) | ||
).batch() | ||
|
||
assertEquals(1, batches.count()) | ||
val cargoMessageSet = batches.first().cargoMessageSet | ||
assertEquals(2, cargoMessageSet.messages.size) | ||
assertEquals(messageSerialized.asList(), cargoMessageSet.messages.first().asList()) | ||
assertEquals(message2Serialized.asList(), cargoMessageSet.messages[1].asList()) | ||
} | ||
|
||
@Test | ||
fun `Messages should be put into as few batches as possible`() = runBlockingTest { | ||
val octetsIn3Mib = 3145728 | ||
val messageSerialized = "a".repeat(octetsIn3Mib).toByteArray() | ||
|
||
val batches = sequenceOf( | ||
CargoMessageWithExpiry(messageSerialized, expiryDate), | ||
CargoMessageWithExpiry(messageSerialized, expiryDate), | ||
CargoMessageWithExpiry(messageSerialized, expiryDate) | ||
).batch() | ||
|
||
assertEquals(2, batches.count()) | ||
val cargoMessageSet1 = batches.first().cargoMessageSet | ||
assertEquals( | ||
listOf(messageSerialized.asList(), messageSerialized.asList()), | ||
cargoMessageSet1.messages.map { it.asList() } | ||
) | ||
val cargoMessageSet2 = batches.last().cargoMessageSet | ||
assertEquals(1, cargoMessageSet2.messages.size) | ||
assertEquals(messageSerialized.asList(), cargoMessageSet2.messages.first().asList()) | ||
} | ||
|
||
@Test | ||
fun `Messages collectively reaching the max length should be placed together`() = | ||
runBlockingTest { | ||
val halfLimit = CargoMessage.MAX_LENGTH / 2 | ||
val message1Serialized = "a".repeat(halfLimit - 3).toByteArray() | ||
val message2Serialized = "a".repeat(halfLimit - 2).toByteArray() | ||
|
||
val batches = sequenceOf( | ||
CargoMessageWithExpiry(message1Serialized, expiryDate), | ||
CargoMessageWithExpiry(message2Serialized, expiryDate) | ||
).batch() | ||
|
||
assertEquals(1, batches.count()) | ||
val cargoMessageSet = batches.first().cargoMessageSet | ||
assertEquals(2, cargoMessageSet.messages.size) | ||
assertEquals(message1Serialized.asList(), cargoMessageSet.messages[0].asList()) | ||
assertEquals(message2Serialized.asList(), cargoMessageSet.messages[1].asList()) | ||
} | ||
|
||
@Test | ||
fun `Expiry date of batch should be that of its message with latest expiry`() = | ||
runBlockingTest { | ||
// Generate two batches where the expiry date of the former is that of its first | ||
// message, and the expiry date of the latter batch is that of its last message | ||
val messageSerialized = "a".repeat(CargoMessage.MAX_LENGTH / 2 - 3).toByteArray() | ||
val now = ZonedDateTime.now() | ||
val message1ExpiryDate = now.plusDays(2) | ||
val message2ExpiryDate = now.plusDays(1) | ||
val message3ExpiryDate = now.plusDays(3) | ||
val message4ExpiryDate = now.plusDays(4) | ||
|
||
val batches = sequenceOf( | ||
CargoMessageWithExpiry(messageSerialized, message1ExpiryDate), | ||
CargoMessageWithExpiry(messageSerialized, message2ExpiryDate), | ||
CargoMessageWithExpiry(messageSerialized, message3ExpiryDate), | ||
CargoMessageWithExpiry(messageSerialized, message4ExpiryDate) | ||
).batch() | ||
|
||
assertEquals(2, batches.count()) | ||
assertEquals(2, batches.first().cargoMessageSet.messages.size) | ||
assertEquals(message1ExpiryDate, batches.first().latestMessageExpiryDate) | ||
assertEquals(2, batches.last().cargoMessageSet.messages.size) | ||
assertEquals(message4ExpiryDate, batches.last().latestMessageExpiryDate) | ||
} | ||
} | ||
|
||
class CargoMessageWithExpiryTest { | ||
@Test | ||
fun `A message with the largest possible length should be accepted`() { | ||
val messageSerialized = "a".repeat(CargoMessage.MAX_LENGTH).toByteArray() | ||
|
||
CargoMessageWithExpiry(messageSerialized, expiryDate) | ||
} | ||
|
||
@Test | ||
fun `Messages exceeding the max per-message size should be refused`() { | ||
val messageSerialized = "a".repeat(CargoMessage.MAX_LENGTH + 1).toByteArray() | ||
|
||
val exception = assertThrows<InvalidMessageException> { | ||
CargoMessageWithExpiry(messageSerialized, expiryDate) | ||
} | ||
|
||
assertEquals( | ||
"Message must not be longer than ${CargoMessage.MAX_LENGTH} octets " + | ||
"(got ${messageSerialized.size})", | ||
exception.message | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters