From fe27a9deda320d935016c1f6ab4ff71eeace0a1f Mon Sep 17 00:00:00 2001 From: IanMeyers Date: Fri, 5 Jun 2020 16:43:29 +0100 Subject: [PATCH 1/2] V2 Version of Deaggregators --- .../kinesis/agg/RecordAggregatorTest.java | 78 ++++--- java/KinesisDeaggregator/README.md | 4 +- java/KinesisDeaggregator/pom.xml | 8 + java/KinesisDeaggregatorV2/README.md | 219 ++++++++++++++++++ ...zon-kinesis-deaggregator-2.0.0-javadoc.jar | Bin 0 -> 39703 bytes ...zon-kinesis-deaggregator-2.0.0-sources.jar | Bin 0 -> 5822 bytes .../amazon-kinesis-deaggregator-2.0.0.jar | Bin 0 -> 10199 bytes .../license/apache-2.0/header.txt | 15 ++ java/KinesisDeaggregatorV2/pom.xml | 163 +++++++++++++ .../kinesis/deagg/RecordDeaggregator.java | 160 +++++++++++++ .../deagg/util/DeaggregationUtils.java | 33 +++ .../src/sample/java/SampleLambdaEvent.json | 20 ++ .../amazonaws/kinesis/deagg/EchoHandler.java | 38 +++ .../test/java/TestDirectDeaggregation.java | 165 +++++++++++++ .../test/java/TestLambdaDeaggregation.java | 172 ++++++++++++++ 15 files changed, 1044 insertions(+), 31 deletions(-) create mode 100644 java/KinesisDeaggregatorV2/README.md create mode 100644 java/KinesisDeaggregatorV2/dist/amazon-kinesis-deaggregator-2.0.0-javadoc.jar create mode 100644 java/KinesisDeaggregatorV2/dist/amazon-kinesis-deaggregator-2.0.0-sources.jar create mode 100644 java/KinesisDeaggregatorV2/dist/amazon-kinesis-deaggregator-2.0.0.jar create mode 100644 java/KinesisDeaggregatorV2/license/apache-2.0/header.txt create mode 100644 java/KinesisDeaggregatorV2/pom.xml create mode 100644 java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/RecordDeaggregator.java create mode 100644 java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/util/DeaggregationUtils.java create mode 100644 java/KinesisDeaggregatorV2/src/sample/java/SampleLambdaEvent.json create mode 100644 java/KinesisDeaggregatorV2/src/sample/java/com/amazonaws/kinesis/deagg/EchoHandler.java create mode 100644 java/KinesisDeaggregatorV2/src/test/java/TestDirectDeaggregation.java create mode 100644 java/KinesisDeaggregatorV2/src/test/java/TestLambdaDeaggregation.java diff --git a/java/KinesisAggregator/src/test/java/com/amazonaws/kinesis/agg/RecordAggregatorTest.java b/java/KinesisAggregator/src/test/java/com/amazonaws/kinesis/agg/RecordAggregatorTest.java index fe814a7..a70593e 100644 --- a/java/KinesisAggregator/src/test/java/com/amazonaws/kinesis/agg/RecordAggregatorTest.java +++ b/java/KinesisAggregator/src/test/java/com/amazonaws/kinesis/agg/RecordAggregatorTest.java @@ -18,37 +18,55 @@ package com.amazonaws.kinesis.agg; import java.nio.charset.StandardCharsets; -import com.amazonaws.kinesis.agg.RecordAggregator; -import com.amazonaws.kinesis.agg.AggRecord; +import java.util.Base64; +import java.util.Random; + import org.junit.Assert; import org.junit.Test; -public class RecordAggregatorTest -{ - protected final String ALPHABET = "abcdefghijklmnopqrstuvwxyz"; - - @Test - public void testSingleUserRecord() - { - RecordAggregator aggregator = new RecordAggregator(); - - Assert.assertEquals(0, aggregator.getNumUserRecords()); - - try - { - aggregator.addUserRecord("partition_key", ALPHABET.getBytes(StandardCharsets.UTF_8)); - } - catch (Exception e) - { - e.printStackTrace(); - Assert.fail("Encountered unexpected exception: " + e.getMessage()); - } - Assert.assertEquals(1, aggregator.getNumUserRecords()); - - AggRecord record = aggregator.clearAndGet(); - Assert.assertNotNull(record); - Assert.assertEquals(0, aggregator.getNumUserRecords()); - - Assert.assertEquals(1, record.getNumUserRecords()); - } +public class RecordAggregatorTest { + protected final String ALPHABET = "abcdefghijklmnopqrstuvwxyz"; + + @Test + public void testSingleUserRecord() { + RecordAggregator aggregator = new RecordAggregator(); + + Assert.assertEquals(0, aggregator.getNumUserRecords()); + + try { + aggregator.addUserRecord("partition_key", ALPHABET.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + e.printStackTrace(); + Assert.fail("Encountered unexpected exception: " + e.getMessage()); + } + Assert.assertEquals(1, aggregator.getNumUserRecords()); + + AggRecord record = aggregator.clearAndGet(); + Assert.assertNotNull(record); + Assert.assertEquals(0, aggregator.getNumUserRecords()); + + Assert.assertEquals(1, record.getNumUserRecords()); + } + + @Test + public void testMultiRecord() throws Exception { + RecordAggregator aggregator = new RecordAggregator(); + Random rand = new Random(); + String key = "abc"; + int c = 100; + String encodedTargetValue = "84mawgoDYWJjEicxOTE0MTU2NTgzNDQxNTg3NjYxNjgwMzE0NzMyNzc5MjI4MDM1NzAaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2Jh0I8WvwEDJiGD4YsiKIfUOw=="; + Assert.assertEquals(0, aggregator.getNumUserRecords()); + String flip = new StringBuilder(ALPHABET).reverse().toString(); + + // add 100 random records all with the same partition key + for (int i = 0; i < c; i++) { + String pattern = i % 2 == 0 ? ALPHABET : flip; + aggregator.addUserRecord(key, pattern.getBytes(StandardCharsets.UTF_8)); + } + + AggRecord r = aggregator.clearAndGet(); + Assert.assertEquals(c, r.getNumUserRecords()); + String encodedString = Base64.getEncoder().encodeToString(r.toRecordBytes()); + Assert.assertEquals(encodedTargetValue, encodedString); + } } diff --git a/java/KinesisDeaggregator/README.md b/java/KinesisDeaggregator/README.md index 6bcff6d..082cda8 100644 --- a/java/KinesisDeaggregator/README.md +++ b/java/KinesisDeaggregator/README.md @@ -1,7 +1,9 @@ -# Kinesis Java Record Deaggregator +# Kinesis Java Record Deaggregator for AWS V1 SDK's This library provides a set of convenience functions to perform in-memory record deaggregation that is compatible with the [Kinesis Aggregated Record Format](https://github.com/awslabs/amazon-kinesis-producer/blob/master/aggregation-format.md) used by the Kinesis Producer Library (KPL) and the KinesisAggregator module. This module can be used in any Java-based application that receives aggregated Kinesis records, including applications running on AWS Lambda. +This module is only compatible with version 1.x AWS SDK's. + ## Record Deaggregation The `RecordDeaggregator` is the class that does the work of extracting individual Kinesis user records from aggregated Kinesis Records received by AWS Lambda or directly through the Kinesis Java SDK. This class provide multiple ways to deaggregate records: stream-based, list-based, batch-based and single record. diff --git a/java/KinesisDeaggregator/pom.xml b/java/KinesisDeaggregator/pom.xml index c0c4c7c..3673de8 100644 --- a/java/KinesisDeaggregator/pom.xml +++ b/java/KinesisDeaggregator/pom.xml @@ -105,6 +105,14 @@ + + maven-assembly-plugin + + + jar-with-dependencies + + + diff --git a/java/KinesisDeaggregatorV2/README.md b/java/KinesisDeaggregatorV2/README.md new file mode 100644 index 0000000..100eb67 --- /dev/null +++ b/java/KinesisDeaggregatorV2/README.md @@ -0,0 +1,219 @@ +# Kinesis Java Record Deaggregator for AWS V2 SDK's + +This library provides a set of convenience functions to perform in-memory record deaggregation that is compatible with the [Kinesis Aggregated Record Format](https://github.com/awslabs/amazon-kinesis-producer/blob/master/aggregation-format.md) used by the Kinesis Producer Library (KPL) and the KinesisAggregator module. This module can be used in any Java-based application that receives aggregated Kinesis records, including applications running on AWS Lambda. + +This module is compatible with the V2 AWS SDK's. + +## Record Deaggregation + +The `RecordDeaggregator` is the class that does the work of extracting individual Kinesis user records from aggregated Kinesis Records received by AWS Lambda or directly through the Kinesis Java SDK. This class provide multiple ways to deaggregate records: stream-based, list-based, batch-based and single record. + +### Creating a Deaggregator + +There are two supported base classes that can be used for Deaggregation, `com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord` and `software.amazon.awssdk.services.kinesis.model.Record`. These support Lambda based access, and Kinesis V2 SDK access respectively. Use of any other base class will throw an `InvalidArgumentsException`. + +This project uses Java Generics to handle these different types correctly. To create a Lambda compliant Deaggregator, use: + +``` +import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; +... +RecordDeaggregator deaggregator = new RecordDeaggregator<>(); +``` + +and for the Kinesis SDK: + +``` +import software.amazon.awssdk.services.kinesis.model.Record; +... +RecordDeaggregator deaggregator = new RecordDeaggregator<>(); +``` + +### Stream-based Deaggregation + +The following examples demonstrate functions to create a new instance of the `RecordDeaggregator` class and then provide it code to run on each extracted UserRecord. For example, using Java 8 Streams: + +``` +deaggregator.stream( + event.getRecords().stream(), + userRecord -> { + // Your User Record Processing Code Here! + logger.log(String.format("Processing UserRecord %s (%s:%s)", + userRecord.partitionKey(), + userRecord.sequenceNumber(), + userRecord.subSequenceNumber())); + } +); +``` + +In this invocation, we are extracting the KinesisEventRecords from the Event provided by AWS Lambda, and converting them to a Stream. We then provide a lambda function which iterates over the extracted user records. You should provide your own application-specific logic in place of the provided `logger.log()` call. + +### List-based Deaggregation + +You can also achieve the same functionality using Lists rather than Java Streams via the `RecordDeaggregator.KinesisUserRecordProcessor` interface: + +``` +try { + // process the user records with an anonymous record processor + // instance + deaggregator.processRecords(event.getRecords(), + new RecordDeaggregator.KinesisUserRecordProcessor() { + public Void process(List userRecords) { + for (KinesisClientRecord userRecord : userRecords) { + // Your User Record Processing Code Here! + logger.log(String.format( + "Processing UserRecord %s (%s:%s)", + userRecord.partitionKey(), + userRecord.sequenceNumber(), + userRecord.subSequenceNumber())); + } + + return null; + } + }); +} catch (Exception e) { + logger.log(e.getMessage()); +} +``` + +As with the previous example, you should provide your own application-specific logic in place of the provided `logger.log()` call. + +### Batch-based Deaggregation + +For those whole prefer simple method call and response mechanisms, the `RecordDeaggregator` provides a `deaggregate` method that takes in a list of aggregated Kinesis records and deaggregates them synchronously in bulk. For example: + +``` +try { + List userRecords = deaggregator.deaggregate(event.getRecords()); + for (KinesisClientRecord userRecord : userRecords) { + // Your User Record Processing Code Here! + logger.log(String.format("Processing KinesisClientRecord %s (%s:%s)", + userRecord.partitionKey(), + userRecord.sequenceNumber(), + userRecord.subSequenceNumber())); + } +} catch (Exception e) { + logger.log(e.getMessage()); +} +``` + +As with the previous example, you should provide your own application-specific logic in place of the provided `logger.log()` call. + +### Single Record Deaggregation + +In some cases, it can also be beneficial to be able to deaggregate a single Kinesis aggregated record at a time. The `RecordDeaggregator` provides a single static `deaggregate` method that takes in a single aggregated Kinesis record, deaggregates it and returns one or more Kinesis user records as a result. For example: + +``` +KinesisEventRecord singleRecord = ...; +try { + List userRecords = deaggregator.deaggregate(singleRecord); + for (KinesisClientRecord userRecord : userRecords) { + // Your User Record Processing Code Here! + logger.log(String.format("Processing UserRecord %s (%s:%s)", + userRecord.partitionKey(), + userRecord.pequenceNumber(), + userRecord.subSequenceNumber())); + } +} catch (Exception e) { + logger.log(e.getMessage()); +} +``` + +As with the previous example, you should provide your own application-specific logic in place of the provided `logger.log()` call. + +### Handling Non-Aggregated Records + +The record deaggregation methods in `RecordDeaggregator` can handle both records in the standard Kinesis aggregated record format as well as Kinesis records in arbitrary user-defined formats. If you pass records to the `RecordDeaggregator` that follow the [Kinesis Aggregated Record Format](https://github.com/awslabs/amazon-kinesis-producer/blob/master/aggregation-format.md), they will be deaggregated into one or more Kinesis user records per the encoding rules. If you pass records to the `RecordDeaggregator` that are not actually aggregated records, they will be returned unchanged as Kinesis user records. You may also mix aggregated and non-aggregated records in the same deaggregation call. + +## Sample Code + +This project includes a set of sample code to help you create a Lambda function that leverages deaggregation. Both of the below contents are provided in the `src/sample/java` folder. + +### EchoHandler.java + +``` +package com.amazonaws.kinesis.deagg; + +import java.util.List; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; + +import software.amazon.kinesis.retrieval.KinesisClientRecord; + +public class EchoHandler implements RequestHandler { + + @Override + public Void handleRequest(KinesisEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + // extract the records from the event + List records = event.getRecords(); + + logger.log(String.format("Recieved %s Raw Records", records.size())); + + try { + // now deaggregate the message contents + List deaggregated = new RecordDeaggregator().deaggregate(records); + logger.log(String.format("Received %s Deaggregated User Records", deaggregated.size())); + + deaggregated.stream().forEachOrdered(rec -> { + logger.log(rec.partitionKey()); + }); + } catch (Exception e) { + logger.log(e.getMessage()); + } + + return null; + } +} +``` + +This class will output the size of the received batch from Kinesis, and then deaggregate the user records and output the count of those records, along with each Partition Key recieved. + +If you would like to test this functionality, create a new Java 8 Lambda function with the above code and required dependencies. You can then use the below TestEvent to show the functionality of the deaggregating Lambda: + +### SampleLambdaEvent.json + +``` +{ + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "84mawgoDYWJjEicxOTE0MTU2NTgzNDQxNTg3NjYxNjgwMzE0NzMyNzc5MjI4MDM1NzAaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2Jh0I8WvwEDJiGD4YsiKIfUOw==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600 + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "us-east-1" + } + ] +} +``` + +This file contains an event that simulates an Aggregated Kinesis Event enclosing 100 User Records. The payload of this message is alternating lower case alpha in forward, then backward order. + +---- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/java/KinesisDeaggregatorV2/dist/amazon-kinesis-deaggregator-2.0.0-javadoc.jar b/java/KinesisDeaggregatorV2/dist/amazon-kinesis-deaggregator-2.0.0-javadoc.jar new file mode 100644 index 0000000000000000000000000000000000000000..c508cd748eac50959613784db1566963e892f55d GIT binary patch literal 39703 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 F{{ckLBg+5) literal 0 HcmV?d00001 diff --git a/java/KinesisDeaggregatorV2/dist/amazon-kinesis-deaggregator-2.0.0.jar b/java/KinesisDeaggregatorV2/dist/amazon-kinesis-deaggregator-2.0.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..353d4ae0c99e290fc4ae2c283a4cb2494a51eaec GIT binary patch literal 10199 zcmbt)1z20#5^iu!arffxP_#gCcXusb2<{pvUV>Yp6n8D|Qrx|?SaJ74DUaTso*ujR zzW46Rx09V@=AT(>?=^eA`A1m}1{N9ceJF(VD*x-`w>PBkze%WzGRr7RvMT>3h6s>< z5W8Hx>#=|MJM`he{-cO*IyZ!)A`AlG9$M_r4S2YNPoGy;CQ2LL3w{CO6z5AK;c+W$U*ze_y& zO~S<9#LLma#2xe>WO4r{YisRb0kZz14j|~y9uWUq9dip4E35xtjpDy(y1H1~{jQD( z*ar0~8*--Na~LqUmV z$-ybzh|UCQZQIP8=H8CWY?m%o3wrd6M>CyKn|X8+X+E1_x}KmTK-t|Y_eI_};fTh! zpe#IBO*QDJK|@@Ua*H9i+LL!2Bws$7TSOE#Wu_YQdy95}pJwLr2H*8pxri zqaszUl@8%m^~Ljr*UI4N^zob*XACM3W*cK^#~d%ae0dosz#(tF&RV(2A>t%yp%aqT z7TL!eCJmyH!{3ZNpOK2+@fk+fYv`wITnLk9eOBW0)-VIsx$Sa-s_hu#c2WnyFizR_ zOj<<)JUK5FIq)iaE8MzjbX|)~aKShqE-pIrSuOsd7!pcEb(F3=d{*;mUXceKX++ps z&Od#e7##!LJ}n)LOo(e!?&Gc1+^w&=`TP1dQZihxC(&ZK*K=%m)-ZR0V_XqkL%8h# zxfL+;QoewuR?;3xrC|I$U)XfSNg&W93c%KMe5#RV;i-!xBd2((zrSrsL&Yk~P}2n0 zQ&?f!$^u};8QYrV`E>HOMd_Vq4-u=25$fnr^*g@H!imU~&0$ND7bn1Po&ojJZaZm9 zrH7P*Fat_M+-bDIeX-fjY;1VuHy4p#MyHK5o!Z622Cq1Qr&Hj{yMmLKd-`rdT3=@y za1GjgcAA*^GjJ^s>XlAGzJBdq-;jRpPDH?2aIf-h`Qe5u902eh4FKT#zq!N3zi$&~ z3o8>BYexr-hkG8xVrFLo0+ng$fUu;{zxDALde_Z&5pPOEL%k}s?}~px)dM!xqd|v- zHc} z_5ap{0Iv+#O*K5Cu5WL#CIeg~_vm&O@Wq7B!PH$^J_i!Qw@rH8{qJ zmJ;7fEs0qXE|g$Z@g!By=|(kX;B4wc-G-p4+jUJ+f8|pPez;(~YuElwEg`d*5TPMX zj-V}P$rU|MN_rWsjRr@)OTIOaZq(wPdFs_0NWF5$K;O%V6>*vQZ#pLY4q#CMhovr( zG0+&6`5xHNY8cC5fFsmu!_$PO8^~MfTSAUXk?7Ia%77Jp6NaFg1-Ynj))MCjs_t5B z7JK2f&?}ptN>WJ%f+zzDbWy?s8<8#FcZk9EX&M`=ST|!`qavhT@Q~)&48EM^w3|~) z_I#;znqDpV+Ek}Mdtb+mJzSqM)nh$|Max5?|FSTHXL@$t$0=u&W#(mcEzd?6QePE3 zmV$(A@vfpCU;1sJ+dPGV=Aka1uup2Gf_od?!TU!;qCjrDRLtc#z4~(cU3+c?! ztHTjI+PTtbcx%Re^WH&<1~zGiOy0N`N)IQi z>Y~XQW6o3V`n%j!Vy^isRuQU$PMgsDLH8dcSyh!A`(vC0Ab5svszD7^I?U8`>GGgR z9LUhnBd+q8Gaq4l6^QCFO7|$jX;{FqJy%(mrwBZL$m$oQ2>8UYsvp%(BX{B3YJ zkD%D6(Kv!`%^RCU;Jl=4fP?3bR2Q$cyFj!Qf@2P|E5gCEcjGF3#XRT}I$%spgLmTw zV8Y2Nj@meFUj!kG^ZJ~qC*7a4LC$kWk+ZyU+{WC#-bgmYu~YZMRI}jc^%_i=N@yz4 zrhsNdrFiq#9S%2c5H;`T35Y_3OV2-?(%!Iq=v$mNCfLIVADRliKZoynJW?NK=`A)x zG$HY7Q9PrJB0-s0X^oz+8L`Lk&Gv~#O})R<0iaBXg#T1tKaraxlz{+h0zT|8yFDt} z{)rLr9ierEiea&4-8in8Y?E98dUwt+qT6IcrgI}qavXUwf;I6Qrv$OYgZ;j%Yow=f zcxo!pW3TqPM%R1qq5d4p#deiU9zCRwp(FqR*MEuSsupIB&gQ>@xuf%sFkYvPq>nvE zaNllWV(uQ2NQcZ|z)Zs!R-!p2-hmvRi1-|q-Db&rp5%qOyCo-Qq-k2dl#OXRsr6!h zep##S(m9(@$P967ESJ#Kt^Cn#n_Bw0H)l-^ZK25ZU5e+~$J@4}`@8ld-=kLF>z6fX z(DNO#zS_av!qY3^Xeexl-I~M~>okI|-#KiS;T_L13%8)NuDxd6e9gLArsuhrCrsRJ z|Mkr6+Y^=*3uwc`XE={;tB~E*U!~4nJK-4)JvHhltH3*kC!1Y`y>IA`SplCmDDR-;~b=1}2TX9mkuPX4m0$ zFD5R1CNh^&npTh6U8S65V`?Q&q1(kyFN~eo<)zA<*g%}rn1C@lt724GY2g~1156L2 zHgIzR%M@XdjBG2I#bOn(BM68xPmzfGf*zhFJJ7JVs9K}bO95CZ zP$Md1l^oL`X+lVsjM~dkZ|rU=mNR}jB^>ToE)UNWk<}>ACOaywne2d1alDKC?}krd z_gL6oDM_aR40v26NFk=yt(ol9T3Vj%9B*VTRcW`cHX-a=^JfE@Kxcpcv|3YoZaN47 ztPsgti7!bRy?pJT5+3Kas&?saXj_VZqnWsLUOH3;%hht}^D^f|l&s8j8ijeMpuRMuy4k}7)b zb;SgqE%<6d&wV(D9=`QD-os}3#EP1bS-<#hL8C!9fw~H`9Y;*B+kB@}mrh24Y+mRR z$65;m-sVoWZRul)zxa}_c&7`$BbLg(1Ot`%TC5Bv9eH_KKMKmIJ!zWqlqZzT8y64W z0ho+Yg`pMkBp3+;@`Q1X_z>?nAROT&ES*o(fVYXI# zDn}9~T3lNKJ{4mh9{}1Y*P5@h0*)fDq5SC9q^_&N`;W1po?4_#Nr!LuaXb98)LOQ|XUAv)Z|mse?-{L`l>V?pbbP71#56hvK~pfu9` z&k3-Z7M*722vGjmC$wL6dW?>>7~~pLC9O74wp^NtZiJ|4O?ll-6P|tpw{tQLYZk=} zs-%DMUwPf*~ZgX>eI4s>8{EUx5 z@9DP76eKFE4XICjdu5yhGlD-qdT^DL5p9Qy9w_&LZ*l7D$#8Oq?0d`D{Aik@>8J4| z0I5J<^ktT!i4?o(B~Cr$b*@D6Ps5fx&(kvu-CS3n7hp$TOm*2{q$F#%(D~E3=f4ia zQKy~@xK9i|A%YWnEn{?WZh6jPp8{x(C8TbM$gSJP~VEc*Ae!YWja>2aw!?&zxBj+m%^NLLz+wU5; zck)G)V!pYhsfZm}Y$?|{LMLDFT_bYeDX4_FCII| zV;fo<1a10k9oEKMNmK^Sy|Eibaiy54AZfC-nJ?4=m9{d2aEQ5!*E8Lw1yU%tYE8N? zbg^Mpplnsrgy1K2bIu2QrBRmBF&M_g(1MGz_fj2Q9Syz`)|+k#9_iE&dD3-0))znD z3Uy2rbF6oyf%F~48rb3Kz$I3wV0gd5T8u!C=kr4mQm2RW>r$aZCdCir7cAlno~7uO zSAx56`d3dhy`Vpb1m)%(Oi{Q$twTeniPUf6UfST{Rb1PlpY{%q)9~GT!N=z-@$3v% z%&O80@~+R;LL8Ea!URFl!1aMSGkFv9!LG+-s1sPPI#*`-c*Rrm8#fk^7d`3=tn_09 zIq7oDXVa&;<(5=S`UByN1g_Lq4eYOTS3-LUH0&qrok0 z)S8lQgtW9VqL_?J;JeIyz?5zJFpboN82m`g8&@gQ6p16EA@~Bls>`=j;n}%x0Qapd z;Vmsjedp$x`=po^r`q0|l2GL6K0Q$1;)g16j9CX%kL%0r>G?vnJm_ek0+#k3%e?%> zrH)rKJ!AQRDcy=GqIO-l4znSnkd7<+O7;nVy~3d7H`3gw4V3x>+*^LZ+)`rhhICvH zK8?BN35lchN&x+?f4rx(3ea$M+lN}mAE$SJ^d9C{eu?`~D;Oy!YIS+26}UeX>6rgZ zeko!0+)>)Z!Q9T`x5Tnc%T^0Z3f~VNQxhUV$$(q&Tm=i;6h|VwZYDgDum>}*?sBb? zAU@F?&z+Zi_Ty1<#U1d@rhO%^Kha(l-&mhFQ7f4?XBd8&PS1J`>-I;IklN; z7`2a!pLN3rju7lR=jo@Zj&k#9s6&$wEXDQ8?4*3-CkM9#?I2Te>~T?1j~f=s!-T>f z_H*uWijL3^fgs5x)ly$IY%)u&mrkb*SQFnf-M*OQ0Tt2Jn>K^(p?$}wWu$1`5bcH; zS`&jd$AupI0Bt&qY8Wu%2rvVA$ax>@=a`>Vd0g zY6d0vX2#jDNC0#J?#k z*IdR}%fM2-s7--bmBdaF&$T5`&O8pBV@VLO@5NZlQe}&l%AyChT1+w$c4+Kbru!%^ z*=<3y!Ra!=4rUyd%xjsF2;DuZ23?a5$XJI{R;0DNz5uANxbkj@lUfdPKXz>I-75$~1;tnzDdklB-z562{#wol2`M&t;U$( zAR>Isz{`6$5|oDidLvyLq^}0);ZRl@9y9RoXw8;5KQBxV3STfuw#%k3m6A5{IGTQT z(8Z0^nNlknv8UtJPnB~&H%Gm?JH1wyB87u~ih&=)o4L;JGm1)5$MpGa*m{8SG^_(4wFs$kV8%@0x5=f_UwH|C4s2Mp&@hixC zV2WP_hy`ka3g2AjL@4Mz|HM=LabP(UzlX(e)H{67@@O%Z!zimwCprAhZlKP$eSR9o z{Lyv$mS0b`|+VF9%+=K1)!@WgYIt?Hbk&Pl&u8A##fkaC5HtGhTtnPNPhgK48s zm3~&6Ou@$ZQ76XYZFODpdRv-@q)F!{lQ^aC2Fa7eiBLRa;WrFq>zqD;tTH<*AGN+} zoB+|j;(l6}a=AnJ6~4chdd)aqlE6X%0KE@~(tioxe=iSE$^GC~G(Z;4KmJvAb~Lj9 zfqqsF5|tI?x<$}4%edLA{64_Ou8FcPN4^VBj!;IW!)^5-5an_6^{l z(7-MsD)GWNrQ*sX*3;M94=R9iUY0`18**YEPu{sMr)d%fg)HR*UXDskC@oNpD>G!Hzy0<-SO#&iD(Yp}qFS!OP}7i!11U zXt#U*M}4!Hv8Y~3CnifqS*(ZoZ-W9>?@OtmVb8OH6L^)-cTXz%iV$Rm&vW&HMN9c= z=TT=N`y@XX{O|nTLwWrde`jyqyB75!cKp?>pM;Rn`?<)1jQzw(6NYxuwOgg@;3 zS@j19^KVQU^S@ZHf0gqcoqzWT5g_xxD^jtF$HYD`cB&WvfXM#>`{KcO4|}^2Ejh<^ z9&};a_8>N|()aU**`nEq+O^hI&+GNL6qXpNCpc;_>EjYI?I!L&uF56tzh|FZt`DOk z9u58^$StEsO{Q%!Zl;FfE2Kv>Wr#634#Y~Suhr&c(va}`cAVAHiFa-4=C>(cLCSs9 z`)-N(BTMGW+M222HxGYrgfyT+!#Fr1NQZl*qPDp;5sq6w0xRp88fI+MdGbq0x5!xa zSxxOL>Joo5+2Fh3Dpigq0R#$BqNEA1U;N`Q@8PEOX{mEH^0XzQ)#;ru$#pmk=sxP! zF6gPB>8o6XXsYHU(O*x!P@!On2XkZM3s7bUY#`rlm*skl$i9s>trd;HQB5Q)Fuw*% zXwfg!9SxSo0WZmSGz+KOlqQz3K%A94=Lj8OJY~oo?f;4z19y3G~+OiN41AuF5<6AosF}3!P5{; z0ZrwRgvki=rs+9xlo&FUk~Z4PZt+^*9XfRToWY?>r@8`&#GWd6!x&v|pd-AFFT^P= zl@!;hVMX~Y2#LfL2hsbH#?@>hThm2Ug$kWqZ6!UD#=Ec{x4G=ob(oFI<#)VKmzSw{ zUT;o_(`6et({W_EkFxjbB1!PHRFA)Zr00J*K(_#%;Su^6mUUk5S=z`v#H~g&9-x>l*`mzqs1bh%}WzfcTKDrK=kgo`>$}yxn0gn+L7?uj-#QT zpOhIa2%u#XzHuYJV=EI&y)H!URjs}nToEHv=+||qsUOO*ryxs0M5UN3b>Ph9r<)Ri z#XT6-Ew+OjgJD&^mnrsv%1kQy@^mP{f7eKQIynjtCr;_axkVqXjX^im?zJSlSNZOZ zrPL*-t5A(BA}+T%)oGLtt2Hyg9S*eZbvg6;j$eeEvGFFdwsNt9kZbA=27)%WNDBcZcUi5GrdOFTWaoj;#27SbUVadm+3hYf0X2c4L_z zoTcvAKD|D#6zX~>%qdUQT(tn2M*^?VRM|9g=oM@0%iS!Wi%(08q_Rzg<9P&bt-o^T z;34YAnE}&pV|)eVl`pToo#$#HPfVB|&xkz5ua*-5Cs*{l$;Q0nFS3RtPy3o;X^(_V zOl2n_APEyp7~{hMLdBQ_I_C09G|&qzJCL+-|`tKSNGBaaaY8u@8H zsbFIMgg2$4w~}lBgnZd&?|yw`^dA0atVRT!KRg^62YNDHAA+3k!y)y*2D=x|jxQ{n zU92rY)B~N!U2Ld;aqqUrm1nYZIV;9^Xb_;_4q6X)(qlR51Dcx)WW!G-icF>I^XmuE zR*Tx%gj{R2$crRAx8?RtmjV4lIGL`vJhQ}*^HFUxis!nobC0%a@pZQ$A%+CfaxGK( zekc7m7@?ryIxA#RK4lqXBV{=#Xl$5&-$(K=y@!DgI1v5*`TNu2m)bw~m3$ZeKHdh> zKD_^~_J=N$UkHD1?D*dL@nd{9-1l($qY>nntAA}4`Q8fhV|>@xemMQE@yCbYzt{cV zZSiA#*TsIwc>b*Wt5M_^#y_=+{KAm_w-`SgM}8swbtV))o%z?I2mKeM-x^7N!u@kA z$xp<$ft?R<|J+#e>vH@v*Z&K{{Nd{S^8)>t+`nA + 4.0.0 + + amazon-kinesis-deaggregator + A library for performing in-memory deaggregation of Kinesis aggregated stream records. + + com.amazonaws + amazon-kinesis-deaggregator + 2.0.0 + + jar + + + UTF-8 + + + https://aws.amazon.com/kinesis + + scm:git:git://github.com/awslabs/kinesis-aggregation.git + https://github.com/awslabs/kinesis-aggregation + + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + amazonwebservices + Amazon Web Services + https://aws.amazon.com + + developer + + + + + + clean compile + src/main/java + + + org.apache.maven.plugins + maven-compiler-plugin + 3.7.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.0 + + + attach-javadocs + + jar + + + + + public + true + false + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy + + + + + + package + + run + + + + + + maven-assembly-plugin + + + jar-with-dependencies + + + + + + + + com.amazonaws + amazon-kinesis-aggregator + 1.1.0 + test + + + software.amazon.kinesis + amazon-kinesis-client + 2.2.11 + + + com.fasterxml.jackson.core + jackson-databind + 2.8.11.3 + + + com.amazonaws + aws-lambda-java-events + 3.1.0 + + + org.apache.commons + commons-lang3 + 3.10 + + + com.amazonaws. + amazon-kinesis-aggregator + 1.1.0 + test + + + junit + junit + 4.13 + test + + + com.amazonaws + aws-lambda-java-core + 1.2.1 + + + diff --git a/java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/RecordDeaggregator.java b/java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/RecordDeaggregator.java new file mode 100644 index 0000000..3e0fb55 --- /dev/null +++ b/java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/RecordDeaggregator.java @@ -0,0 +1,160 @@ +/** + * Kinesis Aggregation/Deaggregation Libraries for Java + * + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.amazonaws.kinesis.deagg; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.kinesis.model.Record; +import software.amazon.kinesis.retrieval.AggregatorUtil; +import software.amazon.kinesis.retrieval.KinesisClientRecord; + +/** + * A Kinesis deaggregator convenience class. This class contains a number of + * static methods that provide different interfaces for deaggregating user + * records from an existing aggregated Kinesis record. This class is oriented + * towards deaggregating Kinesis records as provided by AWS Lambda, or through + * the Kinesis SDK. Parameterise the instance with the required types + * (supporting + * com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord + * or com.amazonaws.services.kinesis.model.Record only) + * + * NOTE: Any non-aggregated records passed to any deaggregation methods will be + * returned unchanged. + * + */ +public class RecordDeaggregator { + /** + * Interface used by a calling method to call the process function + * + */ + public interface KinesisUserRecordProcessor { + public Void process(List userRecords); + } + + private Record convertOne(KinesisEventRecord record) { + KinesisEvent.Record r = record.getKinesis(); + Record out = Record.builder().partitionKey(r.getPartitionKey()).encryptionType(r.getEncryptionType()) + .approximateArrivalTimestamp(r.getApproximateArrivalTimestamp().toInstant()) + .sequenceNumber(r.getSequenceNumber()).data(SdkBytes.fromByteBuffer(r.getData())).build(); + + return out; + + } + + private List convertToKinesis(List inputRecords) { + List response = new ArrayList<>(); + + inputRecords.stream().forEachOrdered(record -> { + response.add(KinesisClientRecord.fromRecord(convertOne(record))); + }); + + return response; + + } + + @SuppressWarnings("unchecked") + private List convertType(List inputRecords) throws Exception { + List records = null; + + if (inputRecords.size() > 0 && inputRecords.get(0) instanceof KinesisEventRecord) { + records = convertToKinesis((List) inputRecords); + } else if (inputRecords.size() > 0 && inputRecords.get(0) instanceof Record) { + records = new ArrayList<>(); + for (Record rec : (List) inputRecords) { + records.add(KinesisClientRecord.fromRecord((Record) rec)); + } + } else { + if (inputRecords.size() == 0) { + return new ArrayList(); + } else { + throw new Exception("Input Types must be Kinesis Event or Model Records"); + } + } + + return records; + } + + /** + * Method to process a set of Kinesis user records from a Stream of Kinesis + * Event Records using the Java 8 Streams API + * + * @param inputStream The Kinesis Records provided by AWS Lambda or the + * Kinesis SDK + * @param streamConsumer Instance implementing the Consumer interface to process + * the deaggregated UserRecords + * @return Void + */ + public Void stream(Stream inputStream, Consumer streamConsumer) throws Exception { + // deaggregate UserRecords from the Kinesis Records + + List streamList = inputStream.collect(Collectors.toList()); + List deaggregatedRecords = new AggregatorUtil().deaggregate(convertType(streamList)); + deaggregatedRecords.stream().forEachOrdered(streamConsumer); + + return null; + } + + /** + * Method to process a set of Kinesis user records from a list of Kinesis + * Records using pre-Streams style API + * + * @param inputRecords The Kinesis Records provided by AWS Lambda + * @param processor Instance implementing KinesisUserRecordProcessor + * @return Void + */ + public Void processRecords(List inputRecords, KinesisUserRecordProcessor processor) throws Exception { + // invoke provided processor + return processor.process(new AggregatorUtil().deaggregate(convertType(inputRecords))); + } + + /** + * Method to bulk deaggregate a set of Kinesis user records from a list of + * Kinesis Event Records. + * + * @param inputRecords The Kinesis Records provided by AWS Lambda + * @return A list of Kinesis UserRecord objects obtained by deaggregating the + * input list of KinesisEventRecords + */ + public List deaggregate(List inputRecords) throws Exception { + List outputRecords = new LinkedList<>(); + outputRecords.addAll(new AggregatorUtil().deaggregate(convertType(inputRecords))); + + return outputRecords; + } + + /** + * Method to deaggregate a single Kinesis record into a List of UserRecords + * + * @param inputRecord The Kinesis Record provided by AWS Lambda or Kinesis SDK + * @return A list of Kinesis UserRecord objects obtained by deaggregating the + * input list of KinesisEventRecords + */ + public List deaggregate(T inputRecord) throws Exception { + return new AggregatorUtil().deaggregate(convertType(Arrays.asList(inputRecord))); + } +} diff --git a/java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/util/DeaggregationUtils.java b/java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/util/DeaggregationUtils.java new file mode 100644 index 0000000..ed8cf8d --- /dev/null +++ b/java/KinesisDeaggregatorV2/src/main/java/com/amazonaws/kinesis/deagg/util/DeaggregationUtils.java @@ -0,0 +1,33 @@ +package com.amazonaws.kinesis.deagg.util; + +import java.util.ArrayList; +import java.util.List; + +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; + +import software.amazon.awssdk.core.SdkBytes; + +public class DeaggregationUtils { + public static software.amazon.awssdk.services.kinesis.model.Record convertOne(KinesisEventRecord record) { + KinesisEvent.Record r = record.getKinesis(); + software.amazon.awssdk.services.kinesis.model.Record out = software.amazon.awssdk.services.kinesis.model.Record + .builder().partitionKey(r.getPartitionKey()).encryptionType(r.getEncryptionType()) + .approximateArrivalTimestamp(r.getApproximateArrivalTimestamp().toInstant()) + .sequenceNumber(r.getSequenceNumber()).data(SdkBytes.fromByteBuffer(r.getData())).build(); + + return out; + } + + public static List convertToKinesis( + List inputRecords) { + List response = new ArrayList<>(); + + inputRecords.stream().forEachOrdered(record -> { + response.add(convertOne(record)); + }); + + return response; + + } +} diff --git a/java/KinesisDeaggregatorV2/src/sample/java/SampleLambdaEvent.json b/java/KinesisDeaggregatorV2/src/sample/java/SampleLambdaEvent.json new file mode 100644 index 0000000..b1171cc --- /dev/null +++ b/java/KinesisDeaggregatorV2/src/sample/java/SampleLambdaEvent.json @@ -0,0 +1,20 @@ +{ + "Records": [ + { + "kinesis": { + "partitionKey": "partitionKey-03", + "kinesisSchemaVersion": "1.0", + "data": "84mawgoDYWJjEicxOTE0MTU2NTgzNDQxNTg3NjYxNjgwMzE0NzMyNzc5MjI4MDM1NzAaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2JhGiAIABAAGhphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ehogCAAQABoaenl4d3Z1dHNycXBvbm1sa2ppaGdmZWRjYmEaIAgAEAAaGmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6GiAIABAAGhp6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYRogCAAQABoaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoaIAgAEAAaGnp5eHd2dXRzcnFwb25tbGtqaWhnZmVkY2Jh0I8WvwEDJiGD4YsiKIfUOw==", + "sequenceNumber": "49545115243490985018280067714973144582180062593244200961", + "approximateArrivalTimestamp": 1428537600 + }, + "eventSource": "aws:kinesis", + "eventID": "shardId-000000000000:49545115243490985018280067714973144582180062593244200961", + "invokeIdentityArn": "arn:aws:iam::EXAMPLE", + "eventVersion": "1.0", + "eventName": "aws:kinesis:record", + "eventSourceARN": "arn:aws:kinesis:EXAMPLE", + "awsRegion": "us-east-1" + } + ] +} \ No newline at end of file diff --git a/java/KinesisDeaggregatorV2/src/sample/java/com/amazonaws/kinesis/deagg/EchoHandler.java b/java/KinesisDeaggregatorV2/src/sample/java/com/amazonaws/kinesis/deagg/EchoHandler.java new file mode 100644 index 0000000..3dc778f --- /dev/null +++ b/java/KinesisDeaggregatorV2/src/sample/java/com/amazonaws/kinesis/deagg/EchoHandler.java @@ -0,0 +1,38 @@ +package com.amazonaws.kinesis.deagg; + +import java.util.List; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; + +import software.amazon.kinesis.retrieval.KinesisClientRecord; + +public class EchoHandler implements RequestHandler { + + @Override + public Void handleRequest(KinesisEvent event, Context context) { + LambdaLogger logger = context.getLogger(); + + // extract the records from the event + List records = event.getRecords(); + + logger.log(String.format("Recieved %s Raw Records", records.size())); + + try { + // now deaggregate the message contents + List deaggregated = new RecordDeaggregator().deaggregate(records); + logger.log(String.format("Received %s Deaggregated User Records", deaggregated.size())); + + deaggregated.stream().forEachOrdered(rec -> { + logger.log(rec.partitionKey()); + }); + } catch (Exception e) { + logger.log(e.getMessage()); + } + + return null; + } +} diff --git a/java/KinesisDeaggregatorV2/src/test/java/TestDirectDeaggregation.java b/java/KinesisDeaggregatorV2/src/test/java/TestDirectDeaggregation.java new file mode 100644 index 0000000..f449977 --- /dev/null +++ b/java/KinesisDeaggregatorV2/src/test/java/TestDirectDeaggregation.java @@ -0,0 +1,165 @@ +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.amazonaws.kinesis.agg.AggRecord; +import com.amazonaws.kinesis.agg.RecordAggregator; +import com.amazonaws.kinesis.deagg.RecordDeaggregator; +import com.amazonaws.kinesis.deagg.RecordDeaggregator.KinesisUserRecordProcessor; + +import software.amazon.awssdk.core.SdkBytes; +import software.amazon.awssdk.services.kinesis.model.Record; +import software.amazon.kinesis.retrieval.KinesisClientRecord; + +public class TestDirectDeaggregation { + private static final int c = 10; + private static Map checkset = new HashMap<>(); + private static List recordList = null; + private static final RecordDeaggregator deaggregator = new RecordDeaggregator<>(); + private static RecordAggregator aggregator = null; + private static AggRecord aggregated = null; + + private final class TestKinesisUserRecordProcessor + implements Consumer, KinesisUserRecordProcessor { + private int recordsProcessed = 0; + + public int getCount() { + return this.recordsProcessed; + } + + @Override + public void accept(KinesisClientRecord t) { + recordsProcessed += 1; + } + + @Override + public Void process(List userRecords) { + recordsProcessed += userRecords.size(); + + return null; + } + + } + + /* Verify that a provided set of UserRecords map 1:1 to the original checkset */ + private void verifyOneToOneMapping(List userRecords) { + userRecords.stream().forEachOrdered(userRecord -> { + // get the original checkset record by ID + Record toCheck = checkset.get(userRecord.partitionKey()); + + // confirm that toCheck is not null + assertNotNull("Found Original CheckSet Record", toCheck); + + // confirm that the data is the same + assertTrue("Data Correct", userRecord.data().compareTo(toCheck.data().asByteBuffer()) == 0); + }); + } + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + aggregator = new RecordAggregator(); + + recordList = new LinkedList<>(); + + // create 10 random records for testing + for (int i = 0; i < c; i++) { + // create trackable id + String id = UUID.randomUUID().toString(); + + // create a kinesis model record + byte[] data = RandomStringUtils.randomAlphabetic(20).getBytes(); + + Record r = Record.builder().partitionKey(id) + .approximateArrivalTimestamp(new Date(System.currentTimeMillis()).toInstant()) + .data(SdkBytes.fromByteArray(data)).build(); + recordList.add(r); + + // add the record to the check set + checkset.put(id, r); + + // add the record to the aggregated AggRecord // create an aggregated set of + aggregator.addUserRecord(id, data); + } + + // get the aggregated data + aggregated = aggregator.clearAndGet(); + assertEquals("Aggregated Record Count Correct", aggregated.getNumUserRecords(), c); + } + + @Test + public void testProcessor() throws Exception { + // create a counting record processor + TestKinesisUserRecordProcessor p = new TestKinesisUserRecordProcessor(); + + // invoke deaggregation on the static records with this processor + deaggregator.processRecords(recordList, p); + + assertEquals("Processed Record Count Correct", p.getCount(), recordList.size()); + } + + @Test + public void testStream() throws Exception { + // create a counting record processor + TestKinesisUserRecordProcessor p = new TestKinesisUserRecordProcessor(); + + // invoke deaggregation on the static records with this processor + deaggregator.stream(recordList.stream(), p); + + assertEquals("Processed Record Count Correct", p.getCount(), recordList.size()); + } + + @Test + public void testList() throws Exception { + // invoke deaggregation on the static records, returning a List of UserRecord + List records = deaggregator.deaggregate(recordList); + + assertEquals("Processed Record Count Correct", records.size(), recordList.size()); + verifyOneToOneMapping(records); + } + + @Test + public void testEmpty() throws Exception { + // invoke deaggregation on the static records, returning a List of UserRecord + List records = deaggregator.deaggregate(new ArrayList()); + + assertEquals("Processed Record Count Correct", records.size(), 0); + verifyOneToOneMapping(records); + } + + @Test + public void testOne() throws Exception { + // invoke deaggregation on the static records, returning a List of UserRecord + List records = deaggregator.deaggregate(recordList.get(0)); + + assertEquals("Processed Record Count Correct", records.size(), 1); + verifyOneToOneMapping(records); + } + + @Test + public void testAggregatedRecord() throws Exception { + // create a new KinesisEvent.Record from the aggregated data + Record r = Record.builder().partitionKey(aggregated.getPartitionKey()) + .approximateArrivalTimestamp(new Date(System.currentTimeMillis()).toInstant()) + .data(SdkBytes.fromByteArray(aggregated.toRecordBytes())).build(); + + // deaggregate the record + List userRecords = deaggregator.deaggregate(Arrays.asList(r)); + + assertEquals("Deaggregated Count Matches", aggregated.getNumUserRecords(), userRecords.size()); + verifyOneToOneMapping(userRecords); + } +} diff --git a/java/KinesisDeaggregatorV2/src/test/java/TestLambdaDeaggregation.java b/java/KinesisDeaggregatorV2/src/test/java/TestLambdaDeaggregation.java new file mode 100644 index 0000000..8082677 --- /dev/null +++ b/java/KinesisDeaggregatorV2/src/test/java/TestLambdaDeaggregation.java @@ -0,0 +1,172 @@ +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.BeforeClass; +import org.junit.Test; + +import com.amazonaws.kinesis.agg.AggRecord; +import com.amazonaws.kinesis.agg.RecordAggregator; +import com.amazonaws.kinesis.deagg.RecordDeaggregator; +import com.amazonaws.kinesis.deagg.RecordDeaggregator.KinesisUserRecordProcessor; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent; +import com.amazonaws.services.lambda.runtime.events.KinesisEvent.KinesisEventRecord; + +import software.amazon.kinesis.retrieval.KinesisClientRecord; + +public class TestLambdaDeaggregation { + private static final int c = 10; + private static Map checkset = new HashMap<>(); + private static List recordList = null; + private static final RecordDeaggregator deaggregator = new RecordDeaggregator<>(); + private static RecordAggregator aggregator = null; + private static AggRecord aggregated = null; + + private final class TestKinesisUserRecordProcessor + implements Consumer, KinesisUserRecordProcessor { + private int recordsProcessed = 0; + + public int getCount() { + return this.recordsProcessed; + } + + @Override + public void accept(KinesisClientRecord t) { + recordsProcessed += 1; + } + + @Override + public Void process(List userRecords) { + recordsProcessed += userRecords.size(); + + return null; + } + + } + + /* Verify that a provided set of UserRecords map 1:1 to the original checkset */ + private void verifyOneToOneMapping(List userRecords) { + userRecords.stream().forEachOrdered(userRecord -> { + // get the original checkset record by ID + KinesisEventRecord toCheck = checkset.get(userRecord.partitionKey()); + + // confirm that toCheck is not null + assertNotNull("Found Original CheckSet Record", toCheck); + + // confirm that the data is the same + assertTrue("Data Correct", userRecord.data().compareTo(toCheck.getKinesis().getData()) == 0); + }); + } + + @BeforeClass + public static void setUpBeforeClass() throws Exception { + aggregator = new RecordAggregator(); + + recordList = new LinkedList<>(); + + // create 10 random records for testing + for (int i = 0; i < c; i++) { + // create trackable id + String id = UUID.randomUUID().toString(); + + // create a kinesis model record + byte[] data = RandomStringUtils.randomAlphabetic(20).getBytes(); + + KinesisEvent.Record r = new KinesisEvent.Record(); + r.withPartitionKey(id).withApproximateArrivalTimestamp(new Date(System.currentTimeMillis())) + .withData(ByteBuffer.wrap(data)); + KinesisEventRecord ker = new KinesisEventRecord(); + ker.setKinesis(r); + recordList.add(ker); + + // add the record to the check set + checkset.put(id, ker); + + // add the record to the aggregated AggRecord // create an aggregated set of + aggregator.addUserRecord(id, data); + } + + // get the aggregated data + aggregated = aggregator.clearAndGet(); + assertEquals("Aggregated Record Count Correct", aggregated.getNumUserRecords(), c); + } + + @Test + public void testProcessor() throws Exception { + // create a counting record processor + TestKinesisUserRecordProcessor p = new TestKinesisUserRecordProcessor(); + + // invoke deaggregation on the static records with this processor + deaggregator.processRecords(recordList, p); + + assertEquals("Processed Record Count Correct", p.getCount(), recordList.size()); + } + + @Test + public void testStream() throws Exception { + // create a counting record processor + TestKinesisUserRecordProcessor p = new TestKinesisUserRecordProcessor(); + + // invoke deaggregation on the static records with this processor + deaggregator.stream(recordList.stream(), p); + + assertEquals("Processed Record Count Correct", p.getCount(), recordList.size()); + } + + @Test + public void testList() throws Exception { + // invoke deaggregation on the static records, returning a List of UserRecord + List records = deaggregator.deaggregate(recordList); + + assertEquals("Processed Record Count Correct", records.size(), recordList.size()); + verifyOneToOneMapping(records); + } + + @Test + public void testAggregatedRecord() throws Exception { + // create a new KinesisEvent.Record from the aggregated data + KinesisEvent.Record r = new KinesisEvent.Record(); + r.setPartitionKey(aggregated.getPartitionKey()); + r.setApproximateArrivalTimestamp(new Date(System.currentTimeMillis())); + r.setData(ByteBuffer.wrap(aggregated.toRecordBytes())); + r.setKinesisSchemaVersion("1.0"); + KinesisEventRecord ker = new KinesisEventRecord(); + ker.setKinesis(r); + + // deaggregate the record + List userRecords = deaggregator.deaggregate(Arrays.asList(ker)); + + assertEquals("Deaggregated Count Matches", aggregated.getNumUserRecords(), userRecords.size()); + verifyOneToOneMapping(userRecords); + } + + @Test + public void testEmpty() throws Exception { + // invoke deaggregation on the static records, returning a List of UserRecord + List records = deaggregator.deaggregate(new ArrayList()); + + assertEquals("Processed Record Count Correct", records.size(), 0); + verifyOneToOneMapping(records); + } + + @Test + public void testOne() throws Exception { + // invoke deaggregation on the static records, returning a List of UserRecord + List records = deaggregator.deaggregate(recordList.get(0)); + + assertEquals("Processed Record Count Correct", records.size(), 1); + verifyOneToOneMapping(records); + } +} From a7f0849d2275eab7afb559a6ce012a51d11414d6 Mon Sep 17 00:00:00 2001 From: IanMeyers Date: Fri, 5 Jun 2020 16:47:11 +0100 Subject: [PATCH 2/2] Doc Update --- java/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/java/README.md b/java/README.md index a96e795..527c0b3 100644 --- a/java/README.md +++ b/java/README.md @@ -8,7 +8,14 @@ The [KinesisAggregator](KinesisAggregator) subproject contains Java classes that ## KinesisDeaggregator -The [KinesisDeaggregator](KinesisDeaggregator) subproject contains Java classes that allow you to deaggregate records that were transmitted using the [Kinesis Aggregated Record Format](https://github.com/awslabs/amazon-kinesis-producer/blob/master/aggregation-format.md), including those transmitted by the Kinesis Producer Library. This library will allow you to deaggregate aggregated records in any Java environment, including AWS Lambda. +The Deaggregation subprojects contain Java classes that allow you to deaggregate records that were transmitted using the [Kinesis Aggregated Record Format](https://github.com/awslabs/amazon-kinesis-producer/blob/master/aggregation-format.md), including those transmitted by the Kinesis Producer Library. This library will allow you to deaggregate aggregated records in any Java environment, including AWS Lambda. + +There are 2 versions of Deaggregator modules, based upon the AWS SDK version you are using: + +| SDK | Project | +| --- | ------- | +|Version 1 | [KinesisDeaggregator](KinesisDeaggregator) | +|Version 2 | [KinesisDeaggregatorV2](KinesisDeaggregatorV2) | ## KinesisTestConsumers