Adding testing architecture and KCL 2.x basic polling and streaming tests
This commit is contained in:
parent
a8fc1367c6
commit
f7d286ac2e
15 changed files with 1271 additions and 0 deletions
|
|
@ -207,6 +207,10 @@
|
||||||
<name>sqlite4java.library.path</name>
|
<name>sqlite4java.library.path</name>
|
||||||
<value>${sqlite4java.libpath}</value>
|
<value>${sqlite4java.libpath}</value>
|
||||||
</property>
|
</property>
|
||||||
|
<property>
|
||||||
|
<name>credentials</name>
|
||||||
|
<value>${credentials}</value>
|
||||||
|
</property>
|
||||||
</systemProperties>
|
</systemProperties>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
package software.amazon.kinesis.common;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import software.amazon.kinesis.utils.RecordValidatorQueue;
|
||||||
|
|
||||||
|
public class RecordValidatorQueueTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RecordValidatorQueue recordValidator;
|
||||||
|
|
||||||
|
private static final String shardId = "ABC";
|
||||||
|
|
||||||
|
private final int outOfOrderError = -1;
|
||||||
|
private final int missingRecordError = -2;
|
||||||
|
|
||||||
|
private final int noError = 0;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validationFailedRecordOutOfOrderTest() {
|
||||||
|
recordValidator = new RecordValidatorQueue();
|
||||||
|
recordValidator.add(shardId, "0");
|
||||||
|
recordValidator.add(shardId, "1");
|
||||||
|
recordValidator.add(shardId, "3");
|
||||||
|
recordValidator.add(shardId, "2");
|
||||||
|
|
||||||
|
int error = recordValidator.validateRecords( 4 );
|
||||||
|
Assert.assertEquals(outOfOrderError, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validationFailedMissingRecordTest() {
|
||||||
|
recordValidator = new RecordValidatorQueue();
|
||||||
|
recordValidator.add(shardId, "0");
|
||||||
|
recordValidator.add(shardId, "1");
|
||||||
|
recordValidator.add(shardId, "2");
|
||||||
|
recordValidator.add(shardId, "3");
|
||||||
|
|
||||||
|
int error = recordValidator.validateRecords( 5 );
|
||||||
|
Assert.assertEquals(missingRecordError, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validRecordsTest() {
|
||||||
|
recordValidator = new RecordValidatorQueue();
|
||||||
|
recordValidator.add(shardId, "0");
|
||||||
|
recordValidator.add(shardId, "1");
|
||||||
|
recordValidator.add(shardId, "2");
|
||||||
|
recordValidator.add(shardId, "3");
|
||||||
|
|
||||||
|
int error = recordValidator.validateRecords( 4 );
|
||||||
|
Assert.assertEquals(noError, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
package software.amazon.kinesis.config;
|
||||||
|
|
||||||
|
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||||
|
import software.amazon.kinesis.utils.RecordValidatorQueue;
|
||||||
|
import software.amazon.kinesis.integration_tests.TestRecordProcessorFactoryV2;
|
||||||
|
import software.amazon.kinesis.utils.KCLVersion;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import software.amazon.awssdk.arns.Arn;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
|
||||||
|
import software.amazon.awssdk.http.Protocol;
|
||||||
|
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
|
||||||
|
import software.amazon.awssdk.http.async.SdkAsyncHttpClient;
|
||||||
|
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClientBuilder;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClientBuilder;
|
||||||
|
import software.amazon.awssdk.services.kinesis.KinesisAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.kinesis.KinesisAsyncClientBuilder;
|
||||||
|
import software.amazon.awssdk.utils.AttributeMap;
|
||||||
|
import software.amazon.kinesis.common.ConfigsBuilder;
|
||||||
|
import software.amazon.kinesis.common.InitialPositionInStream;
|
||||||
|
import software.amazon.kinesis.processor.ShardRecordProcessorFactory;
|
||||||
|
import software.amazon.kinesis.retrieval.RetrievalConfig;
|
||||||
|
import software.amazon.kinesis.utils.OdinCredentialsHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Inet4Address;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface KCLAppConfig {
|
||||||
|
|
||||||
|
String getStreamName();
|
||||||
|
|
||||||
|
default String getStreamArn() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getShardCount();
|
||||||
|
|
||||||
|
String getApplicationName();
|
||||||
|
|
||||||
|
default KCLVersion getKCLVersion() {
|
||||||
|
return KCLVersion.KCL2X;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean canaryMonitorEnabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getEndpoint();
|
||||||
|
|
||||||
|
Region getRegion();
|
||||||
|
|
||||||
|
boolean isProd();
|
||||||
|
|
||||||
|
default boolean durabilityCheck() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// "default" profile, should match with profiles listed in "cat ~/.aws/config"
|
||||||
|
String getProfile();
|
||||||
|
|
||||||
|
default String odinMaterialName() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
default AWSCredentialsProvider getSyncAwsCredentials() throws IOException {
|
||||||
|
return OdinCredentialsHelper.getSyncAwsCredentialsFromMaterialSet( odinMaterialName() );
|
||||||
|
}
|
||||||
|
|
||||||
|
default AwsCredentialsProvider getAsyncAwsCredentials() throws IOException {
|
||||||
|
return OdinCredentialsHelper.getAsyncAwsCredentialsFromMaterialSet( odinMaterialName() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// '-1' means round robin across 0, 5_000, 15_000, 30_000 milliseconds delay.
|
||||||
|
// The delay period is picked according to current time, so expected to be unpredictable across different KCL runs.
|
||||||
|
// '0' means PassThroughRecordProcessor
|
||||||
|
// Any other constant will delay according to the specified value.
|
||||||
|
long getProcessingDelayMillis();
|
||||||
|
|
||||||
|
InitialPositionInStream getKclInitialPosition();
|
||||||
|
|
||||||
|
Protocol getConsumerProtocol();
|
||||||
|
|
||||||
|
Protocol getProducerProtocol();
|
||||||
|
|
||||||
|
ProducerConfig getProducerConfig();
|
||||||
|
|
||||||
|
ReshardConfig getReshardConfig();
|
||||||
|
|
||||||
|
default MultiStreamRotatorConfig getMultiStreamRotatorConfig() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
default KinesisAsyncClient buildConsumerClient() throws URISyntaxException, IOException {
|
||||||
|
return buildAsyncKinesisClient( getConsumerProtocol() );
|
||||||
|
}
|
||||||
|
|
||||||
|
default KinesisAsyncClient buildProducerClient() throws URISyntaxException, IOException {
|
||||||
|
return buildAsyncKinesisClient( getProducerProtocol() );
|
||||||
|
}
|
||||||
|
|
||||||
|
default KinesisAsyncClient buildAsyncKinesisClient( Protocol protocol ) throws URISyntaxException, IOException {
|
||||||
|
return buildAsyncKinesisClient( Optional.ofNullable( protocol ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
default KinesisAsyncClient buildAsyncKinesisClient( Optional< Protocol > protocol ) throws URISyntaxException, IOException {
|
||||||
|
|
||||||
|
// Setup H2 client config.
|
||||||
|
final NettyNioAsyncHttpClient.Builder builder = NettyNioAsyncHttpClient.builder()
|
||||||
|
.maxConcurrency( Integer.MAX_VALUE );
|
||||||
|
|
||||||
|
if ( protocol.isPresent() ) {
|
||||||
|
builder.protocol( protocol.get() );
|
||||||
|
}
|
||||||
|
|
||||||
|
final SdkAsyncHttpClient sdkAsyncHttpClient =
|
||||||
|
builder.buildWithDefaults( AttributeMap.builder().put( SdkHttpConfigurationOption.TRUST_ALL_CERTIFICATES, true ).build() );
|
||||||
|
|
||||||
|
// Setup client builder by default values
|
||||||
|
final KinesisAsyncClientBuilder kinesisAsyncClientBuilder = KinesisAsyncClient.builder().region( getRegion() );
|
||||||
|
|
||||||
|
// Override endpoint if not one of the Prod stacks.
|
||||||
|
// if (!isProd()) {
|
||||||
|
// kinesisAsyncClientBuilder
|
||||||
|
// .endpointOverride(new URI(getEndpoint()));
|
||||||
|
// }
|
||||||
|
|
||||||
|
kinesisAsyncClientBuilder.httpClient( sdkAsyncHttpClient );
|
||||||
|
|
||||||
|
|
||||||
|
if ( getAsyncAwsCredentials() != null ) {
|
||||||
|
kinesisAsyncClientBuilder.credentialsProvider( getAsyncAwsCredentials() );
|
||||||
|
} else if ( getProfile() != null ) {
|
||||||
|
kinesisAsyncClientBuilder.credentialsProvider( ProfileCredentialsProvider.builder().profileName( getProfile() ).build() );
|
||||||
|
} else {
|
||||||
|
kinesisAsyncClientBuilder.credentialsProvider( DefaultCredentialsProvider.create() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return kinesisAsyncClientBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
default DynamoDbAsyncClient buildAsyncDynamoDbClient() throws IOException {
|
||||||
|
final DynamoDbAsyncClientBuilder builder = DynamoDbAsyncClient.builder().region( getRegion() );
|
||||||
|
|
||||||
|
if ( getAsyncAwsCredentials() != null ) {
|
||||||
|
builder.credentialsProvider( getAsyncAwsCredentials() );
|
||||||
|
} else if ( getProfile() != null ) {
|
||||||
|
builder.credentialsProvider( ProfileCredentialsProvider.builder().profileName( getProfile() ).build() );
|
||||||
|
} else {
|
||||||
|
builder.credentialsProvider( DefaultCredentialsProvider.create() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
default CloudWatchAsyncClient buildAsyncCloudWatchClient() throws IOException {
|
||||||
|
final CloudWatchAsyncClientBuilder builder = CloudWatchAsyncClient.builder().region( getRegion() );
|
||||||
|
|
||||||
|
if ( getAsyncAwsCredentials() != null ) {
|
||||||
|
builder.credentialsProvider( getAsyncAwsCredentials() );
|
||||||
|
} else if ( getProfile() != null ) {
|
||||||
|
builder.credentialsProvider( ProfileCredentialsProvider.builder().profileName( getProfile() ).build() );
|
||||||
|
} else {
|
||||||
|
builder.credentialsProvider( DefaultCredentialsProvider.create() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
default String getWorkerId() throws UnknownHostException {
|
||||||
|
return Inet4Address.getLocalHost().getHostName();
|
||||||
|
}
|
||||||
|
|
||||||
|
default RecordValidatorQueue getRecordValidator() {
|
||||||
|
return new RecordValidatorQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
default ShardRecordProcessorFactory getShardRecordProcessorFactory() {
|
||||||
|
if (getKCLVersion() == KCLVersion.KCL2X) {
|
||||||
|
return new TestRecordProcessorFactoryV2( getRecordValidator() );
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default ConfigsBuilder getConfigsBuilder() throws IOException, URISyntaxException {
|
||||||
|
return getConfigsBuilder( "" );
|
||||||
|
}
|
||||||
|
|
||||||
|
default ConfigsBuilder getConfigsBuilder( String workerIdSuffix ) throws IOException, URISyntaxException {
|
||||||
|
final String workerId = getWorkerId() + workerIdSuffix;
|
||||||
|
if ( getStreamArn() == null ) {
|
||||||
|
return new ConfigsBuilder( getStreamName(), getApplicationName(), buildConsumerClient(), buildAsyncDynamoDbClient(),
|
||||||
|
buildAsyncCloudWatchClient(), workerId, getShardRecordProcessorFactory() );
|
||||||
|
} else {
|
||||||
|
return new ConfigsBuilder( Arn.fromString( getStreamArn() ), getApplicationName(), buildConsumerClient(), buildAsyncDynamoDbClient(),
|
||||||
|
buildAsyncCloudWatchClient(), workerId, getShardRecordProcessorFactory() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RetrievalConfig getRetrievalConfig() throws IOException, URISyntaxException;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
class ProducerConfig {
|
||||||
|
private boolean isBatchPut;
|
||||||
|
private int batchSize;
|
||||||
|
private int recordSizeKB;
|
||||||
|
private long callPeriodMills;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
class ReshardConfig {
|
||||||
|
private Double[] reshardingFactorCycle;
|
||||||
|
private int numReshardCycles;
|
||||||
|
private long reshardFrequencyMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
class MultiStreamRotatorConfig {
|
||||||
|
private int totalStreams;
|
||||||
|
private int maxStreamsToProcess;
|
||||||
|
private long streamsRotationMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
package software.amazon.kinesis.config;
|
||||||
|
|
||||||
|
import software.amazon.awssdk.http.Protocol;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.kinesis.common.InitialPositionInStream;
|
||||||
|
import software.amazon.kinesis.common.InitialPositionInStreamExtended;
|
||||||
|
import software.amazon.kinesis.retrieval.RetrievalConfig;
|
||||||
|
import software.amazon.kinesis.retrieval.polling.PollingConfig;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class KCLReleaseCanary2XPollingH1TestConfig implements KCLAppConfig {
|
||||||
|
@Override
|
||||||
|
public String getStreamName() {
|
||||||
|
return "KCLReleaseCanary2XPollingH1TestConfig";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getShardCount() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getApplicationName() {
|
||||||
|
return "KCLReleaseCanary2XPollingH1TestConfigApplication";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEndpoint() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region getRegion() {
|
||||||
|
return Region.US_WEST_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isProd() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProfile() {
|
||||||
|
String iamUser = System.getProperty( "credentials" );
|
||||||
|
return iamUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getProcessingDelayMillis() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InitialPositionInStream getKclInitialPosition() {
|
||||||
|
return InitialPositionInStream.TRIM_HORIZON;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Protocol getConsumerProtocol() {
|
||||||
|
return Protocol.HTTP1_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Protocol getProducerProtocol() {
|
||||||
|
return Protocol.HTTP1_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProducerConfig getProducerConfig() {
|
||||||
|
return ProducerConfig.builder()
|
||||||
|
.isBatchPut( false )
|
||||||
|
.batchSize( 1 )
|
||||||
|
.recordSizeKB( 60 )
|
||||||
|
.callPeriodMills( 100 )
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReshardConfig getReshardConfig() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RetrievalConfig getRetrievalConfig() throws IOException, URISyntaxException {
|
||||||
|
LocalDateTime d = LocalDateTime.now();
|
||||||
|
d = d.minusMinutes( 5 );
|
||||||
|
Instant instant = d.atZone( ZoneId.systemDefault() ).toInstant();
|
||||||
|
Date startStreamTime = Date.from( instant );
|
||||||
|
|
||||||
|
InitialPositionInStreamExtended initialPosition = InitialPositionInStreamExtended
|
||||||
|
.newInitialPositionAtTimestamp( startStreamTime );
|
||||||
|
|
||||||
|
RetrievalConfig config = getConfigsBuilder().retrievalConfig();
|
||||||
|
config.initialPositionInStreamExtended( initialPosition );
|
||||||
|
config.retrievalSpecificConfig( new PollingConfig( getStreamName(), buildConsumerClient() ) );
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
package software.amazon.kinesis.config;
|
||||||
|
|
||||||
|
import software.amazon.awssdk.http.Protocol;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.kinesis.common.InitialPositionInStream;
|
||||||
|
import software.amazon.kinesis.common.InitialPositionInStreamExtended;
|
||||||
|
import software.amazon.kinesis.retrieval.RetrievalConfig;
|
||||||
|
import software.amazon.kinesis.retrieval.polling.PollingConfig;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class KCLReleaseCanary2XPollingH2TestConfig implements KCLAppConfig {
|
||||||
|
@Override
|
||||||
|
public String getStreamName() {
|
||||||
|
return "KCLTest3";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getShardCount() {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getApplicationName() {
|
||||||
|
return "KCLReleaseCanary2XPollingH2TestApplication";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEndpoint() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region getRegion() {
|
||||||
|
return Region.US_WEST_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isProd() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProfile() {
|
||||||
|
String iamUser = System.getProperty( "credentials" );
|
||||||
|
return iamUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getProcessingDelayMillis() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InitialPositionInStream getKclInitialPosition() {
|
||||||
|
return InitialPositionInStream.TRIM_HORIZON;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Protocol getConsumerProtocol() {
|
||||||
|
return Protocol.HTTP2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Protocol getProducerProtocol() {
|
||||||
|
return Protocol.HTTP1_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProducerConfig getProducerConfig() {
|
||||||
|
return ProducerConfig.builder()
|
||||||
|
.isBatchPut( false )
|
||||||
|
.batchSize( 1 )
|
||||||
|
.recordSizeKB( 60 )
|
||||||
|
.callPeriodMills( 100 )
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReshardConfig getReshardConfig() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RetrievalConfig getRetrievalConfig() throws IOException, URISyntaxException {
|
||||||
|
LocalDateTime d = LocalDateTime.now();
|
||||||
|
d = d.minusMinutes( 5 );
|
||||||
|
Instant instant = d.atZone( ZoneId.systemDefault() ).toInstant();
|
||||||
|
Date startStreamTime = Date.from( instant );
|
||||||
|
|
||||||
|
InitialPositionInStreamExtended initialPosition = InitialPositionInStreamExtended
|
||||||
|
.newInitialPositionAtTimestamp( startStreamTime );
|
||||||
|
|
||||||
|
RetrievalConfig config = getConfigsBuilder().retrievalConfig();
|
||||||
|
config.initialPositionInStreamExtended( initialPosition );
|
||||||
|
config.retrievalSpecificConfig( new PollingConfig( getStreamName(), buildConsumerClient() ) );
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
package software.amazon.kinesis.config;
|
||||||
|
|
||||||
|
import software.amazon.awssdk.http.Protocol;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.kinesis.common.InitialPositionInStream;
|
||||||
|
import software.amazon.kinesis.common.InitialPositionInStreamExtended;
|
||||||
|
import software.amazon.kinesis.retrieval.RetrievalConfig;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class KCLReleaseCanary2XStreamingTestConfig implements KCLAppConfig {
|
||||||
|
@Override
|
||||||
|
public String getStreamName() {
|
||||||
|
return "KCLReleaseCanary2XStreamingTestStream";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getShardCount() {
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getApplicationName() {
|
||||||
|
return "KCLReleaseCanary2XStreamingTestApplication";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getEndpoint() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Region getRegion() {
|
||||||
|
return Region.US_WEST_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isProd() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getProfile() {
|
||||||
|
String iamUser = System.getProperty( "credentials" );
|
||||||
|
return iamUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getProcessingDelayMillis() {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InitialPositionInStream getKclInitialPosition() {
|
||||||
|
return InitialPositionInStream.TRIM_HORIZON;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Protocol getConsumerProtocol() {
|
||||||
|
return Protocol.HTTP2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Protocol getProducerProtocol() {
|
||||||
|
return Protocol.HTTP1_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ProducerConfig getProducerConfig() {
|
||||||
|
return ProducerConfig.builder()
|
||||||
|
.isBatchPut( false )
|
||||||
|
.batchSize( 1 )
|
||||||
|
.recordSizeKB( 60 )
|
||||||
|
.callPeriodMills( 100 )
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ReshardConfig getReshardConfig() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RetrievalConfig getRetrievalConfig() throws IOException, URISyntaxException {
|
||||||
|
LocalDateTime d = LocalDateTime.now();
|
||||||
|
d = d.minusMinutes( 5 );
|
||||||
|
Instant instant = d.atZone( ZoneId.systemDefault() ).toInstant();
|
||||||
|
Date startStreamTime = Date.from( instant );
|
||||||
|
|
||||||
|
InitialPositionInStreamExtended initialPosition = InitialPositionInStreamExtended
|
||||||
|
.newInitialPositionAtTimestamp( startStreamTime );
|
||||||
|
|
||||||
|
RetrievalConfig config = getConfigsBuilder().retrievalConfig();
|
||||||
|
config.initialPositionInStreamExtended( initialPosition );
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package software.amazon.kinesis.integration_tests;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.junit.Test;
|
||||||
|
import software.amazon.kinesis.config.KCLAppConfig;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class KCL2XIntegrationTest {
|
||||||
|
|
||||||
|
private static final String CONFIG_PACKAGE = "software.amazon.kinesis.config";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void KCLReleaseCanary2XPollingH2Test() throws Exception {
|
||||||
|
String[] configName = { "KCLReleaseCanary2XPollingH2TestConfig" };
|
||||||
|
KCLAppConfig consumerConfig = (KCLAppConfig) Class.forName(CONFIG_PACKAGE + "." + configName[0]).newInstance();
|
||||||
|
TestConsumerV2 consumer = new TestConsumerV2( consumerConfig );
|
||||||
|
consumer.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void KCLReleaseCanary2XPollingH1Test() throws Exception {
|
||||||
|
String[] configName = { "KCLReleaseCanary2XPollingH1TestConfig" };
|
||||||
|
KCLAppConfig consumerConfig = (KCLAppConfig) Class.forName(CONFIG_PACKAGE + "." + configName[0]).newInstance();
|
||||||
|
TestConsumerV2 consumer = new TestConsumerV2( consumerConfig );
|
||||||
|
consumer.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void KCLReleaseCanary2XStreamingTest() throws Exception {
|
||||||
|
String[] configName = { "KCLReleaseCanary2XStreamingTestConfig" };
|
||||||
|
KCLAppConfig consumerConfig = (KCLAppConfig) Class.forName(CONFIG_PACKAGE + "." + configName[0]).newInstance();
|
||||||
|
TestConsumerV2 consumer = new TestConsumerV2( consumerConfig );
|
||||||
|
consumer.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
package software.amazon.kinesis.integration_tests;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import software.amazon.awssdk.http.Protocol;
|
||||||
|
import software.amazon.awssdk.services.kinesis.KinesisAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.kinesis.model.CreateStreamRequest;
|
||||||
|
import software.amazon.awssdk.services.kinesis.model.DescribeStreamSummaryRequest;
|
||||||
|
import software.amazon.awssdk.services.kinesis.model.DescribeStreamSummaryResponse;
|
||||||
|
import software.amazon.awssdk.services.kinesis.model.ResourceNotFoundException;
|
||||||
|
import software.amazon.awssdk.services.kinesis.model.StreamStatus;
|
||||||
|
import software.amazon.kinesis.config.KCLAppConfig;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import static java.lang.Thread.sleep;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Slf4j
|
||||||
|
public class StreamExistenceManager {
|
||||||
|
private final KinesisAsyncClient client;
|
||||||
|
private final KCLAppConfig testConfig;
|
||||||
|
|
||||||
|
public StreamExistenceManager(KCLAppConfig config) throws URISyntaxException, IOException {
|
||||||
|
this.testConfig = config;
|
||||||
|
this.client = config.buildAsyncKinesisClient(Protocol.HTTP1_1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StreamExistenceManager newManager(KCLAppConfig config) throws URISyntaxException, IOException {
|
||||||
|
return new StreamExistenceManager(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isStreamActive(String streamName) {
|
||||||
|
|
||||||
|
DescribeStreamSummaryRequest request = DescribeStreamSummaryRequest.builder().streamName(streamName).build();
|
||||||
|
|
||||||
|
final CompletableFuture<DescribeStreamSummaryResponse> describeStreamSummaryResponseCompletableFuture = client.describeStreamSummary(request);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final DescribeStreamSummaryResponse response = describeStreamSummaryResponseCompletableFuture.get(30, TimeUnit.SECONDS);
|
||||||
|
boolean isActive = response.streamDescriptionSummary().streamStatus().equals(StreamStatus.ACTIVE);
|
||||||
|
if (!isActive) {
|
||||||
|
throw new RuntimeException("Stream is not active, instead in status: " + response.streamDescriptionSummary().streamStatus());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
if (e.getCause() instanceof ResourceNotFoundException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createStream(String streamName, int shardCount) {
|
||||||
|
CreateStreamRequest request = CreateStreamRequest.builder().streamName(streamName).shardCount(shardCount).build();
|
||||||
|
try {
|
||||||
|
client.createStream(request).get(30, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to create stream with name " + streamName, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
while (true) {
|
||||||
|
i++;
|
||||||
|
if (i > 100) {
|
||||||
|
throw new RuntimeException("Failed stream creation, did not transition into active");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
boolean isActive = isStreamActive(streamName);
|
||||||
|
if (isActive) {
|
||||||
|
log.info("Succesfully created the stream " + streamName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
try {
|
||||||
|
sleep(10_000); // 10 secs backoff.
|
||||||
|
} catch (InterruptedException e1) {
|
||||||
|
log.error("Failed to sleep");
|
||||||
|
}
|
||||||
|
log.info("Stream {} is not active yet, exception: ", streamName, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkStreamAndCreateIfNecessary(String streamName) {
|
||||||
|
|
||||||
|
if (!isStreamActive(streamName)) {
|
||||||
|
createStream(streamName, testConfig.getShardCount());
|
||||||
|
}
|
||||||
|
log.info("Using stream " + streamName + " in endpoint " + testConfig.getEndpoint() + " with region " + testConfig.getRegion());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package software.amazon.kinesis.integration_tests;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import software.amazon.awssdk.core.SdkBytes;
|
||||||
|
import software.amazon.awssdk.regions.Region;
|
||||||
|
import software.amazon.awssdk.services.kinesis.KinesisAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.kinesis.model.PutRecordRequest;
|
||||||
|
import software.amazon.kinesis.common.KinesisClientUtil;
|
||||||
|
import software.amazon.kinesis.config.KCLAppConfig;
|
||||||
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
public class TestConsumer {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger( TestConsumer.class );
|
||||||
|
public final KCLAppConfig consumerConfig;
|
||||||
|
public final Region region;
|
||||||
|
public final String streamName;
|
||||||
|
public final KinesisAsyncClient kinesisClient;
|
||||||
|
public int successfulPutRecords = 0;
|
||||||
|
public BigInteger payloadCounter = new BigInteger( "0" );
|
||||||
|
|
||||||
|
|
||||||
|
public TestConsumer( KCLAppConfig consumerConfig ) {
|
||||||
|
this.consumerConfig = consumerConfig;
|
||||||
|
this.region = consumerConfig.getRegion();
|
||||||
|
this.streamName = consumerConfig.getStreamName();
|
||||||
|
this.kinesisClient = KinesisClientUtil.createKinesisAsyncClient( KinesisAsyncClient.builder().region( this.region ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void publishRecord() {
|
||||||
|
PutRecordRequest request;
|
||||||
|
try {
|
||||||
|
request = PutRecordRequest.builder()
|
||||||
|
.partitionKey( RandomStringUtils.randomAlphabetic( 5, 20 ) )
|
||||||
|
.streamName( streamName )
|
||||||
|
.data( SdkBytes.fromByteBuffer( wrapWithCounter( 5, payloadCounter ) ) ) // 1024 is 1 KB
|
||||||
|
.build();
|
||||||
|
kinesisClient.putRecord( request ).get();
|
||||||
|
// Increment the payload counter if the putRecord call was successful
|
||||||
|
payloadCounter = payloadCounter.add( new BigInteger( "1" ) );
|
||||||
|
successfulPutRecords += 1;
|
||||||
|
} catch ( InterruptedException e ) {
|
||||||
|
log.info( "Interrupted, assuming shutdown." );
|
||||||
|
} catch ( ExecutionException e ) {
|
||||||
|
log.error( "Error during publishRecord. Will try again next cycle", e );
|
||||||
|
} catch ( RuntimeException e ) {
|
||||||
|
log.error( "Error while creating request", e );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ByteBuffer wrapWithCounter( int payloadSize, BigInteger payloadCounter ) throws RuntimeException {
|
||||||
|
byte[] returnData;
|
||||||
|
log.info( "--------------Putting record with data: {}", payloadCounter );
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
try {
|
||||||
|
returnData = mapper.writeValueAsBytes( payloadCounter );
|
||||||
|
} catch ( Exception e ) {
|
||||||
|
log.error( "Error creating payload data for {}", payloadCounter.toString() );
|
||||||
|
throw new RuntimeException( "Error converting object to bytes: ", e );
|
||||||
|
}
|
||||||
|
return ByteBuffer.wrap( returnData );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
package software.amazon.kinesis.integration_tests;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
|
||||||
|
import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
|
||||||
|
import software.amazon.kinesis.checkpoint.CheckpointConfig;
|
||||||
|
import software.amazon.kinesis.common.ConfigsBuilder;
|
||||||
|
import software.amazon.kinesis.common.InitialPositionInStreamExtended;
|
||||||
|
import software.amazon.kinesis.config.KCLAppConfig;
|
||||||
|
import software.amazon.kinesis.coordinator.CoordinatorConfig;
|
||||||
|
import software.amazon.kinesis.coordinator.Scheduler;
|
||||||
|
import software.amazon.kinesis.leases.LeaseManagementConfig;
|
||||||
|
import software.amazon.kinesis.lifecycle.LifecycleConfig;
|
||||||
|
import software.amazon.kinesis.metrics.MetricsConfig;
|
||||||
|
import software.amazon.kinesis.processor.ProcessorConfig;
|
||||||
|
import software.amazon.kinesis.retrieval.RetrievalConfig;
|
||||||
|
import software.amazon.kinesis.utils.RecordValidatorQueue;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
public class TestConsumerV2 extends TestConsumer {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger( TestConsumerV2.class );
|
||||||
|
private final int outOfOrderError = -1;
|
||||||
|
private final int missingRecordError = -2;
|
||||||
|
private MetricsConfig metricsConfig;
|
||||||
|
private RetrievalConfig retrievalConfig;
|
||||||
|
private CheckpointConfig checkpointConfig;
|
||||||
|
private CoordinatorConfig coordinatorConfig;
|
||||||
|
private LeaseManagementConfig leaseManagementConfig;
|
||||||
|
private LifecycleConfig lifecycleConfig;
|
||||||
|
private ProcessorConfig processorConfig;
|
||||||
|
|
||||||
|
public TestConsumerV2( KCLAppConfig consumerConfig ) {
|
||||||
|
super( consumerConfig );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() throws Exception {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if stream is created. If not, create it
|
||||||
|
*/
|
||||||
|
StreamExistenceManager.newManager( this.consumerConfig ).checkStreamAndCreateIfNecessary( this.streamName );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send dummy data to stream
|
||||||
|
*/
|
||||||
|
ScheduledExecutorService producerExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||||
|
ScheduledFuture<?> producerFuture = producerExecutor.scheduleAtFixedRate( this::publishRecord, 10, 1, TimeUnit.SECONDS );
|
||||||
|
|
||||||
|
RecordValidatorQueue recordValidator = new RecordValidatorQueue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup configuration of KCL (including DynamoDB and CloudWatch)
|
||||||
|
*/
|
||||||
|
DynamoDbAsyncClient dynamoClient = DynamoDbAsyncClient.builder().region( region ).build();
|
||||||
|
CloudWatchAsyncClient cloudWatchClient = CloudWatchAsyncClient.builder().region( region ).build();
|
||||||
|
ConfigsBuilder configsBuilder = new ConfigsBuilder( streamName, streamName, kinesisClient, dynamoClient, cloudWatchClient, UUID.randomUUID().toString(), new TestRecordProcessorFactoryV2( recordValidator ) );
|
||||||
|
|
||||||
|
|
||||||
|
retrievalConfig = consumerConfig.getRetrievalConfig();
|
||||||
|
checkpointConfig = configsBuilder.checkpointConfig();
|
||||||
|
coordinatorConfig = configsBuilder.coordinatorConfig();
|
||||||
|
leaseManagementConfig = configsBuilder.leaseManagementConfig()
|
||||||
|
.initialPositionInStream( InitialPositionInStreamExtended.newInitialPosition( consumerConfig.getKclInitialPosition() ) )
|
||||||
|
.initialLeaseTableReadCapacity( 50 ).initialLeaseTableWriteCapacity( 50 );
|
||||||
|
lifecycleConfig = configsBuilder.lifecycleConfig();
|
||||||
|
processorConfig = configsBuilder.processorConfig();
|
||||||
|
metricsConfig = configsBuilder.metricsConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Scheduler
|
||||||
|
*/
|
||||||
|
Scheduler scheduler = new Scheduler(
|
||||||
|
checkpointConfig,
|
||||||
|
coordinatorConfig,
|
||||||
|
leaseManagementConfig,
|
||||||
|
lifecycleConfig,
|
||||||
|
metricsConfig,
|
||||||
|
processorConfig,
|
||||||
|
retrievalConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start record processing of dummy data
|
||||||
|
*/
|
||||||
|
Thread schedulerThread = new Thread( scheduler );
|
||||||
|
schedulerThread.setDaemon( true );
|
||||||
|
schedulerThread.start();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sleep for two minutes to allow the producer/consumer to run and then end the test case.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
Thread.sleep( TimeUnit.SECONDS.toMillis( 60 * 2 ) ); // 60 * 2
|
||||||
|
} catch ( InterruptedException e ) {
|
||||||
|
throw new RuntimeException( e );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops sending dummy data.
|
||||||
|
*/
|
||||||
|
log.info( "Cancelling producer and shutting down executor." );
|
||||||
|
producerFuture.cancel( true );
|
||||||
|
producerExecutor.shutdownNow();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait a few seconds for the last few records to be processed
|
||||||
|
*/
|
||||||
|
Thread.sleep( TimeUnit.SECONDS.toMillis( 10 ) );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops consuming data. Finishes processing the current batch of data already received from Kinesis
|
||||||
|
* before shutting down.
|
||||||
|
*/
|
||||||
|
Future<Boolean> gracefulShutdownFuture = scheduler.startGracefulShutdown();
|
||||||
|
log.info( "Waiting up to 20 seconds for shutdown to complete." );
|
||||||
|
try {
|
||||||
|
gracefulShutdownFuture.get( 20, TimeUnit.SECONDS );
|
||||||
|
} catch ( InterruptedException e ) {
|
||||||
|
log.info( "Interrupted while waiting for graceful shutdown. Continuing." );
|
||||||
|
} catch ( ExecutionException e ) {
|
||||||
|
log.error( "Exception while executing graceful shutdown.", e );
|
||||||
|
} catch ( TimeoutException e ) {
|
||||||
|
log.error( "Timeout while waiting for shutdown. Scheduler may not have exited." );
|
||||||
|
}
|
||||||
|
log.info( "Completed, shutting down now." );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate processed data
|
||||||
|
*/
|
||||||
|
int errorVal = recordValidator.validateRecords( successfulPutRecords );
|
||||||
|
if ( errorVal == outOfOrderError ) {
|
||||||
|
throw new RuntimeException( "There was an error validating the records that were processed. The records were out of order" );
|
||||||
|
} else if ( errorVal == missingRecordError ) {
|
||||||
|
throw new RuntimeException( "There was an error validating the records that were processed. Some records were missing." );
|
||||||
|
}
|
||||||
|
log.info( "--------------Completed validation of processed records.--------------" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package software.amazon.kinesis.integration_tests;
|
||||||
|
|
||||||
|
import software.amazon.kinesis.processor.ShardRecordProcessor;
|
||||||
|
import software.amazon.kinesis.processor.ShardRecordProcessorFactory;
|
||||||
|
import software.amazon.kinesis.utils.RecordValidatorQueue;
|
||||||
|
|
||||||
|
public class TestRecordProcessorFactoryV2 implements ShardRecordProcessorFactory {
|
||||||
|
|
||||||
|
RecordValidatorQueue recordValidator;
|
||||||
|
|
||||||
|
public TestRecordProcessorFactoryV2( RecordValidatorQueue recordValidator ) {
|
||||||
|
this.recordValidator = recordValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ShardRecordProcessor shardRecordProcessor() {
|
||||||
|
return new TestRecordProcessorV2( this.recordValidator );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
package software.amazon.kinesis.integration_tests;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.slf4j.MDC;
|
||||||
|
import software.amazon.kinesis.exceptions.InvalidStateException;
|
||||||
|
import software.amazon.kinesis.exceptions.ShutdownException;
|
||||||
|
import software.amazon.kinesis.lifecycle.events.LeaseLostInput;
|
||||||
|
import software.amazon.kinesis.lifecycle.events.ShardEndedInput;
|
||||||
|
import software.amazon.kinesis.lifecycle.events.ShutdownRequestedInput;
|
||||||
|
import software.amazon.kinesis.processor.ShardRecordProcessor;
|
||||||
|
import software.amazon.kinesis.lifecycle.events.InitializationInput;
|
||||||
|
import software.amazon.kinesis.retrieval.KinesisClientRecord;
|
||||||
|
import software.amazon.kinesis.utils.RecordValidatorQueue;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
public class TestRecordProcessorV2 implements ShardRecordProcessor {
|
||||||
|
|
||||||
|
private static final String SHARD_ID_MDC_KEY = "ShardId";
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger( TestRecordProcessorV2.class );
|
||||||
|
|
||||||
|
private String shardId;
|
||||||
|
|
||||||
|
RecordValidatorQueue recordValidator;
|
||||||
|
|
||||||
|
public TestRecordProcessorV2( RecordValidatorQueue recordValidator ) {
|
||||||
|
this.recordValidator = recordValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize( InitializationInput initializationInput ) {
|
||||||
|
shardId = initializationInput.shardId();
|
||||||
|
MDC.put( SHARD_ID_MDC_KEY, shardId );
|
||||||
|
try {
|
||||||
|
log.info( "Initializing @ Sequence: {}", initializationInput.extendedSequenceNumber() );
|
||||||
|
} finally {
|
||||||
|
MDC.remove( SHARD_ID_MDC_KEY );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processRecords( software.amazon.kinesis.lifecycle.events.ProcessRecordsInput processRecordsInput ) {
|
||||||
|
MDC.put( SHARD_ID_MDC_KEY, shardId );
|
||||||
|
try {
|
||||||
|
log.info( "Processing {} record(s)", processRecordsInput.records().size() );
|
||||||
|
|
||||||
|
for ( KinesisClientRecord r : processRecordsInput.records() ) {
|
||||||
|
String data = new String( asByteArray( r.data() ) );
|
||||||
|
log.info( "Processing record pk: {}", data );
|
||||||
|
recordValidator.add( shardId, data );
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch ( Throwable t ) {
|
||||||
|
log.error( "Caught throwable while processing records. Aborting.", t );
|
||||||
|
Runtime.getRuntime().halt( 1 );
|
||||||
|
} finally {
|
||||||
|
MDC.remove( SHARD_ID_MDC_KEY );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] asByteArray( ByteBuffer buf ) {
|
||||||
|
byte[] bytes = new byte[buf.remaining()];
|
||||||
|
buf.get( bytes );
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void leaseLost( LeaseLostInput leaseLostInput ) {
|
||||||
|
MDC.put( SHARD_ID_MDC_KEY, shardId );
|
||||||
|
try {
|
||||||
|
log.info( "Lost lease, so terminating." );
|
||||||
|
} finally {
|
||||||
|
MDC.remove( SHARD_ID_MDC_KEY );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shardEnded( ShardEndedInput shardEndedInput ) {
|
||||||
|
MDC.put( SHARD_ID_MDC_KEY, shardId );
|
||||||
|
try {
|
||||||
|
log.info( "Reached shard end checkpointing." );
|
||||||
|
shardEndedInput.checkpointer().checkpoint();
|
||||||
|
} catch ( ShutdownException | InvalidStateException e ) {
|
||||||
|
log.error( "Exception while checkpointing at shard end. Giving up.", e );
|
||||||
|
} finally {
|
||||||
|
MDC.remove( SHARD_ID_MDC_KEY );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdownRequested( ShutdownRequestedInput shutdownRequestedInput ) {
|
||||||
|
MDC.put( SHARD_ID_MDC_KEY, shardId );
|
||||||
|
try {
|
||||||
|
log.info( "Scheduler is shutting down, checkpointing." );
|
||||||
|
shutdownRequestedInput.checkpointer().checkpoint();
|
||||||
|
} catch ( ShutdownException | InvalidStateException e ) {
|
||||||
|
log.error( "Exception while checkpointing at requested shutdown. Giving up.", e );
|
||||||
|
} finally {
|
||||||
|
MDC.remove( SHARD_ID_MDC_KEY );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package software.amazon.kinesis.utils;
|
||||||
|
|
||||||
|
public enum KCLVersion {
|
||||||
|
KCL1X,
|
||||||
|
KCL2X
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
package software.amazon.kinesis.utils;
|
||||||
|
|
||||||
|
import com.amazonaws.auth.AWSCredentials;
|
||||||
|
import com.amazonaws.auth.AWSCredentialsProvider;
|
||||||
|
import com.google.common.io.CharStreams;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsCredentials;
|
||||||
|
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to hold odin credentials because odin is not available externally and this package doesn't use brazil.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class OdinCredentialsHelper {
|
||||||
|
|
||||||
|
private final static String PRINCIPAL = "Principal";
|
||||||
|
private final static String CREDENTIAL = "Credential";
|
||||||
|
private final static String ODIN_COMMAND = "/apollo/env/envImprovement/bin/odin-get -t";
|
||||||
|
|
||||||
|
private static String getMaterial(String materialName, String materialType) throws IOException {
|
||||||
|
final InputStream inputStream = Runtime.getRuntime().exec(String.format("%s %s %s", ODIN_COMMAND, materialType, materialName)).getInputStream();
|
||||||
|
return CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).trim();
|
||||||
|
}
|
||||||
|
private static String getPrincipal(String materialName) throws IOException {
|
||||||
|
return getMaterial(materialName, PRINCIPAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getCredential(String materialName) throws IOException {
|
||||||
|
return getMaterial(materialName, CREDENTIAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to pull credentials from odin for testing for AWS SDK sync clients (1.x).
|
||||||
|
*
|
||||||
|
* @param materialName name of the material set to fetch.
|
||||||
|
* @return access/secret key pair from Odin if specified for testing.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static AWSCredentialsProvider getSyncAwsCredentialsFromMaterialSet(String materialName) throws IOException {
|
||||||
|
if (materialName == null) {
|
||||||
|
log.debug("No material name found.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Fetching credentials for material - {}.", materialName);
|
||||||
|
|
||||||
|
final String principal = getPrincipal(materialName);
|
||||||
|
final String credential = getCredential(materialName);
|
||||||
|
|
||||||
|
final AWSCredentialsProvider awsCredentialsProvider = new AWSCredentialsProvider() {
|
||||||
|
@Override
|
||||||
|
public AWSCredentials getCredentials() {
|
||||||
|
return new AWSCredentials() {
|
||||||
|
@Override
|
||||||
|
public String getAWSAccessKeyId() {
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAWSSecretKey() {
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void refresh() {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug("Successfully retrieved credentials from odin. Access key - {}.", principal);
|
||||||
|
|
||||||
|
return awsCredentialsProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to pull credentials from odin for testing for AWS SDK async clients (2.x).
|
||||||
|
*
|
||||||
|
* @param materialName name of the material set to fetch.
|
||||||
|
* @return access/secret key pair from Odin if specified for testing.
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static AwsCredentialsProvider getAsyncAwsCredentialsFromMaterialSet(String materialName) throws IOException {
|
||||||
|
if (materialName == null) {
|
||||||
|
log.debug("No material name found.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Fetching credentials for material - {}.", materialName);
|
||||||
|
|
||||||
|
final String principal = getPrincipal(materialName);
|
||||||
|
final String credential = getCredential(materialName);
|
||||||
|
|
||||||
|
final AwsCredentialsProvider awsCredentialsProvider = () -> new AwsCredentials() {
|
||||||
|
@Override
|
||||||
|
public String accessKeyId() {
|
||||||
|
return principal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String secretAccessKey() {
|
||||||
|
return credential;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug("Successfully retrieved credentials from odin. Access key - {}.", principal);
|
||||||
|
|
||||||
|
return awsCredentialsProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
package software.amazon.kinesis.utils;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class RecordValidatorQueue {
|
||||||
|
|
||||||
|
HashMap<String, List<String>> dict = new HashMap<>();
|
||||||
|
|
||||||
|
public void add( String shardId, String data ) {
|
||||||
|
if ( dict.containsKey( shardId ) ) {
|
||||||
|
// Just add the data to this item
|
||||||
|
List<String> oldVal = dict.get( shardId );
|
||||||
|
oldVal.add( data );
|
||||||
|
dict.put( shardId, oldVal );
|
||||||
|
} else {
|
||||||
|
List<String> newVal = new ArrayList<>();
|
||||||
|
newVal.add( data );
|
||||||
|
dict.put( shardId, newVal );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int validateRecords( int trueTotalShardCount ) {
|
||||||
|
// Validate that each List in the HashMap has data records in increasing order
|
||||||
|
|
||||||
|
boolean incOrder = true;
|
||||||
|
for ( Map.Entry<String, List<String>> entry : dict.entrySet() ) {
|
||||||
|
List<String> recordsPerShard = entry.getValue();
|
||||||
|
int prevVal = -1;
|
||||||
|
boolean shardIncOrder = true;
|
||||||
|
for ( String record : recordsPerShard ) {
|
||||||
|
int nextVal = Integer.parseInt( record );
|
||||||
|
if ( prevVal > nextVal ) {
|
||||||
|
log.error("The records are not in increasing order. Saw record data {} before {}.", prevVal, nextVal );
|
||||||
|
shardIncOrder = false;
|
||||||
|
}
|
||||||
|
prevVal = nextVal;
|
||||||
|
}
|
||||||
|
if ( !shardIncOrder ) {
|
||||||
|
incOrder = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is true, then there was some record that was processed out of order
|
||||||
|
if ( !incOrder ) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that no records are missing over all shards
|
||||||
|
int totalShardCount = 0;
|
||||||
|
for ( Map.Entry<String, List<String>> entry : dict.entrySet() ) {
|
||||||
|
List<String> recordsPerShard = entry.getValue();
|
||||||
|
Set<String> noDupRecords = new HashSet<String>( recordsPerShard );
|
||||||
|
totalShardCount += noDupRecords.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is true, then there was some record that was missed during processing.
|
||||||
|
if ( totalShardCount != trueTotalShardCount ) {
|
||||||
|
log.error( "Failed to get correct number of records processed. Should be {} but was {}", trueTotalShardCount, totalShardCount );
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue